Thanks to visit codestin.com
Credit goes to lib.rs

#protobuf #buffa

buffa-reflect

Runtime reflection for the buffa protobuf implementation

2 unstable releases

Uses new Rust 2024

0.2.0 May 7, 2026
0.1.0 May 2, 2026

#404 in Encoding

21 downloads per month
Used in 5 crates (2 directly)

MIT license

250KB
5.5K SLoC

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)
buffa-reflect = { version = "0.1", features = ["serde", "text-format"] }

Quick start

# Cargo.toml
[dependencies]
buffa = "0.4"
buffa-reflect = "0.1"

[build-dependencies]
buffa-reflect-build = "0.1"
// build.rs
fn main() -> Result<(), Box<dyn std::error::Error>> {
    buffa_reflect_build::Builder::new()
        .file_descriptor_set_bytes("crate::FILE_DESCRIPTOR_SET_BYTES")
        .files(&["proto/acme/api/v1/library.proto"])
        .includes(&["proto/"])
        .compile()?;
    Ok(())
}
// src/lib.rs
pub const FILE_DESCRIPTOR_SET_BYTES: &[u8] =
    include_bytes!(concat!(env!("OUT_DIR"), "/file_descriptor_set.bin"));

buffa::include_proto!("acme.api.v1");

// 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!(concat!(env!("OUT_DIR"), "/_reflect_views.rs"));

Phase 1 — walk descriptors

Every generated message implements ReflectMessage:

use buffa_reflect::{Kind, ReflectMessage};

let book = library::Book::default();
let descriptor = book.descriptor();
for field in descriptor.fields() {
    println!(
        "#{:<2} {:<20} {:?}",
        field.number(),
        field.name(),
        field.cardinality(),
    );
    if let Kind::Enum(e) = field.kind() {
        for value in e.values() {
            println!("    = {} = {}", value.name(), value.number());
        }
    }
}

Lookups by JSON name, proto name, or tag number are available on both MessageDescriptor and DescriptorPool:

let pool = buffa_reflect::DescriptorPool::decode(FILE_DESCRIPTOR_SET_BYTES)?;
let library_desc = pool.get_message_by_name("acme.api.v1.Library").unwrap();
let books = library_desc.get_field_by_name("books").unwrap();
assert!(books.is_list());

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 buffa_reflect::{DescriptorPool, DynamicMessage, MapKey, Value};

let pool = DescriptorPool::decode(FILE_DESCRIPTOR_SET_BYTES)?;
let descriptor = pool.get_message_by_name("acme.api.v1.Library").unwrap();

// Decode arbitrary wire bytes against a runtime descriptor.
let mut dyn_msg = DynamicMessage::decode(descriptor.clone(), wire_bytes)?;

// Inspect.
for (field, value) in dyn_msg.fields() {
    println!("{} = {value:?}", field.name());
}

// Mutate by name or by tag number; both forms are dual.
dyn_msg.set_field_by_name("name", Value::String("Pump Room Library".into()));
if let Some(Value::Map(tags)) = dyn_msg.get_field_by_name_mut("tags") {
    tags.insert(MapKey::String("opened".into()), Value::String("1769".into()));
}

// Validate without panicking.
let bad = dyn_msg.try_set_field_by_name("name", Value::I32(42));
assert!(matches!(bad, Err(buffa_reflect::SetFieldError::InvalidType { .. })));

// 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 buffa::Message as _;
use buffa_reflect::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!(typed.encode_to_vec(), back.encode_to_vec());

Runnable: cargo run -p buffa-reflect-example --example dynamic_message.

Phase 2 — proto3 canonical JSON (serde)

Enable the serde feature.

use buffa_reflect::{DeserializeOptions, DynamicMessage, SerializeOptions};
use serde::de::DeserializeSeed as _;

// Serialize: `DynamicMessage: serde::Serialize`.
let json = serde_json::to_string_pretty(&dyn_msg)?;

// Deserialize: `MessageDescriptor: DeserializeSeed<'de>` — the
// descriptor itself is the seed, no helper struct needed.
let mut de = serde_json::de::Deserializer::from_str(&json);
let parsed: DynamicMessage = descriptor.clone().deserialize(&mut de)?;
assert_eq!(dyn_msg, parsed);

// Knobs match prost-reflect for cross-ecosystem familiarity.
let mut buf = Vec::new();
let mut ser = serde_json::Serializer::new(&mut buf);
dyn_msg.serialize_with_options(
    &mut ser,
    &SerializeOptions::new()
        .stringify_64_bit_integers(false)
        .use_proto_field_name(true)
        .use_enum_numbers(true)
        .skip_default_fields(false),
)?;

// Strict mode rejects unknown fields (default ignores per the proto3
// JSON spec).
let strict = DynamicMessage::deserialize_with_options(
    descriptor.clone(),
    &mut serde_json::de::Deserializer::from_str(r#"{ "futureField": 1 }"#),
    &DeserializeOptions::new().deny_unknown_fields(true),
);
assert!(strict.is_err());

Runnable: cargo run -p buffa-reflect-example --example json.

Phase 2 — textproto (text-format)

Enable the text-format feature.

use buffa_reflect::{DynamicMessage, FormatOptions};

// 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(
    &FormatOptions::new().pretty(true).skip_default_fields(true),
);

// Round-trip back into a `DynamicMessage` of the same descriptor.
let parsed = DynamicMessage::parse_text_format(descriptor.clone(), &pretty)?;
assert_eq!(dyn_msg, parsed);

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 buffa_reflect::{Kind, ReflectMessageView};

let view: __buffa::view::LibraryView<'_> =
    buffa::DecodeOptions::new().decode_view(wire_bytes)?;
let descriptor = view.descriptor();
println!("{}: {} fields", descriptor.full_name(), descriptor.fields().len());

// Generic helpers can take any `ReflectMessageView<'_>`.
fn count_message_fields<'a, V: ReflectMessageView<'a>>(v: &V) -> usize {
    v.descriptor()
        .fields()
        .filter(|f| matches!(f.kind(), Kind::Message(_)))
        .count()
}

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 = buffa_reflect::DescriptorPool::decode(FILE_DESCRIPTOR_SET_BYTES)?;
let (v1, v1alpha) = buffa_grpc_reflection::Builder::from_pool(pool).build();

tonic::transport::Server::builder()
    .add_service(v1)        // grpc.reflection.v1.ServerReflection
    .add_service(v1alpha)   // grpc.reflection.v1alpha.ServerReflection
    .add_service(my_service)
    .serve(addr).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:

cargo run -p buffa-reflect-example                          # Phase 1: descriptor walk
cargo run -p buffa-reflect-example --example dynamic_message
cargo run -p buffa-reflect-example --example json
cargo run -p buffa-reflect-example --example text_format
cargo run -p buffa-reflect-example --example view_reflection

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

Dependencies

~3.5–5MB
~94K SLoC