pw_stream: Create module
pw_stream provides a generic writer interface that allows modules to
write data without assuming anything about what the data is written to.
A pw::stream::Writer could be backed by RAM, flash memory, or just pipe
the data out over a communication channel.
Change-Id: Ie8f8a66819a2c80a47f7cc75e2c9d1bbe25a668d
diff --git a/BUILD.gn b/BUILD.gn
index 4b78b31..f9c1e7a 100644
--- a/BUILD.gn
+++ b/BUILD.gn
@@ -60,6 +60,7 @@
"$dir_pw_result",
"$dir_pw_span",
"$dir_pw_status",
+ "$dir_pw_stream",
"$dir_pw_string",
"$dir_pw_unit_test",
"$dir_pw_varint",
@@ -97,6 +98,7 @@
"$dir_pw_rpc:tests",
"$dir_pw_span:tests",
"$dir_pw_status:tests",
+ "$dir_pw_stream:tests",
"$dir_pw_string:tests",
"$dir_pw_tokenizer:tests",
"$dir_pw_unit_test:tests",
diff --git a/docs/BUILD.gn b/docs/BUILD.gn
index 703dac2..d89057b 100644
--- a/docs/BUILD.gn
+++ b/docs/BUILD.gn
@@ -87,6 +87,7 @@
"$dir_pw_rpc:docs",
"$dir_pw_span:docs",
"$dir_pw_status:docs",
+ "$dir_pw_stream:docs",
"$dir_pw_string:docs",
"$dir_pw_sys_io:docs",
"$dir_pw_sys_io_baremetal_stm32f429:docs",
diff --git a/modules.gni b/modules.gni
index 244b0e3..9ca6bd8 100644
--- a/modules.gni
+++ b/modules.gni
@@ -48,6 +48,7 @@
dir_pw_rpc = "$dir_pigweed/pw_rpc"
dir_pw_span = "$dir_pigweed/pw_span"
dir_pw_status = "$dir_pigweed/pw_status"
+dir_pw_stream = "$dir_pigweed/pw_stream"
dir_pw_string = "$dir_pigweed/pw_string"
dir_pw_sys_io = "$dir_pigweed/pw_sys_io"
dir_pw_sys_io_baremetal_lm3s6965evb =
diff --git a/pw_stream/BUILD b/pw_stream/BUILD
new file mode 100644
index 0000000..1f3244a
--- /dev/null
+++ b/pw_stream/BUILD
@@ -0,0 +1,59 @@
+# Copyright 2020 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+
+load(
+ "//pw_build:pigweed.bzl",
+ "pw_cc_library",
+ "pw_cc_test",
+)
+
+package(default_visibility = ["//visibility:public"])
+
+licenses(["notice"]) # Apache License 2.0
+
+pw_cc_library(
+ name = "pw_stream",
+ hdrs = [
+ "public/pw_stream/buffered_stream.h",
+ "public/pw_stream/memory_stream.h",
+ "public/pw_stream/stream.h",
+ ],
+ srcs = [
+ "memory_stream.cc",
+ "buffered_stream.cc",
+ ],
+ includes = ["public"],
+)
+
+pw_cc_test(
+ name = "memory_stream_test",
+ srcs = [
+ "memory_stream_test.cc",
+ ],
+ deps = [
+ ":pw_stream",
+ "//pw_unit_test",
+ ],
+)
+
+pw_cc_test(
+ name = "buffered_stream_test",
+ srcs = [
+ "buffered_stream_test.cc",
+ ],
+ deps = [
+ ":pw_stream",
+ "//pw_unit_test",
+ ],
+)
\ No newline at end of file
diff --git a/pw_stream/BUILD.gn b/pw_stream/BUILD.gn
new file mode 100644
index 0000000..311eb1b
--- /dev/null
+++ b/pw_stream/BUILD.gn
@@ -0,0 +1,48 @@
+# Copyright 2020 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+
+import("$dir_pw_docgen/docs.gni")
+import("$dir_pw_unit_test/test.gni")
+
+config("default_config") {
+ include_dirs = [ "public" ]
+}
+
+source_set("pw_stream") {
+ public_configs = [ ":default_config" ]
+ public = [
+ "public/pw_stream/memory_stream.h",
+ "public/pw_stream/stream.h",
+ ]
+ sources = [ "memory_stream.cc" ]
+ public_deps = [
+ dir_pw_assert,
+ dir_pw_preprocessor,
+ dir_pw_span,
+ dir_pw_status,
+ ]
+}
+
+pw_doc_group("docs") {
+ sources = [ "docs.rst" ]
+}
+
+pw_test_group("tests") {
+ tests = [ ":memory_stream_test" ]
+}
+
+pw_test("memory_stream_test") {
+ sources = [ "memory_stream_test.cc" ]
+ deps = [ ":pw_stream" ]
+}
diff --git a/pw_stream/docs.rst b/pw_stream/docs.rst
new file mode 100644
index 0000000..753a0e3
--- /dev/null
+++ b/pw_stream/docs.rst
@@ -0,0 +1,203 @@
+.. _chapter-stream:
+
+.. default-domain:: cpp
+
+.. highlight:: sh
+
+---------
+pw_stream
+---------
+
+``pw_stream`` provides a foundational interface for streaming data from one part
+of a system to another. In the simplest use cases, this is basically a memcpy
+behind a reusable interface that can be passed around the system. On the other
+hand, the flexibility of this interface means a ``pw_stream`` could terminate is
+something more complex, like a UART stream or flash memory.
+
+Overview
+========
+At the most basic level, ``pw_stream``'s interfaces provide very simple handles
+to enabling streaming data from one location in a system to an endpoint.
+
+Example:
+
+.. code-block:: cpp
+
+ void DumpSensorData(pw::stream::Writer& writer) {
+ static char temp[64];
+ ImuSample imu_sample;
+ imu.GetSample(&info);
+ size_t bytes_written = imu_sample.AsCsv(temp, sizeof(temp));
+ writer.Write(temp, bytes_written);
+ }
+
+In this example, ``DumpSensorData()`` only cares that it has access to a
+``Writer`` that it can use to stream data to using ``Writer::Write()``. The
+``Writer`` itself can be backed by anything that can act as a data "sink."
+
+
+pw::stream::Writer
+------------------
+This is the foundational stream ``Writer`` abstract class. Any class that wishes
+to implement the ``Writer`` interface **must** provide a ``DoWrite()``
+implementation. Note that ``Write()`` itself is **not** virtual, and should not
+be overridden.
+
+Buffering
+^^^^^^^^^
+If any buffering occurs in a ``Writer`` and data must be flushed before it is
+fully committed to the sink, a ``Writer`` may optionally override ``Flush()``.
+Writes to a buffer may optimistically return ``Status::OK``, so depend on the
+return value of ``Flush()`` to ensure any pending data is committed to a sink.
+
+Generally speaking, the scope that instantiates the concrete ``Writer`` class
+should be in charge of calling ``Flush()``, and functions that only have access
+to the Writer interface should avoid calling this function.
+
+pw::stream::MemoryWriter
+------------------------
+The ``MemoryWriter`` class implements the ``Writer`` interface by backing the
+data destination with an **externally-provided** memory buffer.
+``MemoryWriterBuffer`` extends ``MemoryWriter`` to internally provide a memory
+buffer.
+
+Why use pw_stream?
+==================
+
+Standard API
+------------
+``pw_stream`` provides a standard way for classes to express that they have the
+ability to write data. Writing to one sink versus another sink is a matter of
+just passing a reference to the appropriate ``Writer``.
+
+As an example, imagine dumping sensor data. If written against a random HAL
+or one-off class, there's porting work required to write to a different sink
+(imagine writing over UART vs dumping to flash memory). Building a "dumping"
+implementation against the ``Writer`` interface prevents a dependency from
+forming on an artisainal API that would require porting work.
+
+Similarly, after building a ``Writer`` implementation for a Sink that data
+could be dumped to, that same ``Writer`` can be reused for other contexts that
+already write data to the ``pw::stream::Writer`` interface.
+
+Before:
+
+.. code-block:: cpp
+
+ // Not reusable, depends on `Uart`.
+ void DumpSensorData(Uart& uart) {
+ static char temp[64];
+ ImuSample imu_sample;
+ imu.GetSample(&info);
+ size_t bytes_written = imu_sample.AsCsv(temp, sizeof(temp));
+ uart.Transmit(temp, bytes_written, /*timeout_ms=*/ 200);
+ }
+
+After:
+
+.. code-block:: cpp
+
+ // Reusable; no more Uart dependency!
+ void DumpSensorData(Writer& writer) {
+ static char temp[64];
+ ImuSample imu_sample;
+ imu.GetSample(&info);
+ size_t bytes_written = imu_sample.AsCsv(temp, sizeof(temp));
+ writer.Write(temp, bytes_written);
+ }
+
+Reduce intermediate buffers
+---------------------------
+Often functions that write larger blobs of data request a buffer is passed as
+the destination that data should be written to. This *requires* a buffer is
+allocated, even if the data only exists in that buffer for a very short period
+of time before it's written somewhere else.
+
+In situations where data read from somewhere will immediately be written
+somewhere else, a ``Writer`` interface can cut out the middleman buffer.
+
+Before:
+
+.. code-block:: cpp
+
+ // Requires an intermediate buffer to write the data as CSV.
+ void DumpSensorData(Uart* uart) {
+ char temp[64];
+ ImuSample imu_sample;
+ imu.GetSample(&info);
+ size_t bytes_written = imu_sample.AsCsv(temp, sizeof(temp));
+ uart.Transmit(temp, bytes_written, /*timeout_ms=*/ 200);
+ }
+
+After:
+
+.. code-block:: cpp
+
+ // Both DumpSensorData() and RawSample::AsCsv() use a Writer, eliminating the
+ // need for an intermediate buffer.
+ void DumpSensorData(Writer* writer) {
+ RawSample imu_sample;
+ imu.GetSample(&info);
+ imu_sample.AsCsv(writer);
+ }
+
+Prevent buffer overflow
+-----------------------
+When copying data from one buffer to another, there must be checks to ensure the
+copy does not overflow the destination buffer. As this sort of logic is
+duplicated throughout a codebase, there's more opportunities for bound-checking
+bugs to sneak in. ``Writers`` manage this logic internally rather than pushing
+the bounds checking to the code that is moving or writing the data.
+
+Similarly, since only the ``Writer`` has access to any underlying buffers, it's
+harder for functions that share a ``Writer`` to accidentally clobber data
+written by others using the same buffer.
+
+Before:
+
+.. code-block:: cpp
+
+ Status BuildPacket(Id dest, span<const std::byte> payload,
+ span<std::byte> dest) {
+ Header header;
+ if (dest.size_bytes() + payload.size_bytes() < sizeof(Header)) {
+ return Status::RESOURCE_EXHAUSTED;
+ }
+ header.dest = dest;
+ header.src = DeviceId();
+ header.payload_size = payload.size_bytes();
+
+ memcpy(dest.data(), &header, sizeof(header));
+ // Forgetting this line would clobber buffer contents. Also, using
+ // a temporary span instead could leave `dest` to be misused elsewhere in
+ // the function.
+ dest = dest.subspan(sizeof(header));
+ memcpy(dest.data(), payload.data(), payload.size_bytes());
+ }
+
+After:
+
+.. code-block:: cpp
+
+ Status BuildPacket(Id dest, span<const std::byte> payload, Writer& writer) {
+ Header header;
+ header.dest = dest;
+ header.src = DeviceId();
+ header.payload_size = payload.size_bytes();
+
+ writer.Write(header);
+ return writer.Write(payload);
+ }
+
+Why NOT pw_stream?
+==================
+pw_stream provides a virtual interface. This inherently has more overhead than
+a regular function call. In extremely performance-sensitive contexts, a virtual
+interface might not provide enough utility to justify the performance cost.
+
+Dependencies
+============
+ * ``pw_assert`` module
+ * ``pw_preprocessor`` module
+ * ``pw_status`` module
+ * ``pw_span`` module
diff --git a/pw_stream/memory_stream.cc b/pw_stream/memory_stream.cc
new file mode 100644
index 0000000..efe2c97
--- /dev/null
+++ b/pw_stream/memory_stream.cc
@@ -0,0 +1,36 @@
+// Copyright 2020 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+#include "pw_stream/memory_stream.h"
+
+#include <cstddef>
+#include <cstring>
+
+#include "pw_status/status_with_size.h"
+
+namespace pw::stream {
+
+Status MemoryWriter::DoWrite(span<const std::byte> data) {
+ size_t bytes_to_write =
+ std::min(data.size_bytes(), dest_.size_bytes() - bytes_written_);
+ std::memcpy(dest_.data() + bytes_written_, data.data(), bytes_to_write);
+ bytes_written_ += bytes_to_write;
+
+ if (bytes_to_write != data.size_bytes()) {
+ return Status::RESOURCE_EXHAUSTED;
+ }
+ return Status::OK;
+}
+
+} // namespace pw::stream
\ No newline at end of file
diff --git a/pw_stream/memory_stream_test.cc b/pw_stream/memory_stream_test.cc
new file mode 100644
index 0000000..ada85ab
--- /dev/null
+++ b/pw_stream/memory_stream_test.cc
@@ -0,0 +1,101 @@
+// Copyright 2020 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+#include "pw_stream/memory_stream.h"
+
+#include "gtest/gtest.h"
+#include "pw_preprocessor/compiler.h"
+
+namespace pw::stream {
+namespace {
+
+// Size of the in-memory buffer to use for this test.
+constexpr size_t kSinkBufferSize = 1013;
+
+struct TestStruct {
+ uint8_t day;
+ uint8_t month;
+ uint16_t year;
+};
+
+constexpr TestStruct kExpectedStruct = {.day = 18, .month = 5, .year = 2020};
+
+std::array<std::byte, kSinkBufferSize> memory_buffer;
+
+TEST(MemoryWriter, BytesWritten) {
+ MemoryWriter memory_writer(memory_buffer);
+ EXPECT_EQ(memory_writer.bytes_written(), 0u);
+ Status status =
+ memory_writer.Write(&kExpectedStruct, sizeof(kExpectedStruct));
+ EXPECT_TRUE(status.ok());
+ EXPECT_EQ(memory_writer.bytes_written(), sizeof(kExpectedStruct));
+}
+
+TEST(MemoryWriter, ValidateContents) {
+ MemoryWriter memory_writer(memory_buffer);
+ EXPECT_TRUE(
+ memory_writer.Write(&kExpectedStruct, sizeof(kExpectedStruct)).ok());
+
+ span<const std::byte> written_data = memory_writer.WrittenData();
+ EXPECT_EQ(written_data.size_bytes(), sizeof(kExpectedStruct));
+ TestStruct temp;
+ std::memcpy(&temp, written_data.data(), written_data.size_bytes());
+ EXPECT_EQ(memcmp(&temp, &kExpectedStruct, sizeof(kExpectedStruct)), 0);
+}
+
+TEST(MemoryWriter, MultipleWrites) {
+ constexpr size_t kTempBufferSize = 72;
+ std::byte buffer[kTempBufferSize] = {};
+ size_t counter = 0;
+
+ MemoryWriter memory_writer(memory_buffer);
+
+ do {
+ for (size_t i = 0; i < sizeof(buffer); ++i) {
+ buffer[i] = std::byte(counter++);
+ }
+ } while (memory_writer.Write(span(buffer)) != Status::RESOURCE_EXHAUSTED);
+
+ // Ensure that we counted up to at least the sink buffer size. This can be
+ // more since we write to the sink via in intermediate buffer.
+ EXPECT_GE(counter, kSinkBufferSize);
+
+ EXPECT_EQ(memory_writer.bytes_written(), kSinkBufferSize);
+
+ counter = 0;
+ for (const std::byte& value : memory_writer.WrittenData()) {
+ EXPECT_EQ(value, std::byte(counter++));
+ }
+}
+
+TEST(MemoryWriter, EmptyData) {
+ std::byte buffer[5] = {};
+
+ MemoryWriter memory_writer(memory_buffer);
+ EXPECT_TRUE(memory_writer.Write(buffer, 0).ok());
+ EXPECT_EQ(memory_writer.bytes_written(), 0u);
+}
+
+#if CHECK_TEST_CRASHES
+
+// TODO(amontanez): Ensure that this test triggers an assert.
+TEST(MemoryWriter, NullPointer) {
+ MemoryWriter memory_writer(memory_buffer);
+ memory_writer.Write(nullptr, 21);
+}
+
+#endif // CHECK_TEST_CRASHES
+
+} // namespace
+} // namespace pw::stream
\ No newline at end of file
diff --git a/pw_stream/public/pw_stream/memory_stream.h b/pw_stream/public/pw_stream/memory_stream.h
new file mode 100644
index 0000000..ad9d30d
--- /dev/null
+++ b/pw_stream/public/pw_stream/memory_stream.h
@@ -0,0 +1,56 @@
+// Copyright 2020 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+#pragma once
+
+#include <array>
+#include <cstddef>
+
+#include "pw_span/span.h"
+#include "pw_stream/stream.h"
+
+namespace pw::stream {
+
+class MemoryWriter : public Writer {
+ public:
+ MemoryWriter(span<std::byte> dest) : dest_(dest) {}
+
+ size_t bytes_written() const { return bytes_written_; }
+
+ span<const std::byte> WrittenData() const {
+ return dest_.first(bytes_written_);
+ }
+
+ const std::byte* data() const { return dest_.data(); }
+
+ private:
+ // Implementation for writing data to this stream.
+ //
+ // If the in-memory buffer is exhausted in the middle of a write, this will
+ // perform a partial write and Status::RESOURCE_EXHAUSTED will be returned.
+ Status DoWrite(span<const std::byte> data) override;
+
+ span<std::byte> dest_;
+ size_t bytes_written_ = 0;
+};
+
+template <size_t size_bytes>
+class MemoryWriterBuffer : public MemoryWriter {
+ public:
+ MemoryWriterBuffer() : MemoryWriter(buffer_) {}
+
+ private:
+ std::array<std::byte, size_bytes> buffer_;
+};
+
+} // namespace pw::stream
diff --git a/pw_stream/public/pw_stream/stream.h b/pw_stream/public/pw_stream/stream.h
new file mode 100644
index 0000000..6d51191
--- /dev/null
+++ b/pw_stream/public/pw_stream/stream.h
@@ -0,0 +1,58 @@
+// Copyright 2020 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+#pragma once
+
+#include <array>
+#include <cstddef>
+
+#include "pw_assert/assert.h"
+#include "pw_span/span.h"
+#include "pw_status/status.h"
+
+namespace pw::stream {
+
+// General-purpose writer interface.
+class Writer {
+ public:
+ // There are no requirements around if or how a `Writer` should call Flush()
+ // at the time of object destruction. In general, do not assume a `Writer`
+ // will automatically call Flush when destroyed.
+ virtual ~Writer() = default;
+
+ // Write data to this stream Writer. If the writer runs out of resources, it
+ // will return Status::RESOURCE_EXHAUSTED. The derived class choses whether
+ // to do partial writes in this case, or abort the entire write operation.
+ //
+ // Derived classes should NOT try to override these public write methods.
+ // Instead, provide an implementation by overriding DoWrite().
+ Status Write(span<const std::byte> data) {
+ PW_DCHECK(data.empty() || data.data() != nullptr);
+ return DoWrite(data);
+ }
+ Status Write(const void* data, size_t size_bytes) {
+ return Write(span(static_cast<const std::byte*>(data), size_bytes));
+ }
+
+ // Flush any buffered data, finalizing all writes.
+ //
+ // Generally speaking, the scope that instantiates the concrete `Writer`
+ // class should be in charge of calling `Flush()`, and functions that only
+ // have access to the Writer interface should avoid calling this function.
+ virtual Status Flush() { return Status::OK; }
+
+ private:
+ virtual Status DoWrite(span<const std::byte> data) = 0;
+};
+
+} // namespace pw::stream