buffa: Protobuf for Rust, Rebuilt Around Borrowed Reads

A pure-Rust runtime that treats protobuf editions as policy, then generates both owned messages and zero-copy views from the same schema.

9 min read • View on GitHub • More from anthropics

A wide black-ink editorial illustration shows a single protobuf scroll feeding into a split machine. One branch stamps out heavy, boxy Rust structs, while the other channels the same wire bytes into thin borrowed ribbons. It explains the core idea that one schema can produce both owned and zero-copy read surfaces.
One schema becomes two Rust surfaces, one optimized for mutation and serialization, the other for borrowing directly from the wire.
Key Takeaways

The clever move in buffa is not that it parses protobuf. It is that the same schema produces two different Rust personas: one owned, heap-friendly, and ready to mutate, the other borrowed, allocation-light, and built for reading bytes in place.

One schema, two ways to read it

That split is the article's real hook. Instead of forcing every decode into a fully owned object, buffa generates a paired API, so a message can exist as a normal Rust struct and as a view type that borrows fields like &str and &[u8] straight from the input buffer.

The same input bytes can become either an owned message or a borrowed view, and the write path reuses cached size instead of recalculating it.

pub struct Foo {
    pub name: String,
    pub payload: Vec<u8>,
    pub child: MessageField<Child>,
    pub state: EnumValue<State>,
}

pub struct FooView<'a> {
    pub name: &'a str,
    pub payload: &'a [u8],
    pub child: ChildView<'a>,
    pub state: EnumValue<State>,
}

That is the ergonomic bet. The owned type gives you the usual Rust ownership story. The view type gives you a read path that does not need to copy every string and byte buffer just to inspect a message.

A pure-Rust Protocol Buffers implementation with first-class protobuf editions support.

Project Description, Maintainer · anthropics/buffa README

Why this repo exists at all

Buffa is not a one-crate toy. It is a workspace: runtime in buffa/, code generation in buffa-codegen/, build integration in buffa-build/, protobuf descriptor types in buffa-descriptor/, well-known types in buffa-types/, and a protoc plugin to tie it together. That shape matters, because the project is trying to own the whole translation path from schema to Rust, not just the runtime half.

The README's tone is also part of the story. It says the project was Written by Claude ❣️, which makes the codebase feel less like a sprawling accumulation and more like a deliberately constrained system with a clear architecture and a strong editorial hand.

Written by Claude ❣️

Claude, AI Assistant / Creator · anthropics/buffa README

Editions become features, not branches

The second surprise is that buffa does not treat proto2 and proto3 as separate worlds. It resolves them into a single feature model, then bakes that decision into generated code as a ResolvedFeatures value. The effect is simple but deep: syntax stops being a fork in the runtime and becomes policy at codegen time.

A medium-distance black-ink illustration shows two old mechanical control levers converging into one clean central switch cabinet. The scene explains that multiple protobuf syntax modes collapse into one resolved policy layer instead of branching the runtime into separate systems.
Proto2 and proto3 are treated as feature presets, not separate code paths.

That choice lets the runtime stay lean. The codegen step decides whether presence is explicit or implicit, how unknown enum values should behave, and which validation rules should fire. The generated Rust code then carries those answers forward without rethinking protobuf semantics on every decode.

let size = msg.compute_size();
let mut out = Vec::with_capacity(size as usize);
msg.write_to(&mut out)?;

Inside the generated pair

Buffa's generated message API is where the design feels most Rust-native. Optional submessages do not trap you in an endless Option<Box<T>> maze. The MessageField<T> wrapper dereferences to a default instance, so reads stay clean while the wire format stays faithful.

The same goes for open enums. EnumValue<T> preserves unknown values instead of flattening them into a plain integer and hoping the caller remembers what happened. That matters in long-lived systems, because wire compatibility is not a side quest.

The view side follows the same logic. For a decoded message, buffa can hand you borrowed strings, borrowed bytes, and borrowed nested views through a MessageView trait. The result is a schema that behaves like a carefully split data model rather than a generic codec hiding behind generated structs.

Why serialization stays linear

The write path is disciplined. Buffa computes size first, caches it in the message, then writes once. That keeps nested length-prefix work from ballooning into repeated recalculation, which is exactly the kind of thing that turns a tidy encoder into an accidental time sink.

A close-up black-ink illustration shows a message body passing through a measuring gauge once, then a cached dial feeding a long uninterrupted writing stroke. It explains that serialization computes size first, stores the result, and then emits bytes in a single linear pass.
Buffa measures once, caches the result, then writes the message in one pass.

The implementation detail that makes this less hand-wavy is the cached size field. Buffa stores the result of size computation so the serializer does not have to derive it again every time it needs to emit a nested field. That is a small design choice with outsized consequences for throughput and predictability.

Where buffa beats the incumbents

The comparison is less about raw bragging rights and more about intent. Buffa is optimized around editions, borrowed reads, and a narrow, modern surface. The incumbent Rust protobuf libraries are strong in their own lanes, but they are not organized around the same core bet.

Dimensionbuffaprostrust-protobuf
Editions modelFirst-class policy layerNot the organizing principleNot the organizing principle
Read pathGenerated borrowed view typesOwned decode pathOwned decode path
Owned and borrowed from one schemaYesNoNo
Message ergonomicsMessageField<T> and EnumValue<T>Lean owned APIOlder generated API
Textproto and runtime reflectionOut of scope by designOut of scopeAvailable in broader form
no_std postureCore runtime is no_std-friendlyno_std-friendlyMore std-centric

The practical takeaway is not that buffa is trying to replace every protobuf tool you already know. It is narrower than that. It is betting that a Rust protobuf runtime should be explicit about ownership, explicit about editions, and explicit about what it will not do.

What buffa refuses to be

That restraint is a feature. Buffa supports binary wire format, Proto3 JSON, well-known types, and the everyday message shapes that matter in production. It does not chase text format or runtime reflection, and it does not pretend those omissions are accidents.

Text format (`textproto`) is not supported and is not planned.

Project Description, Maintainer · anthropics/buffa README

That boundary keeps the surface area small enough to reason about. In a crowded ecosystem, sometimes the strongest design move is not another feature, but a cleaner contract.