blob: 579d8f73196c10a136a09906ac3667e438c0e3b9 [file]
.. _module-pw_multibuf-guide:
==============
Usage examples
==============
.. pigweed-module-subpage::
:name: pw_multibuf
MultiBufs are flexible binary data structures. As such, there is no single guide
on how to use them. Instead, several examples are presented that demonstrate
various aspects of how they can be used.
In each of the guides below, a :cc:`pw::Allocator` instance is needed to
instantiate a MultiBuf instance. This allocator is used to allocate the memory
for the MultiBuf instance's deque of :ref:`module-pw_multibuf-concepts-entries`.
For the purposes of these examples, the simple, low-performance
:cc:`AllocatorForTest <pw::allocator::test::AllocatorForTest>` is used.
.. literalinclude:: examples/basic.cc
:language: cpp
:linenos:
:start-after: [pw_multibuf-examples-basic-allocator]
:end-before: [pw_multibuf-examples-basic-allocator]
See :ref:`module-pw_allocator` for more details on allocator selection.
-----------------------------------
Iterating over heterogeneous memory
-----------------------------------
A MultiBuf instance can represent a sequence of non-contiguous regions of
memory. These regions may also have different
:ref:`module-pw_multibuf-concepts-ownership` semantics. For example, a
MultiBuf instance could be composed of some memory that it owns, some that
is shared, and some that is unmanaged (i.e. static or stack-allocated).
The following example creates just such a MultiBuf instance:
.. literalinclude:: examples/iterate.cc
:language: cpp
:linenos:
:start-after: [pw_multibuf-examples-iterate-create]
:end-before: [pw_multibuf-examples-iterate-create]
Note the use of :cc:`ConstMultiBuf <pw::multibuf::v2::ConstMultiBuf>`. This type
alias of :cc:`GenericMultiBuf <pw::multibuf::v2::internal::GenericMultiBuf>`
includes the :cc:`kConst <pw::multibuf::v2::Property>` as one of its
:ref:`module-pw_multibuf-design-properties`.
Regardless of how the underlying memory is stored, MultiBuf methods can
iterate over the data, both by individual bytes or by contiguous
:ref:`module-pw_multibuf-concepts-chunks`.
Iterating by bytes treats the MultiBuf instance as a single, contiguous
buffer. This is useful when the logic does not need to be aware of the
underlying memory layout. Be careful to avoid assuming that the memory is
contiguous; code such as ``std::memcpy(dst, &(*mbuf.begin()), mbuf.size())`` is
almost certainly wrong.
The following example calculates a CRC32 checksum by iterating through every
byte in the MultiBuf instance:
.. literalinclude:: examples/iterate.cc
:language: cpp
:linenos:
:start-after: [pw_multibuf-examples-iterate-bytes]
:end-before: [pw_multibuf-examples-iterate-bytes]
Alternatively, it is possible to iterate over the individual contiguous memory
chunks that make up the MultiBuf instance. This is useful for operations
that can be optimized by working with larger, contiguous blocks of data at once,
such as sending data over a network or writing to a file.
The following example calculates the same CRC32 checksum, but does so by
operating on whole chunks (i.e. ``ConstByteSpan`` objects) at a time:
.. literalinclude:: examples/iterate.cc
:language: cpp
:linenos:
:start-after: [pw_multibuf-examples-iterate-chunks]
:end-before: [pw_multibuf-examples-iterate-chunks]
Both methods iterate over the exact same data, just with different levels of
granularity. The choice of which to use depends on the specific requirements of
the task.
For the complete example, see :cs:`pw_multibuf/examples/iterate.cc`.
---------------------------
Variable-length entry queue
---------------------------
To further demonstrate how memory regions can be added and removed from a
MultiBuf instance consider the following example. This implements a
variable-length queue of binary data entries, very similar to
:ref:`module-pw_containers-inlinevarlenentryqueue`.
.. literalinclude:: examples/queue.cc
:language: cpp
:linenos:
:start-after: [pw_multibuf-examples-queue]
:end-before: [pw_multibuf-examples-queue]
This queue does all of its dynamic allocation in the factory method. After that
method succeeds, all the queue methods are
:ref:`infallible <module-pw_multibuf-design-infallible>`.
For the complete example, see :cs:`pw_multibuf/examples/queue.cc`.
---------
Observers
---------
With a few small changes, the above example can be used to implement an
efficient, asynchronous producer-consumer queue. This example is for
illustrative purposes only. If you need an asynchronous producer-consumer queue,
consider instead :cc:`Sender <pw::async2::Sender>`,
:cc:`Sender <pw::async2::Receiver>`, and their related types.
What this example can illustrate is how a
:cc:`Observer <pw::multibuf::v2::Observer>` can be used to react to changes in a
MultiBuf. Here, one or more tasks produce data and add it to a queue, while one
or more other tasks consume data from the queue. A MultiBuf instance can manage
the lifecycle of the queued data :ref:`module-pw_multibuf-concepts-chunks`, and
its observer mechanism is used to signal between tasks.
To start, an ``AsyncMultiBufObserver`` instance is added to wake up tasks that
are waiting for the queue to become non-empty (for consumers) or non-full (for
producers):
.. literalinclude:: examples/async_queue.cc
:language: cpp
:linenos:
:start-after: [pw_multibuf-examples-async_queue-observer]
:end-before: [pw_multibuf-examples-async_queue-observer]
This type extends :cc:`Observer <pw::multibuf::v2::Observer>` and receives an
:cc:`Event <pw::multibuf::v2::Observer::Event>` every time the contents or
structure of the MultiBuf instance changes.
With this, the queue can leverage the observer to add the same methods that
produces and consumers can wait on:
.. literalinclude:: examples/async_queue.cc
:language: cpp
:linenos:
:start-after: [pw_multibuf-examples-async_queue]
:end-before: [pw_multibuf-examples-async_queue]
Note that this queue uses a
:cc:`TrackedConstMultiBuf <pw::multibuf::v2::TrackedConstMultiBuf>`. The
"Tracked" prefix indicates the MultiBuf instance supports observers, and the
"Const" prefix indicates the data cannot be modified.
A producer task that adds data to this queue might look like the following:
.. literalinclude:: examples/async_queue.cc
:language: cpp
:linenos:
:start-after: [pw_multibuf-examples-async_queue-producer]
:end-before: [pw_multibuf-examples-async_queue-producer]
Finally, a consumer task that pulls data from this queue might look like the
following:
.. literalinclude:: examples/async_queue.cc
:language: cpp
:linenos:
:start-after: [pw_multibuf-examples-async_queue-consumer]
:end-before: [pw_multibuf-examples-async_queue-consumer]
Altogether, this approach is efficient for passing data as it avoids unnecessary
data copies and leverages the :ref:`module-pw_async2` framework for non-blocking
synchronization.
For the complete example, see :cs:`pw_multibuf/examples/async_queue.cc`.
-------------------
Scatter-gather I/O
-------------------
Another possible use case for the MultiBuf type is to manage buffers for
scatter-gather I/O operations. In this scenario, data is either read from a
single source into multiple memory regions (scatter) or written from multiple
memory regions to a single destination (gather).
As an example, the following container holds
:cc:`Message <pw::i2c::Message>`\s for performing multiple I2C reads and
writes in a single operation:
.. literalinclude:: examples/scatter_gather.cc
:language: cpp
:linenos:
:start-after: [pw_multibuf-examples-scatter_gather-message_vector]
:end-before: [pw_multibuf-examples-scatter_gather-message_vector]
This container has a :cc:`TrackedMultiBuf <pw::multibuf::v2::TrackedMultiBuf>`
for data to be read, and a
:cc:`TrackedConstMultiBuf <pw::multibuf::v2::TrackedConstMultiBuf>` for data to
be written. As the "Tracked" prefix indicates, these accept an observer that can
be used to signal when an I2C transfer is complete:
.. literalinclude:: examples/scatter_gather.cc
:language: cpp
:linenos:
:start-after: [pw_multibuf-examples-scatter_gather-observer]
:end-before: [pw_multibuf-examples-scatter_gather-observer]
With a real device, the :cc:`Message <pw::i2c::Message>`\s would be passed
to an :cc:`Initiator <pw::i2c::Initiator>`. This example uses a simpler
``TestInitiator`` type that simply accepts the messages and then waits for
another thread to indicate the transfer is complete:
.. literalinclude:: examples/scatter_gather.cc
:language: cpp
:linenos:
:start-after: [pw_multibuf-examples-scatter_gather-initiator]
:end-before: [pw_multibuf-examples-scatter_gather-initiator]
In this example, the ``MessageVector`` instance collects a series of I2C
messages. For read operations, it adds a destination buffer to its
``rx_buffers_`` field. For write operations, it adds a source buffer to its
``tx_buffers_`` field. An I2C driver could then iterate over the ``messages_``
vector and use the corresponding buffers from the MultiBuf instances to
perform the I/O operations.
The main addition to the existing :ref:`module-pw_i2c` is that this example
abstracts away the memory management of the individual buffers, and
automatically notifies the observer when the transfer is complete and the
messages are dropped.
For the complete example, see :cs:`pw_multibuf/examples/scatter_gather.cc`.
.. _scatter-gather I/O: https://en.wikipedia.org/wiki/Vectored_I/O
----------------------------------------------
Protocol message composition and decomposition
----------------------------------------------
The MultiBuf type was designed to facilitate creating and parsing network
protocol messages. There are two general approaches to creating packets,
referred to here as "top-down" and "bottom-up". The "top-down" approach starts
with payloads of the top-most protocol layer, and then has lower layer protocol
fields added to it.
The MultiBuf type itself does not include code to interpret memory regions
as protocol fields. Products are expected to use a component such as `Emboss`_
to accomplish this. This example provides some simple methods to get and set
fields:
.. literalinclude:: examples/transfer.cc
:language: cpp
:linenos:
:start-after: [pw_multibuf-examples-transfer-fields]
:end-before: [pw_multibuf-examples-transfer-fields]
These can be used to implement types for serializing and deserializing protocol
messages to and from MultiBuf instances. For example, given a network packet
protocol:
.. literalinclude:: examples/public/pw_multibuf/examples/protocol.h
:language: cpp
:linenos:
:start-after: [pw_multibuf-examples-protocol-network_packet]
:end-before: [pw_multibuf-examples-protocol-network_packet]
A type to represent these network packets might look like:
.. literalinclude:: examples/transfer.cc
:language: cpp
:linenos:
:start-after: [pw_multibuf-examples-transfer-network_packet]
:end-before: [pw_multibuf-examples-transfer-network_packet]
Similarly, for a link frame protocol:
.. literalinclude:: examples/public/pw_multibuf/examples/protocol.h
:language: cpp
:linenos:
:start-after: [pw_multibuf-examples-protocol-link_frame]
:end-before: [pw_multibuf-examples-protocol-link_frame]
A type to represent these link frames might look like:
.. literalinclude:: examples/transfer.cc
:language: cpp
:linenos:
:start-after: [pw_multibuf-examples-transfer-link_frame]
:end-before: [pw_multibuf-examples-transfer-link_frame]
With these, creating packets becomes straightforward:
.. literalinclude:: examples/transfer.cc
:language: cpp
:linenos:
:start-after: [pw_multibuf-examples-transfer-create]
:end-before: [pw_multibuf-examples-transfer-create]
For the complete example, see :cs:`pw_multibuf/examples/transfer.cc`.
.. _Emboss: https://github.com/google/emboss
---------------------
In-place modification
---------------------
Distinct from the previous example, the other approach to composing and
decomposing protocol messages is a "bottom-up" approach. This approach starts
with one or more maximally-sized protocol messages, and then proceeding to add
:ref:`module-pw_multibuf-concepts-layers` to restrict the view of the data to
higher and higher protocols in the stack.
This approach is especially useful when the lower protocol fields need to be
preserved. For example, an application which modifies only the top-most protocol
in-place would benefit from this approach.
As an example of such an in-place modification, consider an "encryptor" that
simply XORs the data with a seeded pseudorandom byte stream.
.. warning::
This example is only an example, and is NOT cryptographically secure! DO NOT
use it to protect data!
This example includes the link frames and network packets from before, as well
as a transport segment:
.. literalinclude:: examples/public/pw_multibuf/examples/protocol.h
:language: cpp
:linenos:
:start-after: [pw_multibuf-examples-protocol-transport_segment]
:end-before: [pw_multibuf-examples-protocol-transport_segment]
The types to implement this and the other protocols are similar to the previous
example. A notable departure is how objects for each layer are created, with
callers creating link frames from a pool of memory buffers, and then using
factory methods that consume objects of one protocol layer and produce another,
simply by adding and removing layers.
.. literalinclude:: examples/pseudo_encrypt.cc
:language: cpp
:linenos:
:start-after: [pw_multibuf-examples-pseudo_encrypt-from]
:end-before: [pw_multibuf-examples-pseudo_encrypt-from]
All the tasks are derived from the ``Transformer`` type. They receive protocol
messages of a certain type from an async channel, transform them, and send them
to another async queue:
.. literalinclude:: examples/pseudo_encrypt.cc
:language: cpp
:linenos:
:start-after: [pw_multibuf-examples-pseudo_encrypt-transformer]
:end-before: [pw_multibuf-examples-pseudo_encrypt-transformer]
For example, the ``Relay`` type uses the ``From`` methods above to transform
messages up or down the protocl stack:
.. literalinclude:: examples/pseudo_encrypt.cc
:language: cpp
:linenos:
:start-after: [pw_multibuf-examples-pseudo_encrypt-relay]
:end-before: [pw_multibuf-examples-pseudo_encrypt-relay]
The ``Encryptor`` performs the "encryption" by XOR-ing data with a pseudorandom
byte stream:
.. literalinclude:: examples/pseudo_encrypt.cc
:language: cpp
:linenos:
:start-after: [pw_multibuf-examples-pseudo_encrypt-encryptor]
:end-before: [pw_multibuf-examples-pseudo_encrypt-encryptor]
Even the data ``Producer`` and ``Consumer`` take this form to write and verify
payload data, respectively:
.. literalinclude:: examples/pseudo_encrypt.cc
:language: cpp
:linenos:
:start-after: [pw_multibuf-examples-pseudo_encrypt-line_transformer]
:end-before: [pw_multibuf-examples-pseudo_encrypt-line_transformer]
.. literalinclude:: examples/pseudo_encrypt.cc
:language: cpp
:linenos:
:start-after: [pw_multibuf-examples-pseudo_encrypt-producer]
:end-before: [pw_multibuf-examples-pseudo_encrypt-producer]
.. literalinclude:: examples/pseudo_encrypt.cc
:language: cpp
:linenos:
:start-after: [pw_multibuf-examples-pseudo_encrypt-transport_segment-payload]
:end-before: [pw_multibuf-examples-pseudo_encrypt-transport_segment-payload]
.. literalinclude:: examples/pseudo_encrypt.cc
:language: cpp
:linenos:
:start-after: [pw_multibuf-examples-pseudo_encrypt-consumer]
:end-before: [pw_multibuf-examples-pseudo_encrypt-consumer]
Finally, a ``Recycler`` type resets buffers to be reused:
.. literalinclude:: examples/pseudo_encrypt.cc
:language: cpp
:linenos:
:start-after: [pw_multibuf-examples-pseudo_encrypt-recycler]
:end-before: [pw_multibuf-examples-pseudo_encrypt-recycler]
All of these tasks can then be connected to one another, given some buffers to
work with, and run end to end:
.. literalinclude:: examples/pseudo_encrypt.cc
:language: cpp
:linenos:
:start-after: [pw_multibuf-examples-pseudo_encrypt-e2e]
:end-before: [pw_multibuf-examples-pseudo_encrypt-e2e]
For the complete example, see :cs:`pw_multibuf/examples/pseudo_encrypt.cc`.
-------------------------------
Top-down forwarding, and Emboss
-------------------------------
``examples/top_down_forward.cc`` demonstrates how to decode a received packet
(bottom-up decoding), and then excerpt the received payload to use it in
generating a new packet.
.. literalinclude:: examples/top_down_forward.cc
:language: cpp
:linenos:
:start-after: [pw_multibuf-examples-top_down_forward-main]
:end-before: [pw_multibuf-examples-top_down_forward-main]
It also serves as an example of using `Emboss`_, which allows the packed data
for each header to be read and written using view. However it requires some
manual glue code for each header type.
Here for example is how to use the Emboss view types to serialize and
deserialize the 6-byte ``DemoLinkHeader``, unpacking it into an 8-byte C/C++
structure that is friendlier for the CPU to access.
.. literalinclude:: examples/top_down_forward.cc
:language: cpp
:linenos:
:start-after: [pw_multibuf-examples-top_down_forward-demo_link_header_serialize]
:end-before: [pw_multibuf-examples-top_down_forward-demo_link_header_serialize]
For that, there is a `.emb` file for the demo protocol that describes the packed
layout.
.. literalinclude:: examples/public/pw_multibuf/examples/protocol.emb
:language: gsql
:linenos:
:start-after: [pw_multibuf-examples-protocol-link_frame-emboss]
:end-before: [pw_multibuf-examples-protocol-link_frame-emboss]
For the complete code, see :cs:`pw_multibuf/examples/top_down_forward.cc`, and
for the Emboss definition, see
:cs:`pw_multibuf/examples/public/pw_multibuf/examples/protocol.emb`