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:
- Serialization can dominate the CPU. In data-heavy domains like robotics and autonomous vehicles, messages carry large payloads (LIDAR scans, camera frames), and serialization/deserialization can consume well over half of available CPU. Google has estimated serialization at ~30% of CPU time across its data centers.
- Deserializing messages you never read is pure waste. Many real-time systems only act on the most recent message and drop the rest β every message deserialized and discarded is wasted work.
- Shared-memory IPC makes serialization redundant. When subscribers can read the same physical memory directly, serializing into shared memory just to deserialize it again in every reader burns cycles for no benefit.
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.
- No serialization / deserialization on the hot path β the buffer is ready to send.
- Direct buffer access for bulk data: write straight into a vector's backing store, or allocate many sub-messages in a single allocation.
- Works in shared memory and other externally-provided buffers (fixed or resizable).
- Protobuf version compatibility is preserved β old and new message versions interoperate via per-message field metadata.
- Protobuf wire-format transcoding is available when you do need it (e.g. storing in systems like BigQuery that expect protobuf bytes).
- Familiar API β same accessor names as protobuf, with extra zero-copy helpers.
- proto3 (primary) and proto2 IDL support
- Message printing to
std::ostream - Fixed- and variable-sized buffers
- User-supplied per-buffer metadata
- Full
google.protobuf.Anysupport (zero-copy) - Enum printing and parsing
- Message reflection
- Field presence masks
- Bazel build integration
- Modern C++17 with Abseil
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:
- The source message is the C++ object your code interacts with. It is lightweight β it holds only a pointer to the runtime and an offset, not the field values. It can live on the stack, the heap, anywhere.
- The binary message is the actual data, stored in wire-format inside a relocatable
PayloadBuffer.
Your code Source message PayloadBuffer (the bytes you send)
ββββββββββββ βββββββββββββββββ βββββββββββββββββββββββββββββββββββ
β set_x(7) β βββββββΊ β offset + rt β βββββββΊ β [header][metadata][fields...] β
β x() β βββββββ β (no data!) β βββββββ β x = 7 ... β
ββββββββββββ βββββββββββββββββ βββββββββββββββββββββββββββββββββββ
- When you call
set_x(...), the value is written directly into the binary buffer. - When you call
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 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.