Skip to content
ch0pexPublic

Repository files navigation

RBE: Reflection-Based Encoding

RBE logo

CI codecov C++ GCC License: MIT

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.

Define the wire format using annotations

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;
};

Serialize and deserialize

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_wirable and 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); 

Framing

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 wirable type
  • Payload: one of
    • a wirable type
    • another rbe::frames::frame
    • rbe::frames::any<T...>: one of several messages, picked by the header's =rbe::id field
    • rbe::frames::many<T>: a run of messages, each one walked using its own length field
#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.

Key Features

  • Declarative
    • Describe your binary format with standard C++ structs
    • Describe your frames with rbe::frames::frame, rbe::frames::any and rbe::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

Documentation

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

Contributing

Building RBE from source, running the test suite and the repository layout are covered in CONTRIBUTING.md.

Logo

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.

Releases

Packages

Contributors

Languages