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.
- buffa makes protobuf feel native to Rust by splitting one schema into owned messages for writes and borrowed views for reads.
- Its editions model turns proto2 and proto3 differences into resolved policy, so codegen stays on one path instead of two runtimes.
- The performance story is about cached size and linear writes, not heroic allocation tricks.
- buffa wins by being opinionated about scope, with binary and JSON support in bounds and textproto and runtime reflection left out.
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.
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.
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 ❣️
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.
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.
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.
| Dimension | buffa | prost | rust-protobuf |
|---|---|---|---|
| Editions model | First-class policy layer | Not the organizing principle | Not the organizing principle |
| Read path | Generated borrowed view types | Owned decode path | Owned decode path |
| Owned and borrowed from one schema | Yes | No | No |
| Message ergonomics | MessageField<T> and EnumValue<T> | Lean owned API | Older generated API |
| Textproto and runtime reflection | Out of scope by design | Out of scope | Available in broader form |
| no_std posture | Core runtime is no_std-friendly | no_std-friendly | More 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.
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.





