Serialization Reference¶
Header: rbe/srl.hpp (rbe/srl/serialize.hpp)
Serialization is eager and has two forms, one narrower than the other:
std::size_t bytes_written = rbe::serialize(buffer, value); // narrow contract
std::optional<std::size_t> checked = rbe::try_serialize(buffer, value); // wider contract
| Function | Returns | Contract |
|---|---|---|
rbe::serialize(buffer, value) |
std::size_t, the bytes written |
Narrow. buffer must hold at least rbe::wire_size_of<T>() bytes; otherwise the behavior is undefined. |
rbe::try_serialize(buffer, value) |
std::optional<std::size_t> |
Wider. Checks the size first and returns std::nullopt, writing nothing, when the buffer is too small. Still assumes the span points to live memory. |
buffer is a std::span<std::byte>; std::vector<std::byte>, std::array<std::byte, N> and byte arrays
convert to it. value is any wirable object. The bytes are written from offset 0
and the count returned is always rbe::wire_size_of<T>(), trailing padding included.
struct [[=rbe::derive<rbe::pack, rbe::little>]] Order {
std::uint32_t order_id;
std::uint32_t price;
};
Order order {.order_id = 12345, .price = 15050};
std::vector<std::byte> buffer(rbe::wire_size_of<Order>());
std::size_t n = rbe::serialize(buffer, order); // n == rbe::wire_size_of<Order>() == 8
std::vector<std::byte> small(4);
std::optional<std::size_t> n = rbe::try_serialize(small, Order {}); // std::nullopt: 4 < 8, nothing written
Narrow and wider contracts
A function has a narrow contract when some of the inputs it accepts have undefined behavior: a
partial function, in the mathematical sense. rbe::serialize is one: a buffer that is too short
compiles and runs, and the write past its end is undefined.
The try_ forms have a wider contract, not a wide one. They check the precondition that can be
checked, the buffer's size, and report std::nullopt instead of
faulting. What cannot be checked stays assumed: that the span points to live memory, and that the
bytes are what the type says. The subset of inputs without undefined behavior is larger; it is not
everything.
Dispatch¶
rbe::serialize is overloaded on the type concept T satisfies; there is nothing
to select by hand.
T is |
Behavior |
|---|---|
trivially_wirable, with no byte order or packing inherited from an enclosing type |
One memcpy of the object's bytes. |
a trivially_wirable_primitive under an inherited non-default byte order |
Byte-swapped and stored. |
a wirable_range that is not trivially copyable as a whole |
Element by element. |
a wirable_class otherwise |
Member by member, in declaration order, each at the offset its wire layout gives it and with the byte order its resolved annotations give it. |
The resolution is recursive: a nested struct is serialized with the annotations it inherits, so a struct that is trivially wirable on its own becomes member-by-member work inside a big-endian parent.
Composing several values¶
Each call writes at the start of the span it is given. To write a header and its payload back to back, advance the span by the count returned:
// A header and its payload: each call writes at offset 0 of the span it is given
std::vector<std::byte> out(rbe::wire_size_of<Header>() + rbe::wire_size_of<Order>());
std::span<std::byte> cursor = out;
cursor = cursor.subspan(rbe::serialize(cursor, Header {.length = 11, .type = 1}));
cursor = cursor.subspan(rbe::serialize(cursor, Order {.order_id = 1, .price = 2}));
Planned¶
🚧 Frames are not serializable yet: rbe::serialize(buffer, frame_value) and a writer that appends header
and payload into the buffer are designed but not implemented. See Frames and the
feature matrix.