Composite Buffers

In real protocols, a message to be transmitted is often composed of a couple of chunks prepared separately in separate contiguous regions of storage. For instance, you may separately get message headers, the payload, and the message checksum. Trying to get them into a single contiguous region would require the user to do unnecessary copying.

In order to address this use case, all read and write operations in Capy allow you to provide ranges of contiguous buffers:

const std::string headers = make_headers();
const std::string body = make_body();
const std::string checksum = stamp(headers, body);

std::array buf {capy::make_buffer(headers), 
                capy::make_buffer(body), 
                capy::make_buffer(checksum)};

co_await stream.write_some(buf);

Buffer Sequences

In order to address both the simple case of a single contiguous buffer, and the complex cases when the data is produced in a number of contiguous buffers, Capy introduces a concept of a buffer sequence:

  • A contiguous buffer is a buffer sequence.

  • Any bidirectional range over contiguous buffers is a buffer sequence.

This generalizes the notion of a handle:

composite buffer

In the composite case, the entire range mechanism becomes a handle; but since the elements are const_buffer or mutable_buffer, the contiguous regions of memory they point to are not part of the handle. This still allows the algorithms to copy the handle while retaining the storage at its original location.

Treating a single contiguous buffer as a one-element sequence is intentional, to make the library API smaller. Internally, it is treated as a single-element range.

While a structure looks two-dimensional, the bytes across all contiguous buffers are interpreted and transmitted as a single sequence: first the bytes from the first contiguous buffer, then the bytes from the second contiguous buffer, and so on.

The mechanism for handling ranges of buffers assumes that the number of elements will be around four. In fact, some implementations will perform suboptimally when you use ranges of more than 16 contiguous buffers.

Serializing more buffers in one transmission would not be efficient, and you might as well merge the contents into a single buffer.

The Concepts

Concept MutableBufferSequence tests if the type models a mutable buffer sequence, suitable for read algorithms. It can be one of:

static_assert(capy::MutableBufferSequence<capy::mutable_buffer>);
static_assert(capy::MutableBufferSequence<std::span<capy::mutable_buffer>>);
static_assert(capy::MutableBufferSequence<std::vector<capy::mutable_buffer>>);
static_assert(capy::MutableBufferSequence<std::array<capy::mutable_buffer, 4>>);

Concept ConstBufferSequence tests if the type models a const buffer sequence, suitable for write algorithms. It can be one of:

static_assert(capy::ConstBufferSequence<capy::const_buffer>);
static_assert(capy::ConstBufferSequence<capy::mutable_buffer>);
static_assert(capy::ConstBufferSequence<std::span<capy::const_buffer>>);
static_assert(capy::ConstBufferSequence<std::span<capy::mutable_buffer>>);
static_assert(capy::ConstBufferSequence<std::array<capy::const_buffer, 4>>);
static_assert(capy::ConstBufferSequence<std::array<capy::mutable_buffer, 4>>);

These concepts are present in header <boost/capy/buffers.hpp>.

Your Own Buffer Types

A type of your own can act as a buffer when it converts to the buffer types. To act as a const buffer, it satisfies ConstBuffer by converting to const_buffer. To act as a mutable buffer, it satisfies MutableBuffer by converting to mutable_buffer and to const_buffer:

struct page
{
    std::array<char, 4096>* bytes;

    operator capy::mutable_buffer() const noexcept
    {
        return { bytes->data(), bytes->size() };
    }

    operator capy::const_buffer() const noexcept
    {
        return { bytes->data(), bytes->size() };
    }
};

static_assert(capy::MutableBufferSequence<page>);
static_assert(capy::ConstBufferSequence<page>);
static_assert(capy::MutableBufferSequence<std::array<page, 2>>);

A conversion to mutable_buffer alone is not enough. Reaching const_buffer from it takes two user-defined conversions, and C++ allows one in an implicit conversion. Algorithms read every mutable buffer sequence through const_buffer, so the concepts reject such a type:

struct mutable_only
{
    operator capy::mutable_buffer() const noexcept;
};

static_assert(! capy::ConstBufferSequence<mutable_only>);
static_assert(! capy::MutableBufferSequence<mutable_only>);

Algorithms convert through a const reference, using both const_buffer b = x; and const_buffer(x), and the concepts require both to compile. Conversion operators that are const and return the buffer by value, as above, satisfy both.

A type that converts to const_buffer is treated as one buffer, even if it is also a range of buffers. Its elements are ignored, and so are any conversions they provide.

Mark the conversions noexcept. Algorithms call them inside noexcept functions, so a conversion that throws terminates the program.

Navigating Buffer Sequences

Header <boost/capy/buffers.hpp> also provides a couple of function objects for navigating buffer sequences.

In order to iterate over contiguous buffers themselves (as opposed to bytes), use buffer_sequence_begin and buffer_sequence_end:

template<capy::ConstBufferSequence Buffers>
void process(Buffers const& bufs)
{
    for (auto it = capy::buffer_sequence_begin(bufs);
         it != capy::buffer_sequence_end(bufs); ++it)
    {
        capy::const_buffer buf = *it;
        // Process buf.data(), buf.size()
    }
}

These functions treat single contiguous buffers as a one-element range of contiguous buffers:

capy::const_buffer single;
auto it = capy::buffer_sequence_begin(single); // pointer to `single`
auto e = capy::buffer_sequence_end(single);    // pointer past `single`
assert(std::distance(it, e) == 1);

std::array<capy::const_buffer, 3> multi;
auto it2 = capy::buffer_sequence_begin(multi);  // Returns multi.begin()
auto e2 = capy::buffer_sequence_end(multi);     // Returns multi.end()
assert(std::distance(it2, e2) == 3);

Function buffers_count says how many contiguous buffers are present in the buffer sequence. It is equivalent to:

std::distance(
    capy::buffer_sequence_begin(buf), capy::buffer_sequence_end(buf))

Iterating Over Bytes

To treat a buffer sequence as one run of bytes, use buffers_begin and buffers_end. They return a buffers_iterator, a random access iterator that crosses buffer boundaries and skips empty buffers, so standard algorithms can scan the data without copying it into contiguous storage:

#include <boost/capy/buffers/buffers_iterator.hpp>

std::array<capy::const_buffer, 2> bufs{
    capy::make_buffer(std::string_view("GET / HTTP/1.1\r")),
    capy::make_buffer(std::string_view("\n\r\nbody")) };

// The terminator straddles the two buffers
std::string_view const crlfcrlf = "\r\n\r\n";
auto it = std::search(
    capy::buffers_begin(bufs), capy::buffers_end(bufs),
    crlfcrlf.begin(), crlfcrlf.end());
std::size_t header_size = (it - capy::buffers_begin(bufs)) + 4;

Moving the iterator by n bytes walks the buffers it crosses, so jumps are linear in the number of buffers. The sequence must outlive the iterators.