Contiguous Buffers
The most efficient way to provide storage for bytes is a contiguous region: an array. While it is not always possible to efficiently compose a full message in a single contiguous region, it is certainly a good default. Capy offers two types for this purpose:
-
const_buffer— a handle to a contiguous sequence of bytes. I/O operations will never modify these bytes. -
mutable_buffer— a handle to a contiguous region for sttoring bytes; I/O operations will fill it.
Both types are present in header <boost/capy/buffers.hpp>.
Passing Handles
Objects of type const_buffer and mutable_buffer store a
pointer to the beginning of a memory region and the size of the region measured in bytes.
This is reflected in how they are constructed and how they are copied.
std::string_view header = "Content-Type: application/json";
capy::const_buffer buf(header.data(), header.size()); (1)
capy::const_buffer buf2 = buf; (2)
assert(buf.data() == header.data());
assert(buf2.data() == header.data());
| 1 | You need to pass the pointer to the beginning of the region and its size. |
| 2 | Copies the pointer and the size. |
As operations take the buffers by value, or store them by value as data members, only the handle is copied, while the location of the bytes does not change.
Unlike std::span, a single buffer type can represent storage for different trivially-copyable types:
char carray [1024];
capy::mutable_buffer buf1(carray, sizeof(carray)); (1)
std::array<std::byte, 1024> barray;
capy::mutable_buffer buf2(barray.data(), barray.size()); (2)
std::vector<int> iarray(256);
capy::mutable_buffer buf3(iarray.data(), iarray.size() * sizeof(int)); (3)
| 1 | C array of char is fine. |
| 2 | Array of std::byte is fine. |
| 3 | Vector of int is fine: the integers will be interpreted as bytes during transmission. |
Lifetime Management
The contract for read and write operations in Capy is:
-
The user manages the lifetime of the byte storage.
-
I/O operations receive a handle to the storage and assume that it will be available for the entire duration of the asynchronous operations.
char storage[1024];
capy::mutable_buffer buf(storage, sizeof(storage));
auto [ec, n] = co_await stream.read_some(buf); (1)
process(storage, n);
| 1 | OK: The read will be performed at an unpredictable time, but because afterwards the coroutine needs to be resumed, its frame (containing the array) is guaranteed to outlive the read operation. |
char storage[1024];
capy::mutable_buffer buf(storage, sizeof(storage));
return stream.read_some(buf); (1)
| 1 | Bug: the awaitable, rather than being `co_await`ed, is returned to be used later. At that later time the array will no longer be alive. |
Mutable versus Const Buffers
mutable_buffer represents the storage for bytes, intended to be written. The value
of the bytes is irrelevant, because they will be overwritten. In fact, they may have indeterminate value.
const_buffer represents bytes to be read. It is not only storage, it is also values.
std::string_view header = "Content-Type: application/json";
capy::const_buffer buf(header.data(), header.size());
auto [ec, n] = co_await stream.read_some(buf); (1)
| 1 | Compiler error: passing const_buffer but the read operation needs to mutate its contents. |
A mutable_buffer is convertible to const_buffer. However, you should be very careful
about this, as this conversion allows using pure storage, possibly with indeterminate byte values,
in functions that need to read the byte values:
char storage[1024]; (1)
capy::mutable_buffer mbuf(storage, sizeof(storage));
auto [ec, n] = co_await stream.write_some(mbuf); (2)
| 1 | The storage starts with indeterminate byte values (intended to be overwritten). |
| 2 | Bug: will read indeterminate byte values, which is undefined behavior. |
Buffer Processing Interface
Algorithms operating on buffers use the following interface provided by both
const_buffer and mutable_buffer.
void buffer_interface(capy::const_buffer cbuf,
capy::mutable_buffer mbuf)
{
std::size_t len = cbuf.size(); (1)
std::size_t mlen = mbuf.size();
void const* ptr = cbuf.data(); (2)
void* mptr = mbuf.data();
mbuf += len; (3)
assert(mbuf.data() == (char*)mptr + len);
assert(mbuf.size() == mlen - len);
}
| 1 | The size of the buffer measured in bytes. |
| 2 | Pointer to the first byte of the buffer. It is const for const_buffer. |
| 3 | Shrink to represent a subrange of the original by omitting the initial len bytes. |
make_buffer
There is a convenience factory function make_buffer that can create either
a const_buffer or mutable_buffer from your type, as long as it is recognized
as a contiguous storage of trivially-copyable types.
#include <boost/capy/buffers/make_buffer.hpp>
char arr[10];
capy::mutable_buffer buf = capy::make_buffer(arr);
std::array<std::byte, 10> std_arr;
capy::mutable_buffer buf = capy::make_buffer(std_arr);
std::vector<int> vec(100);
capy::mutable_buffer buf = capy::make_buffer(vec);
std::string str = "message";
capy::mutable_buffer buf = capy::make_buffer(str);
std::string_view str = "message";
capy::const_buffer buf = capy::make_buffer(str);
std::span<char> sp(arr); // or boost::span
capy::mutable_buffer buf = capy::make_buffer(sp);
std::array<std::array<char, 4>, 100> array2;
capy::mutable_buffer buf = capy::make_buffer(array2);
assert(buf.size() == 400);
The const-ness of .data() on the source type determines if the factory produces
const_buffer or mutable_buffer.