Skip to content

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.