blob: 3acca0013059be266961a79634c11397cf1ce1d2 [file] [log] [blame]
.. _module-pw_stream:
.. cpp:namespace-push:: pw::stream
=========
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
Status 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));
return writer.Write(temp, bytes_written);
}
In this example, ``DumpSensorData()`` only cares that it has access to a
:cpp:class:`Writer` that it can use to stream data to using ``Writer::Write()``.
The :cpp:class:`Writer` itself can be backed by anything that can act as a data
"sink."
---------------------
pw::stream Interfaces
---------------------
There are three basic capabilities of a stream:
* Reading -- Bytes can be read from the stream.
* Writing -- Bytes can be written to the stream.
* Seeking -- The position in the stream can be changed.
``pw_stream`` provides a family of stream classes with different capabilities.
The most basic class, :cpp:class:`Stream` guarantees no functionality, while the
most capable class, :cpp:class:`SeekableReaderWriter` supports reading, writing,
and seeking.
Usage overview
==============
.. list-table::
:header-rows: 1
* - pw::stream Interfaces
- Accept in APIs?
- Extend to create new stream?
* - :cpp:class:`pw::stream::Stream`
- ❌
- ❌
* - | :cpp:class:`pw::stream::Reader`
| :cpp:class:`pw::stream::Writer`
| :cpp:class:`pw::stream::ReaderWriter`
- ✅
- ❌
* - | :cpp:class:`pw::stream::SeekableReader`
| :cpp:class:`pw::stream::SeekableWriter`
| :cpp:class:`pw::stream::SeekableReaderWriter`
- ✅
- ✅
* - | :cpp:class:`pw::stream::RelativeSeekableReader`
| :cpp:class:`pw::stream::RelativeSeekableWriter`
| :cpp:class:`pw::stream::RelativeSeekableReaderWriter`
- ✅ (rarely)
- ✅
* - | :cpp:class:`pw::stream::NonSeekableReader`
| :cpp:class:`pw::stream::NonSeekableWriter`
| :cpp:class:`pw::stream::NonSeekableReaderWriter`
- ❌
- ✅
Interface documentation
=======================
Summary documentation for the ``pw_stream`` interfaces is below. See the API
comments in `pw_stream/public/pw_stream/stream.h
<https://cs.pigweed.dev/pigweed/+/main:pw_stream/public/pw_stream/stream.h>`_
for full details.
.. doxygenclass:: pw::stream::Stream
:members:
:private-members:
Reader interfaces
-----------------
.. doxygenclass:: pw::stream::Reader
:members:
.. doxygenclass:: pw::stream::SeekableReader
:members:
.. doxygenclass:: pw::stream::RelativeSeekableReader
:members:
.. doxygenclass:: pw::stream::NonSeekableReader
:members:
Writer interfaces
-----------------
.. doxygenclass:: pw::stream::Writer
:members:
.. doxygenclass:: pw::stream::SeekableWriter
:members:
.. doxygenclass:: pw::stream::RelativeSeekableWriter
:members:
.. doxygenclass:: pw::stream::NonSeekableWriter
:members:
ReaderWriter interfaces
-----------------------
.. doxygenclass:: pw::stream::ReaderWriter
:members:
.. doxygenclass:: pw::stream::SeekableReaderWriter
:members:
.. doxygenclass:: pw::stream::RelativeSeekableReaderWriter
:members:
.. doxygenclass:: pw::stream::NonSeekableReaderWriter
:members:
---------------
Implementations
---------------
``pw_stream`` includes a few stream implementations for general use.
.. cpp:class:: MemoryWriter : public SeekableWriter
The ``MemoryWriter`` class implements the :cpp:class:`Writer` interface by
backing the data destination with an **externally-provided** memory buffer.
``MemoryWriterBuffer`` extends ``MemoryWriter`` to internally provide a memory
buffer.
The ``MemoryWriter`` can be accessed like a standard C++ container. The
contents grow as data is written.
.. cpp:class:: MemoryReader : public SeekableReader
The ``MemoryReader`` class implements the :cpp:class:`Reader` interface by
backing the data source with an **externally-provided** memory buffer.
.. cpp:class:: NullStream : public SeekableReaderWriter
``NullStream`` is a no-op stream implementation, similar to ``/dev/null``.
Writes are always dropped. Reads always return ``OUT_OF_RANGE``. Seeks have no
effect.
.. cpp:class:: CountingNullStream : public SeekableReaderWriter
``CountingNullStream`` is a no-op stream implementation, like
:cpp:class:`NullStream`, that counts the number of bytes written.
.. cpp:function:: size_t bytes_written() const
Returns the number of bytes provided to previous ``Write()`` calls.
.. cpp:class:: StdFileWriter : public SeekableWriter
``StdFileWriter`` wraps an ``std::ofstream`` with the :cpp:class:`Writer`
interface.
.. cpp:class:: StdFileReader : public SeekableReader
``StdFileReader`` wraps an ``std::ifstream`` with the :cpp:class:`Reader`
interface.
.. cpp:class:: SocketStream : public NonSeekableReaderWriter
``SocketStream`` wraps posix-style TCP sockets with the :cpp:class:`Reader`
and :cpp:class:`Writer` interfaces. It can be used to connect to a TCP server,
or to communicate with a client via the ``ServerSocket`` class.
.. cpp:class:: ServerSocket
``ServerSocket`` wraps a posix server socket, and produces a
:cpp:class:`SocketStream` for each accepted client connection.
------------------
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 :cpp:class:`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 :cpp:class:`Writer` interface prevents a dependency
on a bespoke API that would require porting work.
Similarly, after building a :cpp:class:`Writer` implementation for a Sink that
data could be dumped to, that same :cpp:class:`Writer` can be reused for other
contexts that already write data to the :cpp:class:`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!
Status DumpSensorData(Writer& writer) {
static char temp[64];
ImuSample imu_sample;
imu.GetSample(&info);
size_t bytes_written = imu_sample.AsCsv(temp, sizeof(temp));
return 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 to be
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 :cpp:class:`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.
Status DumpSensorData(Writer& writer) {
RawSample imu_sample;
imu.GetSample(&info);
return 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 :cpp:class:`Writer` has access to any underlying
buffers, it's harder for functions that share a :cpp:class:`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::ResourceExhausted();
}
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);
}
------------
Design notes
------------
Sync & Flush
============
The :cpp:class:`pw::stream::Stream` API does not include ``Sync()`` or
``Flush()`` functions. There no mechanism in the :cpp:class:`Stream` API to
synchronize a :cpp:class:`Reader`'s potentially buffered input with its
underlying data source. This must be handled by the implementation if required.
Similarly, the :cpp:class:`Writer` implementation is responsible for flushing
any buffered data to the sink.
``Flush()`` and ``Sync()`` were excluded from :cpp:class:`Stream` for a few
reasons:
* The semantics of when to call ``Flush()``/``Sync()`` on the stream are
unclear. The presence of these methods complicates using a :cpp:class:`Reader`
or :cpp:class:`Writer`.
* Adding one or two additional virtual calls increases the size of all
:cpp:class:`Stream` vtables.
.. _module-pw_stream-class-hierarchy:
Class hierarchy
===============
All ``pw_stream`` classes inherit from a single, common base with all possible
functionality: :cpp:class:`pw::stream::Stream`. This structure has
some similarities with Python's `io module
<https://docs.python.org/3/library/io.html>`_ and C#'s `Stream class
<https://docs.microsoft.com/en-us/dotnet/api/system.io.stream>`_.
An alternative approach is to have the reading, writing, and seeking portions of
the interface provided by different entities. This is how Go's `io
package <https://pkg.go.dev/io>`_ and C++'s `input/output library
<https://en.cppreference.com/w/cpp/io>`_ are structured.
We chose to use a single base class for a few reasons:
* The inheritance hierarchy is simple and linear. Despite the linear
hierarchy, combining capabilities is natural with classes like
:cpp:class:`ReaderWriter`.
In C++, separate interfaces for each capability requires either a complex
virtual inheritance hierarchy or entirely separate hierarchies for each
capability. Separate hierarchies can become cumbersome when trying to
combine multiple capabilities. A :cpp:class:`SeekableReaderWriter` would
have to implement three different interfaces, which means three different
vtables and three vtable pointers in each instance.
* Stream capabilities are clearly expressed in the type system, while
naturally supporting optional functionality. A :cpp:class:`Reader` may
or may not support :cpp:func:`Stream::Seek`. Applications that can handle
seek failures gracefully way use seek on any :cpp:class:`Reader`. If seeking
is strictly necessary, an API can accept a :cpp:class:`SeekableReader`
instead.
Expressing optional functionality in the type system is cumbersome when
there are distinct interfaces for each capability. ``Reader``, ``Writer``,
and ``Seeker`` interfaces would not be sufficient. To match the flexibility
of the current structure, there would have to be separate optional versions
of each interface, and classes for various combinations. :cpp:class:`Stream`
would be an "OptionalReaderOptionalWriterOptionalSeeker" in this model.
* Code reuse is maximized. For example, a single
:cpp:func:`Stream::ConservativeLimit` implementation supports many stream
implementations.
Virtual interfaces
==================
``pw_stream`` uses virtual functions. Virtual functions enable runtime
polymorphism. The same code can be used with any stream implementation.
Virtual functions have inherently has more overhead than a regular function
call. However, this is true of any polymorphic API. Using a C-style ``struct``
of function pointers makes different trade-offs but still has more overhead than
a regular function call.
For many use cases, the overhead of virtual calls insignificant. However, in
some extremely performance-sensitive contexts, the flexibility of the virtual
interface may not justify the performance cost.
Asynchronous APIs
=================
At present, ``pw_stream`` is synchronous. All :cpp:class:`Stream` API calls are
expected to block until the operation is complete. This might be undesirable
for slow operations, like writing to NOR flash.
Pigweed has not yet established a pattern for asynchronous C++ APIs. The
:cpp:class:`Stream` class may be extended in the future to add asynchronous
capabilities, or a separate ``AsyncStream`` could be created.
.. cpp:namespace-pop::
------------
Dependencies
------------
* :ref:`module-pw_assert`
* :ref:`module-pw_preprocessor`
* :ref:`module-pw_status`
* :ref:`module-pw_span`
------
Zephyr
------
To enable ``pw_stream`` for Zephyr add ``CONFIG_PIGWEED_STREAM=y`` to the
project's configuration.
----
Rust
----
Pigweed centric analogs to Rust ``std``'s ``Read``, ``Write``, ``Seek`` traits
as well as a basic ``Cursor`` implementation are provided by the
`pw_stream crate </rustdoc/pw_stream>`_.
.. toctree::
:hidden:
:maxdepth: 1
Backends <backends>