The shared rules — they apply to both decode models and affect how you write correct consumer code. This page is the single home for them; the model pages (arena, streaming) link here instead of restating.
- Lifetimes. Both models borrow the input. Streaming: the input
ByteViewmust outlive the decoder and everystring_viewit hands a callback. Arena: the tree's structure lives in theArena, but its strings/bytes arestring_views into the input, so the tree stays valid only while both theArenaand the input buffer live. Usedecode_ownedfor ashared_ptrthat owns both. - Untrusted input is validated; values are not. Wire input is fully checked for wire-format
integrity (structure, lengths, group nesting), so a malformed buffer fails cleanly and never
triggers UB. Field values are not range-checked: RapidProto trusts the schema, not the bytes. A
stringin particular is handed back unvalidated, so it may carry bytesprotocwould reject as invalid UTF-8. - Defaults & presence. Arena: an implicit-presence field (plain proto3 scalars) reads back its
zero default (
0/""/ the first enum value) when absent; an explicit-presence scalar/string/ enum field returnsstd::optional<T>(std::nulloptwhen absent — apply a proto2[default=X]yourself viavalue_or); a sub-message's presence is itsconst T*accessor returningnullptr. Streaming: an absent field simply fires no callback, and no defaults are delivered. - Enums are open and shared between the models. A proto enum becomes one
enum class : std::int32_t(e.g.example::Status) used by both decoders. An unrecognized wire value arrives as its raw integer cast into the enum;INT32_MIN/INT32_MAXsentinels force adefault:arm under-Wswitch, andrp_known_min/rp_known_maxcarry the schema's declared value range (e.g.if (v <= Status::rp_known_max)). (The generator places the enums in a shared<stem>.rp.common.hppthat each decoder#includes for you, so you never include it directly.) This applies to closed enums too (proto2, or editionsenum_type = CLOSED): RapidProto intentionally decodes every enum as open — where protoc would route an unrecognized closed-enum value to unknown fields, RapidProto delivers the raw value — so do not rely on closed-enum semantics. - A field occurring more than once on the wire. A conformant encoder writes each singular field
once, but a buffer can still repeat one — most often because two serialized messages were
concatenated, which protobuf defines as merging them. Streaming applies no policy: it
delivers what is on the wire on the schedule it always uses — per element for
repeated fields, per entry for maps, otherwise per occurrence — so last-wins / concatenation /
de-duplication are yours to implement. The one case a duplicate is not handed to you is inside a
map entry: that entry's
(key, value)callback fires once, with the lastkeyand lastvaluethe entry carried. Arena materializes a tree and so must choose: singular scalars,string,bytesand enums take the last occurrence (values are never joined —"AAA"then"BBB"reads back"BBB"); repeated fields concatenate, in any mix of packed and expanded; a oneof keeps the last member set. Two of arena's choices differ from protobuf. A map keeps every entry, where protobuf overwrites:find()returns the last — protobuf's value — butsize()and iteration also see the duplicates protobuf collapses. And a duplicate singular sub-message, which protobuf merges, is instead rejected (ArenaDecodeError::Code::RepeatedSingularMessage, carrying the field number — for a map, the map's own number, since the entry is a synthetic type you never wrote). The rejection is unconditional rather than an attempt to tell apart the occurrences whose merge a plain overwrite would have matched anyway, so some rejected buffers would in fact have decoded identically; telling those apart needs the merge machinery itself. Besides a plain sub-message field, it covers a group, arequiredmessage field, arawone, a sub-message oneof member repeating while the oneof still holds it, and a map entry repeating itsvalue. It does not fire for a oneof whose members alternate — a different member clears the oneof, and protobuf likewise starts the later occurrence fresh — nor for a field a profiledrops, which is skipped unexamined. If you need merge semantics, merge upstream, or decode with the streaming model and combine the occurrences yourself. - Well-known types (
google.protobuf.Timestamp, etc.) decode as plain messages (theirseconds/nanosfields), with no special Timestamp/Duration/Any semantics. - Extensions are not decoded, so an extension on the wire arrives as an unknown field. A message
marked
option message_set_wire_format = true(a proto1-era container holding only extensions) is accepted with a warning and decodes as unknown fields — its schema no longer fails generation, but its contents are not readable. - Thread-safety. A streaming
decode()isconstand holds no mutable state, so decoders over one buffer run concurrently as long as the buffer isn't mutated. An arenadecode()mutates itsArena, so give each thread its own arena; the resulting read-only tree can then be shared.
The full list of intentional non-goals and known limitations (what is deliberately not supported, and why) is in architecture.md.