RBE (Reflection-Based Encoding) is a modern, header-only C++26 library that bridges the gap between plain C++ structs and raw binary protocols. By leveraging C++26 static reflection, RBE allows you to declaratively define byte order, packing, and message framing directly via annotations.
Warning
RBE is in early development (0.x). Until version 1.0.0 the API is unstable and breaking changes may land
in any minor release (0.1 → 0.2). Patch releases (0.1.0 → 0.1.1) stay backwards compatible.
The library automatically generates highly optimized serialization and deserialization code utilities at compile time. Whether you are reading from a network socket, a memory-mapped file, or a ring buffer, your struct remains the strict single source of truth, eliminating boilerplate and guaranteeing zero runtime overhead compared to hand-written code.
#include <rbe/annotations.hpp>
enum class msg_type : std::uint8_t { add_order = 0x21, reduce_size = 0x25, tick = 0x2A };
// struct will travel packed and big-endian over the wire
struct [[=rbe::pack, =rbe::big]] PacketHeader {
std::uint16_t length;
std::uint8_t count;
std::uint8_t unit;
std::uint32_t sequence;
};
// =rbe::pack_be is a preset for `=rbe::pack, =rbe::big`
struct [[=rbe::pack_be]] MessageHeader {
[[=rbe::frame_length]] std::uint8_t length;
[[=rbe::id]] msg_type message_type;
};
struct [[=rbe::pack_be, =rbe::id(msg_type::add_order)]] AddOrder {
std::uint32_t time_offset;
std::uint32_t order_id;
std::uint8_t side_indicator;
std::uint32_t quantity;
std::uint64_t symbol;
std::uint32_t price;
};
struct [[=rbe::pack_be, =rbe::id(msg_type::reduce_size)]] ReduceSize {
std::uint32_t time_offset;
std::uint64_t order_id;
std::uint32_t cancelled_shares;
};
// trivially_wirable example, wire and host representation are identical
struct [[=rbe::id(msg_type::tick)]] Tick {
std::uint64_t timestamp;
std::uint32_t price;
std::uint32_t quantity;
};
Serialization and deserialization are as easy as calling rbe::serialize and rbe::deserialize. The library supports different decoding strategies: eager, lazy and in-place to suit different use cases.
- Eager: decode every field up front, get the struct back
- Lazy: get a proxy over the buffer, each field decoded on access
- In-place: type pun the buffer into a struct, no decoding at all (the type must be
trivially_wirableand the buffer must be aligned)
#include <rbe/dsrl.hpp> // deserialization module
#include <rbe/srl.hpp> // serialization module
alignas(Tick) std::array<std::byte, 1500> buffer{};
// Serialize to raw buffer
auto size = rbe::serialize(buffer, AddOrder{});
// Eager: decode every field up front, get the struct back
auto order = rbe::deserialize<AddOrder>(buffer, rbe::dsrl::eager);
auto price = order.price;
// Lazy: get a proxy over the buffer, each field decoded on access
auto view = rbe::deserialize<AddOrder>(buffer, rbe::dsrl::lazy);
auto timestamp = view.field<"time_offset">(); // ✅ correct
// auto invalid = view.field<"invalid">(); // ❌ compile-time error: no such field
// In-place: type pun the buffer into a struct, no decoding at all.
// The type must be `trivially_wirable` and the buffer must meet alignment requirements.
auto& tick = rbe::deserialize<Tick>(buffer, rbe::dsrl::in_place); RBE provides a flexible and recursive message framing system. By defining a frame as a combination of a header and a payload, where payloads can be frames themselves, an entire protocol can be expressed as one unified type.
- Header: any
wirabletype - Payload: one of
- a
wirabletype - another
rbe::frames::frame rbe::frames::any<T...>: one of several messages, picked by the header's=rbe::idfieldrbe::frames::many<T>: a run of messages, each one walked using its own length field
- a
#include <rbe/frames.hpp>
using packet = rbe::frames::frame<
PacketHeader, // packet header
rbe::frames::many< // ...followed by a run of messages
rbe::frames::frame<
MessageHeader, // message header
rbe::frames::any<AddOrder, ReduceSize, Tick> // one of these
>
>
>;
// Read it back as a view over the buffer: headers are proxies, the run is a range
auto packet_view = rbe::deserialize<packet>(buffer);
auto sequence = packet_view.header().field<"sequence">();
for (auto [hdr, payload] : packet_view.payload()) {
payload.match( // match supports both eager and lazy dispatch
[](AddOrder const& add_order) { /* decoded */ },
[](ReduceSize const& reduce_size) { /* decoded */ },
[](rbe::dsrl::proxy<Tick> tick) { /* read field by field */ },
[](rbe::unmatched auto other) { /* unknown or unhandled message type */ }
);
}
// Write it part by part, each where the previous one ended
auto out = std::span {buffer};
out = out.subspan(rbe::serialize(out, packet_header));
out = out.subspan(rbe::serialize(out, msg_header0));
out = out.subspan(rbe::serialize(out, add_order));Note
A value type generated from the frame (packet::value_type) and a writer that appends header and messages
into the buffer (packet::srl_type) are designed but not implemented yet. See the
feature matrix.
- Declarative
- Describe your binary format with standard C++ structs
- Describe your frames with
rbe::frames::frame,rbe::frames::anyandrbe::frames::many - Use C++26 annotations to specify endianness, packing, field order, framing and more
- Safety
- Annotations correctness is verified at compile time
- Proxy member access is verified at compile time during deserialization
- Frame composition correctness is verified at compile time
- Performance
- Serialization and deserialization logic is generated at compile time, with zero runtime overhead compared to hand-written code
- Different decoding strategies, eager, lazy and in-place, for different use cases
- Low adoption cost
- Header-only
- No external dependencies
Full documentation lives at ch0pex.github.io/rbe:
| Section | Start here |
|---|---|
| Getting started | Overview · Installation |
| Examples | How-to · Market data feeds |
| Reference | Annotations · Serialization · Deserialization · Frames · Feature matrix |
| Design notes | Design overview · Framing · Type concepts · Requirements |
Building RBE from source, running the test suite and the repository layout are covered in CONTRIBUTING.md.
Note
The RBE logo was generated with AI. It is a placeholder: if time and budget allow, a proper logo will be commissioned from an artist.