The streaming decoder

Generated with --stream. Header: <stem>.rp.stream.hpp, types at rp::stream::pkg::Msg. Back to the README; shared rules (lifetimes, presence, enums) in semantics.md.

A streaming decoder forwards wire data 1:1, with no aggregation, defaulting, or merging. For each message Foo the generator emits a struct Foo holding a non-owning ByteView, plus a field-identity tag type per field:

struct Person {
  explicit Person(rapidproto::ByteView bytes) noexcept;

  struct name    { using Value = std::string_view;     /* kNumber=1, kName="name"    */ };
  struct id      { using Value = std::uint32_t;        /* kNumber=2, kName="id"      */ };
  struct email   { using Value = std::string_view;     /* kNumber=3, kName="email"   */ };
  struct address { using Value = ::rp::stream::example::Address; /* kNumber=4, kName="address" */ };

  template <class... rp_Callbacks>
  [[nodiscard]] rapidproto::DecodeStatus decode(rp_Callbacks&&... rp_callbacks) const;
};

The examples below use namespace ex_s = rp::stream::example; - streaming types live under rp::stream::<your.package>.

A callback is [](Foo::field, Value v){ … }. The tag type names the field (tied to its proto name, so referencing a removed or renamed field is a compile error), and Value is the field’s type. Each tag also carries static constexpr std::uint32_t kNumber and std::string_view kName (the proto name). Callbacks fire in wire order, once per occurrence (repeated/packed fire per element; maps per entry). The decoder never materializes the whole message.

Absent fields fire no callback and no defaults are delivered - and a proto3 scalar equal to its default is not on the wire at all. Initialize your own destination variables.

Three ways to consume fields

All snippets decode a Person buffer wire (a rapidproto::ByteView). decode() is [[nodiscard]] and returns a DecodeStatus (see Error handling).

1. A subset. Pass callbacks only for the fields you want; the rest are skipped - the skip-heavy microbenchmarks in benchmarks.md measure that shape:

std::string name; std::uint32_t id = 0;
ex_s::Person{wire}.decode(
    [&](ex_s::Person::name, std::string_view v) { name = std::string(v); },
    [&](ex_s::Person::id,   std::uint32_t v)    { id = v; });
// email and address are never decoded.

2. A catch-all. A generic [](auto tag, auto&& value) matches every known field you didn’t give a specific callback (logging, generic processing). The tag’s kName/kNumber identify it, and you can mix a catch-all with specific callbacks (the specific one wins). For a sub-message field, value is an undecoded sub-decoder; a catch-all does not recurse, so call value.decode(...) yourself.

ex_s::Person{wire}.decode([&](auto tag, auto&& value) {
    log("field %s (#%u)", tag.kName.data(), tag.kNumber);
});

3. Known fields and unknown ones. Give specific callbacks, and add a one-argument [](rapidproto::UnknownField uf) that fires for fields whose number is not in your schema (a newer producer’s field). This is the forward-compatibility pattern:

ex_s::Person{wire}.decode(
    [&](ex_s::Person::name,  std::string_view v) { name = std::string(v); },
    [&](ex_s::Person::email, std::string_view v) { emails.push_back(std::string(v)); }, // per element
    [&](ex_s::Person::address, ex_s::Address a) -> rapidproto::DecodeStatus { // recurse
        return a.decode([&](ex_s::Address::city, std::string_view v) { city = std::string(v); });
    },
    [&](rapidproto::UnknownField uf) {                                  // a field not in the schema
        log("unknown #%u (wire type %d, %zu bytes)", uf.field_number, int(uf.wire_type), uf.bytes.size());
    });

UnknownField carries { std::uint32_t field_number; rapidproto::WireType wire_type; rapidproto::ByteView bytes; } - the field’s bytes as they appear on the wire after the tag, so a LEN field’s view starts with its length prefix and a group’s ends with its closing end-group tag. That framing is why these bytes are not what another decoder’s decode() takes - a sub-decoder’s rp_bytes() is (see using both models); strip the prefix yourself if you want to decode an unknown field. A known field you simply didn’t handle is not “unknown” (use a catch-all for those). Proto2 extend fields are not decoded; an extension on the wire arrives here as a raw UnknownField.

Field kinds

Enums decode open - see semantics.

Error handling

decode() returns a rapidproto::DecodeStatus:

struct DecodeStatus {
    rapidproto::WireError wire;    // a wire-format error (None when ok or aborted)
    bool                  aborted; // a callback asked to stop
    std::size_t           offset;  // byte offset of a wire error
    bool ok() const noexcept;      // true unless a wire error or an abort
};

Mistakes are compile errors

Dispatch is entirely compile-time (no allocation, no std::function, no virtual calls). Each of these is a compile error:

See also