Decode profiles & unknown fields

Arena-only features that tailor decoding to what this consumer needs, without touching the schema. Back to the README; the arena model itself is in arena.md.

Unknown fields

By default, fields not in your schema (a newer producer’s field, or a proto2 extension) are skipped and dropped. To detect (not recover) that unknowns were present, reserve a per-message “saw an unknown field” flag, exposed as has_unknown_fields():

The selection is part of the decode profile: it folds into the profile identity - see below.

Decode profiles: drop, raw, and unknown-fields

The schema says what can be on the wire; a decode profile says what this consumer does with it. Choose, per field (pkg.Msg.field), per type (pkg.Msg, covering every field of that message type), or per message (for unknown-fields).

drop - no storage, no accessor

No decode work beyond wire-validated skipping. Reading a dropped field is a compile error, not a silent default. (Dropping a required field is rejected.)

unknown-fields - reserve one message’s detection bit

Reserves that message’s has_unknown_fields() bit (see Unknown fields). Unlike drop/raw it names a message directly, not a field or a field’s type; an enum or a field name is an error.

raw - keep a sub-message as bytes, decode on demand

For message-typed fields (groups included): the sub-message’s payload is borrowed as a ByteView into the input instead of a materialized tree; a repeated field becomes a StringArrayView, one payload per element. Each view is exactly what the field type’s own decode() accepts, so the tree is built only if you ask. Typical targets: a huge or rarely-read sub-message, or a million-element repeated field read element-wise.

Writing a profile: --field-modes and the inline flags

Profiles come from a file (one drop <name> / raw <name> / unknown-fields <message> per line, # comments, an optional name <identifier> line) via --field-modes=<file>, or inline via --drop=<name> / --raw=<name> / --unknown=<message> (and --unknown-present for every message). A field-level entry beats a type-level entry; field modes do not apply inside a oneof. Profile entries resolve against every schema the invocation generates, as one batch, so a global profile works by listing (or PROTOS-listing, in CMake) every schema it spans in one generation; a name unknown across the whole batch is still a hard error.

# lean.modes - this consumer never reads sides, and reads origin only on demand
name lean
drop demo.Shape.sides
raw  demo.Shape.origin
namespace demo = rp::arena::demo;                           // arena types live under rp::arena
const demo::Shape* s = demo::Shape::decode(bytes, arena);   // s->sides() does not compile
if (s->origin()) {                                          // the Point payload (borrowed from the input)
    const demo::Point* p = demo::Point::decode(*s->origin(), arena);  // deferred: only now
}

Profiles change the generated types

A profile changes the generated types, so you still write rp::arena::demo::Shape, but two TUs generated under different profiles (including differing only in which messages reserve the unknown-fields bit) hold distinct types and fail to link rather than silently exchanging mismatched layouts.

That guard is C++ name mangling, so it reaches exactly as far as mangling does - a function’s parameters, not its return type. Shape make(); compiled under two profiles links, and the caller reads the result with the layout it was built against. A signature whose only generated types are enums is unguarded for a different reason: enums are one type across profiles by design, shared through the common header. Cross a profile boundary through a parameter, or keep it inside one TU.

Don’t forward-declare generated types yourself: under a profile, namespace rp::arena::demo { class Shape; } declares a different class. See examples/consumer/lean_main.cpp for the full pattern.