buffa-reflect
Runtime reflection for the buffa
protobuf implementation. Designed as a near-drop-in for
prost-reflect:
| crate | what it gives you |
|---|---|
buffa-reflect |
DescriptorPool, MessageDescriptor, FieldDescriptor, EnumDescriptor, OneofDescriptor, ReflectMessage, ReflectMessageView, DynamicMessage, proto3 JSON (serde), textproto |
buffa-reflect-derive |
#[derive(ReflectMessage)] proc-macro (re-exported by the runtime crate's derive feature) |
buffa-reflect-build |
build.rs integration that runs protoc/buf, emits OUT_DIR/file_descriptor_set.bin, decorates every generated message with the derive, and (optionally) impl ReflectMessageView for every borrowed *View<'a> |
Phase 1 (descriptor pool + build script) and Phase 2 (DynamicMessage,
JSON, textproto, gRPC server reflection, view reflection) are both
shipped — see specs/ for the design history.
Cargo features (buffa-reflect)
| feature | default | what it pulls in |
|---|---|---|
derive |
yes | the #[derive(ReflectMessage)] re-export |
dynamic |
yes | DynamicMessage, Value, MapKey, transcode_to_dynamic |
serde |
no | proto3 canonical JSON (serde::Serialize + DeserializeSeed) |
text-format |
no | textproto encode (to_text_format) and parse (parse_text_format) |
= { = "0.1", = ["serde", "text-format"] }
Quick start
# Cargo.toml
[]
= "0.4"
= "0.1"
[]
= "0.1"
// build.rs
// src/lib.rs
pub const FILE_DESCRIPTOR_SET_BYTES: & =
include_bytes!;
include_proto!;
// One `impl ReflectMessageView` block per generated `*View<'a>`,
// auto-emitted because `file_descriptor_set_bytes(..)` is configured.
// Pass `.generate_view_reflection(false)` on the builder to opt out.
include!;
Phase 1 — walk descriptors
Every generated message implements ReflectMessage:
use ;
let book = default;
let descriptor = book.descriptor;
for field in descriptor.fields
Lookups by JSON name, proto name, or tag number are available on both
MessageDescriptor and DescriptorPool:
let pool = decode?;
let library_desc = pool.get_message_by_name.unwrap;
let books = library_desc.get_field_by_name.unwrap;
assert!;
Phase 2 — DynamicMessage
DynamicMessage lets one binary read, mutate, and re-encode a message
of any shape that lives in the pool — without knowing the static Rust
type:
use ;
let pool = decode?;
let descriptor = pool.get_message_by_name.unwrap;
// Decode arbitrary wire bytes against a runtime descriptor.
let mut dyn_msg = decode?;
// Inspect.
for in dyn_msg.fields
// Mutate by name or by tag number; both forms are dual.
dyn_msg.set_field_by_name;
if let Some = dyn_msg.get_field_by_name_mut
// Validate without panicking.
let bad = dyn_msg.try_set_field_by_name;
assert!;
// Re-encode (preserves unknown fields, deterministic tag-number order).
let bytes = dyn_msg.encode_to_vec;
DynamicMessage is symmetric with the typed types via
transcode_to/transcode_from:
use Message as _;
use ReflectMessage as _;
let typed: Library = /* ... */;
// typed → dynamic (one wire round-trip; specialised to clone for
// `DynamicMessage` itself).
let dyn_msg = typed.transcode_to_dynamic;
// dynamic → typed.
let back: Library = dyn_msg.transcode_to?;
assert_eq!;
Runnable: cargo run -p buffa-reflect-example --example dynamic_message.
Phase 2 — proto3 canonical JSON (serde)
Enable the serde feature.
use ;
use DeserializeSeed as _;
// Serialize: `DynamicMessage: serde::Serialize`.
let json = to_string_pretty?;
// Deserialize: `MessageDescriptor: DeserializeSeed<'de>` — the
// descriptor itself is the seed, no helper struct needed.
let mut de = from_str;
let parsed: DynamicMessage = descriptor.clone.deserialize?;
assert_eq!;
// Knobs match prost-reflect for cross-ecosystem familiarity.
let mut buf = Vecnew;
let mut ser = new;
dyn_msg.serialize_with_options?;
// Strict mode rejects unknown fields (default ignores per the proto3
// JSON spec).
let strict = deserialize_with_options;
assert!;
Runnable: cargo run -p buffa-reflect-example --example json.
Phase 2 — textproto (text-format)
Enable the text-format feature.
use ;
// Default: single-line, machine-friendly (matches `protoc --decode`).
let compact = dyn_msg.to_text_format;
// Multi-line, indented; drop fields equal to the proto default.
let pretty = dyn_msg.to_text_format_with_options;
// Round-trip back into a `DynamicMessage` of the same descriptor.
let parsed = parse_text_format?;
assert_eq!;
Runnable: cargo run -p buffa-reflect-example --example text_format.
Phase 2 — view-type reflection
The auto-generated _reflect_views.rs include adds ReflectMessageView
to every borrowed *View<'a>. Reflection works on the zero-copy decode
path without owning the data:
use ;
let view: LibraryView =
new.decode_view?;
let descriptor = view.descriptor;
println!;
// Generic helpers can take any `ReflectMessageView<'_>`.
Runnable: cargo run -p buffa-reflect-example --example view_reflection.
Phase 2 — gRPC server reflection
examples/grpc-reflection/ ships
buffa-grpc-reflection, a drop-in for tonic-reflection backed by a
DescriptorPool. It lives outside the workspace to keep tonic /
prost out of the parent Cargo.lock.
let pool = decode?;
let = from_pool.build;
builder
.add_service // grpc.reflection.v1.ServerReflection
.add_service // grpc.reflection.v1alpha.ServerReflection
.add_service
.serve.await?;
Once the server is up, grpcurl localhost:50051 list enumerates every
service in the pool.
End-to-end demos
apps/example/ compiles a small library.proto with
nested messages, oneofs, maps, and an enum, then exercises every Phase 2
code path through cargo examples. From the workspace root:
examples/equivalence/ cross-checks
descriptor parsing against prost-reflect over the same
FileDescriptorSet.
Workspace layout
crates/
buffa-reflect/ # runtime descriptor pool, DynamicMessage, JSON, textproto
buffa-reflect-derive/ # proc-macro
buffa-reflect-build/ # build.rs library
apps/
example/ # end-to-end demos referenced by this README
examples/ # leaf workspaces (kept out of the main Cargo.lock)
equivalence/ # parser equivalence vs. prost-reflect
grpc-reflection/ # buffa-grpc-reflection (tonic gRPC server reflection)
specs/ # PRDs, design docs, impl plans
docs/research/ # background research used to scope each phase
License
Distributed under the terms of MIT. See LICENSE.
Copyright 2026 Tyr Chen