The rapidprotoc CLI & CMake integration

How generation is wired into a build. Back to the README.

The rapidprotoc CLI

rapidprotoc [options] <entry.proto>...
Flag Meaning
--arena Emit the arena decoder (<stem>.rp.hpp). The default if neither model flag is given.
--stream Emit the streaming decoder (<stem>.rp.stream.hpp). Combine with --arena to emit both.
--dump Emit the debug dumper (<stem>.rp.dump.hpp), a JSON-like text dumper over the arena tree. Implies --arena.
--unknown-present Arena: reserve the “unknown fields present” bit (has_unknown_fields()) on every message.
--unknown=<message> Arena: reserve that bit on one message (repeatable; a one-line unknown-fields profile entry).
--field-modes=<file> Arena: apply a decode profile file (repeatable; see Decode profiles).
--drop=<name> Arena: drop one field or type inline (as a one-line profile entry).
--raw=<name> Arena: keep a message field’s or type’s payloads for deferred decode()s, inline.
-I <dir> Add an import search path (repeatable).
--out-dir <dir> Where to write the headers (and rapidproto/runtime.hpp, plus arena_runtime.hpp for --arena and dump_runtime.hpp for --dump). Default: the current directory.
--namespace-prefix <ns> Rename the root GENERATED code lives under - <ns>::arena::pkg::Msg and so on (default rp). Dot-separated; what is accepted is emitted verbatim, and a component that could not compile as written is refused instead - empty, a C++ keyword or std, a macro, rp_/RP_/rapidproto (the generator’s own names), or a reserved identifier (__x, _X..., and a leading _ in the first component). The runtime stays in rapidproto:: either way. See using both models.
--no-wellknown Don’t load the bundled well-known-type definitions.
--depfile <path> Write a Make/Ninja depfile (the entries’ headers depend on every input .proto and profile file) so a build regenerates when any input changes. Used by the CMake helper.
--list-outputs Dry run: print every path a generation would write, relative to --out-dir, one per line (listed entries first, decoders before each file’s common header) - nothing is written. The full resolve pipeline runs first, so a schema error fails the listing exactly as it would fail generation. Not combinable with --list-inputs or --depfile.
--list-inputs Dry run: print the on-disk .proto closure (absolute, deduplicated; embedded well-known types excluded) - the files whose edits can change the output list. Same restrictions as --list-outputs.
-v, --verbose Log each written file (wrote <path>); output is otherwise silent on success.
-h, --help Print the full flag table and exit.
--version Print the tool version and exit.

Non-fatal warning: diagnostics (e.g. a schema using the MessageSet wire format) go to stderr regardless of -v, and never change the exit code.

<stem> is the schema’s path relative to the first -I directory that contains it, and its basename when no -I does - so -I proto proto/sub/a.proto writes sub/a.rp.hpp, while sub/a.proto with no -I writes a.rp.hpp. The output therefore always stays under --out-dir. Three inputs are refused rather than resolved, because either answer would silently lose a schema: two entries that generate the same header, two that share a name relative to the include paths (they would deduplicate to one), and an import whose path escapes the output directory. Nothing is written when one of these fires.

Multiple entries resolve as one batch: shared imports parse once, every file in the union gets its decoder exactly once, and a decode profile resolves against all of them together. Each generated file covers the entry and its transitive imports and the well-known types it uses, plus the shared <stem>.rp.common.hpp and the runtime, so the output directory is self-contained.

CMake integration

RapidProto ships a rapidproto_generate() helper that turns a .proto into a linkable, header-only target: it runs rapidprotoc at build time, tracks the whole import closure (a touched import re-triggers generation, via a depfile), and puts the output directory on your include path. Link the target and #include the generated header.

How much the build system knows about the generated files depends on whether the generator exists at configure time. With find_package (an installed rapidproto), the helper asks the real generator (--list-outputs) and declares every generated file - deleting any of them regenerates it, and a schema error fails cmake itself with the generator’s own diagnostic. In-tree and under FetchContent the tool is built by your own buildsystem and cannot be asked yet, so the helper declares the listed schemas’ headers and the runtime copies; an imported schema’s headers are still generated and kept fresh, but deleting one by hand needs a regeneration (touch an entry, or rebuild from clean) to recover. A .proto entry that another build rule produces (so it does not exist at configure time) takes the same reduced path - which also means a mistyped entry path surfaces at build time, not at configure. Two rapidproto_generate() targets must not share an OUT_DIR - the helper refuses it at configure.

rapidproto_generate(my_schema
  GENERATOR   both                  # arena | stream | both           (default: arena)
  PROTOS      proto/person.proto    # one or more entry .proto files
  IMPORT_DIRS proto)                # -I roots your schema imports against
  # also: NAMESPACE_PREFIX <ns>, OUT_DIR <dir>, UNKNOWN_PRESENT (arena), NO_WELLKNOWN, DUMP (arena dumper),
  #       FIELD_MODES <file>... / DROP <name>... / RAW <name>... / UNKNOWN <message>...  (arena profiles)

add_executable(app main.cpp)
target_link_libraries(app PRIVATE my_schema)   # generates before `app` compiles, adds the include dir

Then #include "person.rp.hpp" (arena) and/or "person.rp.stream.hpp" (streaming); each is the entry’s stem under its import-relative path. GENERATOR both writes both decoders from one rapidprotoc invocation, so a single TU can use both models for one schema (see Using both models).

Get the helper and the rapidproto::rapidprotoc tool it drives, either way:

# Build from source within your build:
include(FetchContent)
FetchContent_Declare(rapidproto
  GIT_REPOSITORY https://github.com/VeaaC/rapidproto
  GIT_TAG        v0.4.0)                       # pin the release you want (v0.4.0 is the first
                                               # with the rp:: model roots this manual
                                               # documents; earlier tags spell every generated
                                               # type differently)
FetchContent_MakeAvailable(rapidproto)         # defines rapidproto_generate() + rapidproto::rapidprotoc

# …or use an installed RapidProto (cmake --install <build> --prefix <prefix>):
find_package(rapidproto REQUIRED)              # same helper + tool, imported

CMake version. Incremental import-tracking uses add_custom_command(DEPFILE): supported on Ninja at any version, and on the Makefile generators with CMake ≥ 3.20 (Xcode / the Visual Studio generator ≥ 3.21). On an older CMake with those generators the helper still generates correctly but won’t auto-retrigger on an import edit (it warns); re-run CMake or clean-build after editing an imported .proto.

Compile cost. Generated decoders are header-only, so every translation unit that includes them recompiles them. How compile seconds, .text size and compiler peak RSS are measured - and representative magnitudes - is in benchmarks.md; keep generated headers out of widely-included headers, exactly as with protoc.

Cross-compiling. rapidprotoc must run on the build host, not the target, so it must be a host build. Build/install RapidProto for the host and bring that host tool in (e.g. a host-prefixed find_package). rapidproto_generate() rejects the in-tree (target-built) tool when CMAKE_CROSSCOMPILING is set; ensure the imported rapidproto::rapidprotoc it sees is a host binary.