Zero-copy Protocol Buffers for C++ — no serialization required.
Phaser is a Protocol Buffers (protobuf) compiler plugin that
generates C++ message classes whose data lives directly in a memory buffer, in
wire-format, instead of in a tree of heap-allocated objects. Once a message is built,
it can be written to disk, placed in shared memory, or sent over an IPC system without
a serialization step — the bytes in the buffer are the message.
The generated API is intentionally almost identical to the standard protobuf C++ API, so if you know protobuf, you already know Phaser.
📖 For the full reference, see the Phaser User Guide.
Every program that uses classic protobuf follows the same pattern: build messages as heap (or arena) objects, serialize them into a buffer, send the buffer, then deserialize on the other side. For small messages the conversion cost is negligible — but it isn't always small:
Phaser removes that overhead. By writing values directly into their final location in the buffer, message construction can be up to an order of magnitude faster, and reading costs nothing until you actually touch a field.
std::ostreamgoogle.protobuf.Any support (zero-copy)Phaser runs as a plugin to protoc. protoc parses your .proto files and hands the
descriptors to Phaser, which emits C++ (*.phaser.h / *.phaser.cc).
The key idea is the split between two representations:
PayloadBuffer.Your code Source message PayloadBuffer (the bytes you send)
┌──────────┐ ┌───────────────┐ ┌─────────────────────────────────┐
│ set_x(7) │ ──────► │ offset + rt │ ──────► │ [header][metadata][fields...] │
│ x() │ ◄────── │ (no data!) │ ◄────── │ x = 7 ... │
└──────────┘ └───────────────┘ └─────────────────────────────────┘
set_x(...), the value is written directly into the binary buffer.x(), the value is read back from the binary buffer, located via a
small per-message field-metadata array. That indirection is what enables protobuf's
version compatibility: a reader built with a different schema version can still find the
fields present in the data.The PayloadBuffer (from the cpp_toolbelt
library) is a relocatable heap — a malloc/free/realloc allocator that uses only offsets
(never raw pointers), so the whole buffer can be copied or moved anywhere. It offers a fast
bitmap allocator for small blocks (performance mode, the default) and a free-list
allocator that trades speed for compactness (size mode), selectable via
::phaser::Tuning.
phaser_library to your buildPhaser integrates with Bazel through the phaser_library rule. Point it at a standard
proto_library, much like you would a cc_proto_library:
load("@phaser//phaser:phaser_library.bzl", "phaser_library")
proto_library(
name = "foo_proto",
srcs = ["Foo.proto"],
)
phaser_library(
name = "foo_phaser",
add_namespace = "phaser", # optional: avoids clashing with protobuf classes
deps = [":foo_proto"],
)
If Foo.proto is in package foo.bar, the generated classes live in
::foo::bar::phaser (when add_namespace = "phaser"), and you include them as you would
any protobuf header:
#include "foo/bar/Foo.phaser.h"
Creating a message looks just like protobuf — the binary data is backed by a dynamic buffer allocated from the heap that grows as needed:
foo::bar::phaser::TestMessage msg; // optional: TestMessage msg(initial_size, tuning);
msg.set_x(1234);
// The buffer is ready to send — no serialize step.
SendMessage(msg.Data(), msg.ByteSizeLong());
Build directly inside an externally-provided buffer (e.g. shared memory from an IPC system):
auto msg = foo::bar::phaser::TestMessage::CreateMutable(buffer, size);
msg.set_x(1234);
Read a message received in a read-only buffer (all field access is bounds-checked against the buffer you provide):
auto msg = foo::bar::phaser::TestMessage::CreateReadonly(buffer, size);
int x = msg.x();
Beyond the standard protobuf accessors, Phaser adds helpers that hand you the final storage location so you can skip intermediate copies:
// Strings/bytes: allocate space and write straight into it.
absl::Span<char> dst = msg.allocate_s(len);
// Repeated primitives: get a mutable span over the backing store.
msg.resize_vi32(n);
absl::Span<int32_t> data = msg.vi32_as_mutable_span();
// Repeated messages: allocate many at once (one allocation).
std::vector<InnerMessage*> items = msg.allocate_vm(n);
Phaser's native layout is not protobuf wire-format, but full transcoding is provided for when you need to interoperate with protobuf-based systems:
size_t SerializedSize() const;
bool SerializeToArray(char* array, size_t size) const;
bool ParseFromArray(const char* array, size_t size);
bool SerializeToString(std::string* str) const;
std::string SerializeAsString() const;
bool ParseFromString(const std::string& str);
google.protobuf.Any is supported with zero-copy semantics: the value field holds a real
binary message you can access directly (via Is<T>() / As<T>() / MutableAny<T>()), with
PackFrom / UnpackTo provided for protobuf-compatible copying.
The Phaser Bank lets you operate on messages given only their type name — stream, clear, copy, transcode, allocate, and reflect over fields — without compile-time knowledge of the type. Message libraries register themselves via static initializers, so they just need to be linked in:
absl::StatusOr<bool> present =
::phaser::PhaserBankHasField("foo.bar.TestMessage", msg, 100);
auto field = ::phaser::PhaserBankGetFieldByNumber<::phaser::Int32Field<>>(
"foo.bar.TestMessage", msg, 100);
int value = (*field)->Get();
See the user guide's Phaser Bank and Message information sections for the full surface
(reflection, MessageInfo/FieldInfo, protobuf transcoding helpers, etc.).
Phaser uses Bazel (with Bzlmod) and is developed against the version
pinned in .bazelversion. Dependencies (Abseil, protobuf, cpp_toolbelt,
GoogleTest, …) are declared in MODULE.bazel.
# Build everything
bazelisk build //phaser/...
# Run the tests
bazelisk test //phaser/...
# AddressSanitizer build/test (see .bazelrc for the asan config)
bazelisk test --config=asan //phaser/...
On Apple Silicon, the asan config already pulls in the native apple_silicon settings;
see .bazelrc for the available configurations.
Phaser is a protoc plugin, so any build system can drive it as long as the plugin binary
and dependencies are available:
protoc --plugin=protoc-gen-phaser=DIR/bin/phaser/compiler/phaser \
--phaser_out=add_namespace=NS,package_name=PACKAGE,target_name=TARGET:OUTPUT_DIR \
-I IPATH \
FILE...
Output is written to OUTPUT_DIR/PACKAGE/TARGET. See the user guide for the full argument
reference.
| Path | Description |
|---|---|
phaser/compiler/ |
The protoc plugin that generates C++ code (gen, enum_gen, message_gen, main). |
phaser/runtime/ |
The runtime library: Message, fields, vectors, unions, wire-format, the Phaser Bank, and PayloadBuffer glue. |
phaser/phaser_library.bzl |
The phaser_library Bazel rule and supporting aspect. |
phaser/testdata/ |
Example .proto files used by the tests. |
phaser/docs/ |
Reference documentation (the user guide). |
The complete reference — message layout, buffer internals, the allocator, reflection, the Phaser Bank, and more — is in the Phaser User Guide.
Phaser is licensed under the Apache License 2.0.
1.1.12026-06-11 |