buffa

repository·main·Indexed 19 days ago

https://github.com/anthropics/buffa

A pure Rust Protocol Buffers implementation featuring first-class support for the protobuf 'editions' model. It provides high-performance, zero-copy serialization and deserialization for binary, JSON, and text formats. The ecosystem includes buffa-descriptor, which provides self-hosted Rust implementations of standard Protobuf descriptor types for compile-time code generation and runtime reflection.

Tokens
94.1K
Snippets
243
Records
342
Agent score
67%

What's inside buffa

  1. What is buffa?

    main

    buffa is a pure-Rust implementation of Protocol Buffers with first-class support for protobuf editions. It is designed to treat editions as a core abstraction, allowing it to support both proto2 and proto3 via feature presets.

    Key features include:

    • Two-tier types: Generates both owned (MyMessage) and zero-copy view (MyMessageView<'a>) types.
    • Efficient serialization: Uses cached encoded sizes to ensure linear-time serialization.
    • Advanced types: Provides MessageField<T> (derefs to default when unset) and EnumValue<T> (type-safe enums that preserve unknown values).
    • Reflection: Supports runtime reflection via buffa-descriptor (requires reflect feature).
    • Environment support: Works in no_std environments with alloc support.
  2. Overview of buffa-descriptor

    main

    The buffa-descriptor crate provides self-hosted Rust implementations of standard Protobuf descriptor types. It includes types for google/protobuf/descriptor.proto and google/protobuf/compiler/plugin.proto. These types are generated by buffa-codegen and serve as the foundation for both compile-time code generation and runtime reflection within the buffa ecosystem.

    Key characteristics:

    • Zero external protobuf dependencies: The only runtime dependency is the buffa crate itself.
    • Self-hosted: It uses types generated by the buffa toolchain rather than relying on external protobuf libraries.
  3. Understand the Buffa crate ecosystem

    main

    Buffa is a pure Rust Protocol Buffers implementation designed with a first-class support for Protobuf Editions. The project is split into several crates to separate the runtime from the code generation tools:

    • buffa: The core runtime library. All generated code depends on this. It provides the Message and MessageView traits, wire format codecs, and support for no_std environments.
    • buffa-types: Provides pre-generated Rust types for Google's Well-Known Types (e.g., Timestamp, Any, Struct). It is a lightweight dependency that does not require protoc or buffa-codegen at build time.
    • buffa-descriptor: Contains Rust types for Protobuf descriptors and compiler plugin messages. It is used by the code generation logic.
    • buffa-codegen: The shared logic that transforms Protobuf descriptors into Rust source code.
    • protoc-gen-buffa: A protoc plugin binary used as the primary entry point for code generation.
    • buffa-build: A convenience crate for integrating code generation into your Rust build.rs script.
  4. Understand the Buffa benchmark history and metrics

    main

    The benchmarks/history directory tracks Buffa's performance across releases to identify regressions or improvements. Unlike general benchmarks that compare Buffa to other libraries, this tracks Buffa against its own past versions.

    Key Metrics

    • Throughput (MiB/s): The primary headline metric. It is used because it remains comparable across releases even if the benchmark dataset size changes.
    • Median nanoseconds per iteration: Stored alongside throughput for detailed timing.
    • Median across cores: To ensure robustness against noise, each benchmark number is the median across several runs on distinct physical cores.

    Measurement Methodology

    To isolate code changes from environmental noise, the following are held constant across the entire series:

    • The Machine: Runs are performed on a quiesced host (CPU turbo disabled, performance frequency governor, and benchmarks pinned to physical cores).
    • The Build Profile: Every binary is built with lto=true and codegen-units=1 to ensure a reproducible, optimized layout.
    • The Compiler: A specific toolchain is pinned via RUSTUP_TOOLCHAIN (e.g., 1.96.0) so that performance shifts reflect code changes rather than compiler updates.
  5. Use Lazy Views for large messages

    main

    By enabling Config::lazy_views(true) during code generation, Buffa produces a third type: a Lazy View (FooLazyView<'a>).

    Unlike eager views which decode the entire sub-message tree immediately, lazy views perform a single non-recursive scan and only decode sub-messages when they are explicitly accessed via .get(). This is ideal for workloads that only need to read a few fields from very large, deeply nested messages.

    Note on Security: Lazy decoding uses a per-subtree recursion and unknown-field limit. If you are handling untrusted input and require strict global bounds, use the eager decode_view path instead.

    // Using a lazy view
    let person = PersonLazyView::decode_lazy(&wire_bytes)?;
    
    // Sub-message is only decoded here, on access
    if let Some(addr) = person.address.get()? {
        println!("city: {}", addr.city);
    }
  6. Optimize decode throughput with `MessageView::decode_view`

    main

    Buffa provides two primary decoding paths. Choosing the right one depends on your ownership requirements:

    1. Owned Decode (Message::decode_from_slice): Creates owned data structures. This is standard for most use cases but involves heap allocations (e.g., Box<T> for nested messages) and potential overhead from unknown-field preservation.
    2. View Decode (MessageView::decode_view): This is the recommended fast path for read-only request handling. It sidesteps allocation costs by borrowing strings and bytes directly from the input buffer. It does not use Box and avoids the overhead of owned types.

    Performance Tip: If you do not need to preserve unknown fields (which adds a Vec header and pointer overhead per message), you can disable this feature using .preserve_unknown_fields(false) to improve throughput.

  7. Understand the ReflectMessage contract and ValueRef types

    main

    The ReflectMessage trait defines how to interact with message data via reflection. When calling get(field: &FieldDescriptor), the behavior follows these rules:

    • Singular Scalars: If the field is absent, get() returns the type's default value. Presence is checked via has().
    • Repeated/Map Fields: If absent, get() returns an empty list or map.
    • Enums: Returns the number only, matching DynamicMessage behavior.
    • Messages: Returns a ValueRef::Message containing either a borrowed view (MessageFieldView<V>) or a static default instance if unset.

    ValueRef is the container for reflected values. It uses trait objects for collections to avoid materializing full data structures:

    • ValueRef::List(&'a dyn ReflectList)
    • ValueRef::Map(&'a dyn ReflectMap)
    • ValueRef::String(&'a str)
    • ValueRef::Bytes(&'a [u8])
    • ValueRef::I32(i32) (and other scalars)
    • ValueRef::EnumNumber(i32)
    pub enum ValueRef<'a> {
        String(&'a str),
        Bytes(&'a [u8]),
        I32(i32),
        EnumNumber(i32),
        Message(ReflectCow<'a>),
        List(&'a dyn ReflectList),
        Map(&'a dyn ReflectMap),
    }
  8. Understand the shape of generated Buffa structs

    main

    Buffa generates Rust structs that map to Protobuf messages. Key characteristics include:

    • Sub-messages: Represented as buffa::MessageField<T> rather than Option<Box<T>> to allow inline storage and ergonomic access.
    • Open Enums: Represented as EnumValue<E> to preserve unknown integer values (Proto3 behavior).
    • Unknown Fields: A hidden field __buffa_unknown_fields of type buffa::UnknownFields is included to preserve data from newer schema versions.
    • Module Nesting: Nested messages are placed in modules named after the parent (e.g., outer::Inner) rather than being flattened (e.g., OuterInner).
    • No Serialization State: Structs do not contain internal mutability or size caches; sizes are managed externally via a SizeCache.
    // Example Protobuf:
    // message Person {
    //   string name = 1;
    //   int32 id = 2;
    //   repeated string tags = 3;
    //   Address address = 4;
    //   optional string nickname = 5;
    // }
    
    // Generated Rust:
    pub struct Person {
        pub name: String,
        pub id: i32,
        pub tags: Vec<String>,
        pub address: buffa::MessageField<Address>,
        pub nickname: Option<String>,
        #[doc(hidden)]
        pub __buffa_unknown_fields: buffa::UnknownFields,
    }
  9. Handle Descriptor types with `buffa-descriptor`

    main

    If your protobuf files reference google/protobuf/descriptor.proto or google/protobuf/compiler/plugin.proto types (such as FieldDescriptorProto or FileOptions) as field types, you must add buffa-descriptor as a dependency.

    Note that if you generate code with views=true, json=true, or text=true, you must also enable the corresponding features in buffa-descriptor to ensure the generated code can use them.

    # Example: Codegen with views and json enabled
    cargo add buffa-descriptor --features views,json
  10. How to handle divergent Protobuf representations

    main

    If you generate the same .proto file in two different crates using different representation types (e.g., one uses SmolStr and another uses Bytes), Rust treats them as unrelated types (a::Foo and b::Foo). They cannot be used interchangeably.

    Recommended Solutions:

    1. Use extern_path (Best Practice): Instead of generating the same proto twice, use extern_path to point to a single, shared definition. This ensures one definition and zero conversions.
    2. Manual From Implementation: If you must have two different representations, the most efficient way to convert between them is to hand-write a static field-by-field impl From<a::Foo> for b::Foo. This avoids the overhead of a wire round-trip (encoding/decoding) and dynamic dispatch.

    Note on Conversions: Because different representations own separate buffers, any conversion path will require allocating the destination's string and bytes fields.

  11. Handle enum fields with EnumValue<E>

    main

    Buffa uses EnumValue<E> for open enums (proto3 default) and bare E for closed enums (proto2).

    • Reading: Match on buffa::EnumValue::Known(variant) or buffa::EnumValue::Unknown(value). You can also use direct comparison (e.g., msg.status == Status::ACTIVE).
    • Setting: Use .into() on the enum variant.
    • Raw Access: Use .to_i32() to get the underlying integer.
     // Setting an enum field
    -msg.status = Status::Active as i32;
    +msg.status = Status::ACTIVE.into();
    
     // Reading
    -match Status::try_from(msg.status) {
    -    Ok(Status::Active) => { /* ... */ }
    -    Ok(s) => { /* other known variant */ }
    -    Err(_) => { /* unknown value */ }
    -}
    +match msg.status {
    +    buffa::EnumValue::Known(Status::ACTIVE) => { /* ... */ }
    +    buffa::EnumValue::Known(s) => { /* other known variant */ }
    +    buffa::EnumValue::Unknown(v) => { /* unknown value */ }
    +}
    +// or use direct comparison:
    +if msg.status == Status::ACTIVE { /* ... */ }
    
     // Getting the raw integer
    -let raw: i32 = msg.status;
    +let raw: i32 = msg.status.to_i32();
  12. How to use custom owned types with buffa

    main

    Buffa allows you to use your own owned representations for Protobuf fields (like string or bytes) by implementing the buffa::ProtoString trait on a crate-local newtype. This is controlled via the string_type_custom knob in buffa_build.

    To implement this for a foreign string type (e.g., smol_str::SmolStr):

    1. Create a newtype wrapper around your preferred string type.
    2. Implement buffa::ProtoString for that newtype.
    3. Configure buffa_build to use your newtype's path using the string_type_custom option.

    Note: The buffa-smolstr crate in this repository is a reference implementation only and is not published to crates.io to avoid dependency version conflicts. You should copy the implementation pattern into your own crate and point string_type_custom to your local newtype.