Skip to content

dallison/phaser

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

56 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

Phaser

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.


Why Phaser?

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.

Benefits at a glance

  • 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.

Features

  1. proto3 (primary) and proto2 IDL support
  2. Message printing to std::ostream
  3. Fixed- and variable-sized buffers
  4. User-supplied per-buffer metadata
  5. Full google.protobuf.Any support (zero-copy)
  6. Enum printing and parsing
  7. Message reflection
  8. Field presence masks
  9. Bazel build integration
  10. Modern C++17 with Abseil

How it works

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.

Quick start

1. Add a phaser_library to your build

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"

2. Create and use a message

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();

3. Zero-copy field access

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);

Protobuf interoperability

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 (type-erased operations & reflection)

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.).

Building from source

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.

Using Phaser without Bazel

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.

Project layout

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).

Documentation

The complete reference β€” message layout, buffer internals, the allocator, reflection, the Phaser Bank, and more β€” is in the Phaser User Guide.

License

Phaser is licensed under the Apache License 2.0.

About

Zero-copy protobuf transport

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors