Reflection-Based Encoding (RBE)¶
A header-only C++ library for declarative binary serialization and deserialization via reflection-based annotations. Describe a binary layout once as plain C++ structs; RBE handles the rest.
The library source code is available on GitHub and is distributed under the MIT License.
Experimental
RBE is at an early, experimental stage. 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.
Check the feature matrix for what is implemented today.
#include <rbe/annotations.hpp>
#include <rbe/srl.hpp>
struct Position { std::int32_t x, y, z; };
enum class Item : std::uint16_t { Sword = 1, Shield = 2, Potion = 3 };
// nested types inherit the annotations: Position is packed little-endian too
struct [[=rbe::pack_le]] SaveGame {
std::uint32_t version;
std::uint16_t level;
std::uint32_t health;
Position position;
std::array<Item, 8> inventory;
};
std::array<std::byte, rbe::wire_size_of<SaveGame>()> file{};
auto size = rbe::serialize(file, SaveGame{
.version = 2,
.level = 3,
.health = 100,
.position = {.x = 10, .y = -20, .z = 0},
.inventory = { Item::Sword, Item::Potion },
});
// then write `file` to disk
#include <rbe/annotations.hpp>
#include <rbe/dsrl.hpp>
struct Version { std::uint8_t major, minor, patch; };
// header at the start of a firmware image, as a bootloader finds it in flash
struct [[=rbe::pack, =rbe::big]] FirmwareHeader {
std::uint32_t magic;
Version version;
std::uint32_t image_size;
std::uint32_t crc32;
};
// `flash` holds the bytes at the start of the firmware partition
// lazy: a view over the bytes, each field is decoded only when read
auto header = rbe::deserialize<FirmwareHeader>(flash, rbe::dsrl::lazy);
auto magic = header.field<"magic">(); // big-endian to host order, on access
auto version = header.field<"version">().value(); // a nested struct, decoded as a whole
// wider contract: std::nullopt instead of undefined behavior if the partition is too small
auto checked = rbe::try_deserialize<FirmwareHeader>(flash, rbe::dsrl::lazy);
#include <rbe/annotations.hpp>
#include <rbe/frames.hpp>
struct [[=rbe::pack_be]] Header {
[[=rbe::frame_length]] std::uint16_t length;
[[=rbe::id]] std::uint8_t type;
};
struct [[=rbe::pack_be, =rbe::id(std::uint8_t{1})]] Order { std::uint32_t order_ref; std::uint64_t price; };
struct [[=rbe::pack_be, =rbe::id(std::uint8_t{2})]] Cancel { std::uint32_t order_ref; };
using message = rbe::frames::frame<Header, rbe::frames::any<Order, Cancel>>;
auto msg = rbe::deserialize<message>(buffer);
msg.payload().match(
[](Order const&) { /* ... */ }, // decoded eagerly
[](rbe::dsrl::proxy<Cancel>) { /* ... */ }, // or read lazily
[](rbe::unmatched auto) { /* unknown type */ }
);
What is [[=rbe::pack_le]]?
It is a C++26 annotation, not an attribute. Attributes like [[nodiscard]] are hints the
compiler acts on; an annotation, spelled [[=value]], attaches a constant value to a declaration
that code can read back through C++26 static reflection. RBE reads it to decide how the type travels on
the wire: here, packed and little-endian.
The annotation does not change the struct itself. SaveGame keeps its usual C++ layout, padding
included, so sizeof(SaveGame) is still 40; only the bytes RBE writes are packed, and
rbe::wire_size_of<SaveGame>() is 38.
Next steps
- Overview: the whole library on one protocol, annotations to frames
- Installation: compiler requirements, Conan, CMake and the single header
- How-to examples: one short, compiled program per task
- Market data feeds: five real protocols modelled end to end
- Reference and feature matrix: every symbol, and what is implemented today