Concepts#

pw_multibuf: A buffer API optimized for zero-copy messaging

Why MultiBuf?#

On embedded devices, as on other platforms, it is often convenient to associate multiple regions of memory. Scatter-gather I/O, in which several buffers are read or written in a single operation is one such use case, assembly of network protocol messages from different buffers representing different layers in a protocol stack is another. Ideally, these collections of memory regions should be dynamic, allowing regions to be added or removed with few restrictions.

Simply aggregating memory buffers fails to provide a convenient interface for interacting with this memory. Ideally, the container of buffers should provide callers with ways to access the memory byte-by-byte and buffer-by-buffer. If these memory regions are to be used with core platform I/O, they should also avoid copying data unnecessarily to keep performance high.

Finally, different memory regions may have different ownership semantics. Some memory may be uniquely owned, and should be freed when the collection goes out of scope. Others may be externally owned, and should be left intact. Still others may have shared pointer semantics. The collection should correctly handle memory ownership in all these cases to preserve correctness and prevent leaks.

GenericMultiBuf and its derived types have been designed to meet these needs. These types are collectively referred to as “MultiBuf” in the following sections, except where details of the derived types differ.

MultiBuf ideas#

In order to reduce ambiguity, it is important to establish the meaning of some terms throughout this module:

Entries#

A MultiBuf instance manages an internal deque of metadata values. These values describe part of a memory region. Each entry holds either a pointer, or an offset, length, and flags. The entry type is internal to pw_multibuf, and represents the primary contribution to memory overhead for the type.

MultiBufs have an internal deque of entries

Chunks#

Chunks are contiguous memory regions in a MultiBuf instance. They are represented by two or more entries. The first entry will be the pointer to memory region. The remaining will correspond to each layer. Each chunk is conceptually smiliar to a ByteSpan instance.

Entries store memory addresses and views into those regions

Layers#

Every MultiBuf instance provides a view of its aggregated memory regions. If a MultiBuf type includes kLayerable as one of its Properties, then it supports adding layers that restrict the view of the memory. Each layer represents a subset of the layer below, with the bottom-most layer being the memory regions themselves. Each layer adds one entry to each chunk, describing the offset and length of the chunk that is visible at that layer.

Layers add new chunks rather than modifying existing ones. This allows “popping” the top layer to return the MultiBuf instance to the state it was in before the layer was added. For example, popping a TCP segment layer might return a MultiBuf instance to a larger view that encompasses an IP packet.

Layers add additional entries specifying narrower views of memory

Fragments#

Each time a layer is added, all the chunks in a MultiBuf instance are considered part of the same fragment at the new layer.

Thus, if several frames are combined and a layer added to make a packet, the packet is considered a single fragment at that layer. If several packets are combined and a layer added to make a segment, the segment is considered a single fragment at that layer. This can be used when decomposing higher level protocol messages back into lower level ones, e.g. a segment back into multiple packets.

Fragments group chunks together at different levels

Memory ownership#

MultiBuf instances can hold memory regions with different ownership semantics. Ownership here refers to what object is responsible for freeing the memory when it is no longer needed. A single MultiBuf instance can hold memory from each of three categories:

  • Unique Ownership: Memory is provided as a pw::UniquePtr. A flag in the corresponding entry marks the memory as “owned”. The MultiBuf instance will deallocate the memory when it is no longer referenced.

  • Shared Ownership: Memory is provided as a pw::SharedPtr. A flag in the corresponding entry marks the memory as “shared”. The MultiBuf instance will deallocate the memory when it is discarded, but only if no other existing objects share ownership.

  • No Ownership: Memory is provided as a ByteSpan, and treated as unowned. The MultiBuf instance simply holds a reference, and the caller is responsible for managing the memory’s lifetime.