The arena decoder

The default model. Header: <stem>.rp.hpp. Back to the README; shared rules (lifetimes, presence, enums) in semantics.md.

decode() reads the whole message into a read-only object tree in a single bump arena. Strings and bytes are borrowed as std::string_views into the input wire buffer (zero-copy); the structure (nodes, arrays, maps) lives in the arena. The tree borrows both the arena and the input, so both must outlive it. For a result that owns its input, use decode_owned. For each message Foo the generator emits a class Foo:

class Person {
 public:
  [[nodiscard]] static const Person* decode(rapidproto::ByteView input, rapidproto::Arena& arena,
                                            rapidproto::ArenaDecodeError* err = nullptr) noexcept;

  std::uint32_t id() const noexcept;        // one const getter per field
  std::string_view name() const noexcept;   // string: a view into the input buffer
  const Address* address() const noexcept;  // sub-message: a pointer, null when absent
};

What each field kind returns

A decode profile changes these types where it applies - a dropped field loses its accessor, a raw one returns undecoded bytes.

Construct Accessor returns
scalar / enum the value, by value (std::int32_t, bool, the generated enum class, …); absent reads as the zero default, indistinguishable from a written zero (semantics)
string / bytes std::string_view into the input buffer (borrowed, zero-copy); std::optional<std::string_view> when marked optional. Not NUL-terminated, and a bytes value may contain NULs - compare with == on the view, never hand .data() to a C string API
sub-message const Sub*, nullptr when absent - the pointer carries the presence. (A proto2 required field is never null: absence fails the decode as MissingRequired.)
repeated T rapidproto::ArrayView<T>, a contiguous range with size(), empty(), operator[] and range-for. Elements are values, not pointers - a repeated field has no presence. Repeated string/bytes instead return rapidproto::StringArrayView, yielding std::string_view per element.
map<K, V> rapidproto::MapView<Entry>: insertion-order entries with .key()/.value(). find(key) returns an iterator to compare against end(), as std::map does, and it->value() reads the entry. A message-valued .value() is itself const V*; test it before dereferencing. Duplicate keys are kept rather than collapsed (duplicate fields).
oneof o a reader o(handlers…): one typed handler per member, with the active member dispatched to its handler (see below)
optional on a scalar/string/bytes/enum std::optional<T> (std::nullopt = absent). There is no has_<field>() accessor, and the keyword is a no-op on a message field - that pointer already carries presence.

Reading a generated schema

The examples below use namespace ex = rp::arena::example; - generated arena types live under rp::arena::<your.package>.

In a generated schema pkg.Foo becomes rp::arena::pkg::Foo, and each accessor is its proto field name. A map’s entry type is nested in its message: Foo::LabelsEntry. Any name that would clash with C++ or with the generated API takes a trailing _ - messages, enums and package components included (enum stdstd_). Reading a Person carrying one of each shape:

// message Person { string name = 1; uint32 id = 2; Address address = 3;
//                  repeated Phone phones = 4; map<string, string> labels = 5;
//                  optional string nickname = 6; }

std::string describe(const ex::Person* p) {
  std::string out(p->name());                                       // string: a view into the input
  out += std::to_string(p->id());                                   // scalar: by value
  if (const ex::Address* a = p->address()) out += a->city();   // message: null when absent
  for (const ex::Phone& ph : p->phones()) out += ph.number();  // repeated: elements are values
  const auto labels = p->labels();
  if (auto it = labels.find("env"); it != labels.end()) out += it->value();
  out += p->nickname().value_or("");                                // `optional`: std::optional<T>
  return out;
}

Two mistakes the view types invite on any schema, neither diagnosable from what the compiler prints (gcc-13 below; clang-20 words both differently):

If you write you get write instead
for (const auto* ph : p->phones()) unable to deduce ‘const auto*’ (clang: incompatible initializer of type ‘const Phone’) for (const ex::Phone& ph : …)
for (auto& [k, v] : p->labels()) cannot decompose inaccessible member … ‘rp_key’ (clang: private member) for (const auto& e : …), then e.key() / e.value()

Enums decode open, so a switch over one needs a default: arm - see semantics.

A oneof is read with a visitor, so an inactive member cannot be read:

// oneof contact { string email = 1; Address work = 2; }
person->contact(
    [](ex::Person::Contact::email, std::string_view e)      { use(e); },
    [](ex::Person::Contact::work,  const ex::Address& a) { use(a.city()); },  // const&, no null-check
    [](std::monostate)                                            { /* unset */ });      // optional

Handlers are matched by their tag type, so same-typed members stay distinct; members you omit are ignored, and a single [](auto, auto){…} catch-all takes the rest. Each handler returns void - the tree is already decoded, so there is nothing to abort.

Memory & lifetimes

rapidproto::Arena is a growable, single-threaded bump allocator that owns the whole decoded tree.

rapidproto::Arena arena;                   // owns its chunks (RAII); frees the whole tree at scope exit
const Foo* a = Foo::decode(buf1, arena);   // tree #1
// … use a …
arena.reset();                             // rewinds for reuse - keeps the chunks, frees nothing
const Foo* b = Foo::decode(buf2, arena);   // tree #2 reuses the same memory (no malloc after warm-up)

Self-contained decode (decode_owned)

rapidproto::decode_owned<Foo> takes the input by value, decodes into a default Arena, and returns a std::shared_ptr<const Foo> that owns both the input bytes and the arena:

std::string bytes = read_request();                 // input you own
std::shared_ptr<const ex::Person> p =
    rapidproto::decode_owned<ex::Person>(std::move(bytes));  // move in -> no copy
if (!p) { /* malformed input (pass &err for the reason) */ }
use(p->name());                                     // valid while any copy of `p` lives

Bind the handle to a named variable before reading through it: in for (const auto& a : decode_owned<Foo>(b)->items()) the handle dies at the end of the range-init, before the body runs. And because the Arena is created inside, set_capacity_limit is not available - use decode(ByteView, Arena&) for untrusted input, or when you hold a string_view you’d rather not copy into a std::string.

Error handling

decode() returns nullptr on any failure and, if you pass an ArenaDecodeError*, fills in why. It writes err only on failure and never clears it, so test the returned pointer - a struct reused across a loop still holds the previous failure:

struct ArenaDecodeError {
    enum class Code { None, Wire, OutOfMemory, RecursionTooDeep, MissingRequired,
                      RepeatedSingularMessage, StringTooLong, InputTooLarge };
    Code code;
    rapidproto::WireError wire;     // valid when code == Wire
    std::size_t offset;             // byte offset of a wire failure
    std::uint32_t field_number;     // the offending field (MissingRequired / RepeatedSingularMessage)
};
Code Meaning
Wire Malformed wire input (truncation, length overrun, group mismatch); wire/offset locate it
MissingRequired A proto2 required field was absent (matches protoc); field_number names it
RecursionTooDeep Message nesting exceeded the depth guard (kMaxDecodeDepth, 100). Nested groups hit their own guard and report Wire with GroupTooDeep
OutOfMemory The arena could not satisfy an allocation
RepeatedSingularMessage A singular sub-message appeared more than once, which protobuf merges and a read-only tree cannot; field_number names the field (for a map, the map itself). Covers four more shapes - see duplicate fields
InputTooLarge The input exceeded UINT32_MAX bytes
StringTooLong Reserved; never produced

On any error the tree is incomplete; discard it (or reset() the arena).

See also