Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .changes/unreleased/added-20260713-121537.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
kind: Added
body: |-
**`buffa_build::Config::shared_descriptor_pool`** — enables the shared descriptor pool (see `CodeGenConfig::shared_descriptor_pool`) from `build.rs`. The descriptor set is written once as a binary sidecar next to the generated tree and `include_bytes!`-d by the shared `__buffa_fds` module, removing both the per-package duplication and the byte-literal source expansion. Requires reflection and `.include_file(...)`. With a checked-in `out_dir`, commit the emitted `*.descriptor_set.binpb` sidecar alongside the generated `.rs`.
time: 2026-07-13T12:15:37.209981-04:00
290 changes: 283 additions & 7 deletions buffa-build/src/lib.rs

Large diffs are not rendered by default.

16 changes: 8 additions & 8 deletions buffa-codegen/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1450,18 +1450,18 @@ pub struct CodeGenConfig {
/// instance.
///
/// The shared root module itself is emitted by the module-tree builder
/// (`buffa-build` or `protoc-gen-buffa-packaging`), not by `generate`, so
/// this mode requires one of those front-ends to assemble the tree.
/// Consumers that wire the per-package modules by hand should leave it
/// `false` (the default), which keeps today's self-contained per-package
/// embedding.
/// (`buffa-build`), not by `generate`, so this mode requires that
/// front-end to assemble the tree. Consumers that wire the per-package
/// modules by hand should leave it `false` (the default), which keeps the
/// self-contained per-package embedding.
///
/// Use the same setting for every codegen run assembled into one module
/// tree. Packages generated with this set to `false` keep their own pools
/// even when sibling packages delegate to a shared pool; this mixed setup
/// is not diagnosed.
///
/// Defaults to `false`. Has no effect unless `generate_reflection` is on.
/// Defaults to `false`. [`generate`] errors when this is on without
/// `generate_reflection`.
pub shared_descriptor_pool: bool,
/// Gate the reflection impls behind a `reflect` crate feature, *without*
/// gating json/views/text (unlike
Expand Down Expand Up @@ -2542,8 +2542,8 @@ pub enum IncludeMode<'a> {
/// [shared-pool mode](CodeGenConfig::shared_descriptor_pool).
///
/// `file_descriptors` is the full transitive closure (the same slice passed to
/// [`generate`]). Front-ends (`buffa-build`, `protoc-gen-buffa-packaging`) call
/// this to obtain the single copy of the bytes, then hand them to
/// [`generate`]). Front-ends (`buffa-build`) call this to obtain the single
/// copy of the bytes, then hand them to
/// [`shared_descriptor_root_module`].
#[must_use]
pub fn encode_descriptor_set(
Expand Down
15 changes: 15 additions & 0 deletions buffa-test/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -754,6 +754,21 @@ fn main() {
.compile()
.expect("buffa_build failed for lazy_views_lean.proto");

// Shared descriptor pool, `$OUT_DIR` mode — the real build-script flow:
// cargo sets OUT_DIR, the descriptor-set sidecar is written there, and the
// include file references it via `concat!(env!("OUT_DIR"), ...)`. Proves
// the sidecar lands exactly where the emitted path expects at consumer
// compile time (the explicit-out_dir flavor is covered by buffa-build's
// own unit tests).
buffa_build::Config::new()
.files(&["protos/shared_pool_a.proto", "protos/shared_pool_b.proto"])
.includes(&["protos/"])
.reflect_mode(buffa_build::ReflectMode::VTable)
.include_file("sharedpool_include.rs")
.shared_descriptor_pool(true)
.compile()
.expect("buffa_build failed for shared_pool protos");

// Edition 2024 — requires protoc v30+ (stabilized edition 2024).
// Older protoc rejects it with "later than the maximum supported edition".
// Skip gracefully on older protoc so the crate still builds; tests are
Expand Down
10 changes: 10 additions & 0 deletions buffa-test/protos/shared_pool_a.proto
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
// Shared descriptor pool e2e (`shared_descriptor_pool(true)`, `$OUT_DIR`
// mode): package A of the two-package tree that must observe one shared pool.

syntax = "proto3";

package sharedpool.a;

message MsgA {
int32 id = 1;
}
12 changes: 12 additions & 0 deletions buffa-test/protos/shared_pool_b.proto
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
// Shared descriptor pool e2e: package B, with a cross-package reference into
// package A that must resolve through the one shared pool.

syntax = "proto3";

package sharedpool.b;

import "shared_pool_a.proto";

message MsgB {
sharedpool.a.MsgA a = 1;
}
9 changes: 9 additions & 0 deletions buffa-test/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -662,6 +662,15 @@ pub mod mixed_reflect_parent {
buffa::include_proto!("mixedref.parent");
}

// Shared descriptor pool (`shared_descriptor_pool(true)`, `$OUT_DIR` mode):
// the include file hosts the one `__buffa_fds` root at this module's top
// level, with both packages (`sharedpool::a`, `sharedpool::b`) delegating to
// it. See `src/tests/shared_pool.rs`.
#[allow(clippy::derivable_impls, clippy::match_single_binding)]
pub mod shared_pool {
include!(concat!(env!("OUT_DIR"), "/sharedpool_include.rs"));
}

#[allow(
clippy::derivable_impls,
clippy::match_single_binding,
Expand Down
1 change: 1 addition & 0 deletions buffa-test/src/tests/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ mod proto2;
mod proto3_semantics;
mod repeated_type;
mod rope_encode;
mod shared_pool;
mod string_type;
mod textproto;
mod type_prefix;
Expand Down
44 changes: 44 additions & 0 deletions buffa-test/src/tests/shared_pool.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
//! Shared descriptor pool (`shared_descriptor_pool(true)`, `$OUT_DIR` mode).
//!
//! The build script compiles `shared_pool_a.proto` / `shared_pool_b.proto`
//! with one shared `__buffa_fds` root (see `build.rs` and
//! `crate::shared_pool` in lib.rs); these tests prove both packages observe
//! the same pool instance and that cross-package symbols resolve through it.
//! With the option off, each package builds its own per-package pool, so the
//! `Arc::ptr_eq` assertion here would fail.

use crate::shared_pool::sharedpool::{a, b};

#[test]
fn packages_share_one_pool_instance() {
let pool_a = a::descriptor_pool();
let pool_b = b::descriptor_pool();
assert!(
std::sync::Arc::ptr_eq(pool_a, pool_b),
"both packages must delegate to the one shared pool"
);
// And the bytes themselves are physically one copy, not two equal ones.
assert!(
core::ptr::eq(
a::FILE_DESCRIPTOR_SET_BYTES.as_ptr(),
b::FILE_DESCRIPTOR_SET_BYTES.as_ptr()
),
"both packages must alias the one embedded descriptor set"
);
}

#[test]
fn shared_pool_resolves_both_packages() {
// One pool covers the whole codegen run: either package's handle resolves
// both packages' symbols, including the type `sharedpool.b.MsgB`
// references across the package boundary.
let pool = b::descriptor_pool();
let msg_b = pool
.message_by_name("sharedpool.b.MsgB")
.expect("MsgB registered in the shared pool");
assert!(msg_b.field_by_name("a").is_some(), "field a on MsgB");
assert!(
pool.message_by_name("sharedpool.a.MsgA").is_some(),
"cross-package type must resolve through the same pool"
);
}
20 changes: 20 additions & 0 deletions docs/guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,7 @@ The macro pulls in `OUT_DIR/<dotted.pkg>.mod.rs`, which in turn includes the per
| `.bytes_type_custom(path)` / `.bytes_type_custom_in(path, &[...])` | — | Use a custom `bytes` representation by Rust path |
| `.generate_reflection(bool)` | `false` | Emit reflection support (vtable mode) plus an embedded per-package descriptor pool (see [Runtime reflection](#runtime-reflection)) |
| `.reflect_mode(mode)` | `Off` | Finer-grained reflection selector: `ReflectMode::{Off, Bridge, VTable}` |
| `.shared_descriptor_pool(bool)` | `false` | Embed the reflection descriptor set once (as an `include_bytes!` sidecar) instead of per package; every package delegates to it. Requires `.include_file(...)` and reflection. With a checked-in `out_dir`, commit the emitted `*.descriptor_set.binpb` sidecar alongside the generated `.rs`. See [Runtime reflection](#runtime-reflection) |
| `.idiomatic_enum_aliases(bool)` | `true` | Emit `UpperCamelCase` associated-const aliases for enum values (see the aliases note under `EnumValue<T>`) |
| `.file_per_package(bool)` | `false` | Emit one `<dotted.package>.rs` per package instead of per-proto-file content + a stitcher |
| `.idiomatic_imports(bool)` | `false` | **Experimental.** Emit `use`-backed short type names at the package root (struct fields read `MessageField<Timestamp>` instead of fully-qualified paths). Requires `.file_per_package(true)`. Only type declarations are shortened — impl bodies and nested modules stay fully qualified — and the generated file must keep its `#[allow]` wrapper (the short names coexist with qualified impl-body paths, which `unused_qualifications` would otherwise flag) |
Expand Down Expand Up @@ -1969,6 +1970,25 @@ magnitude (14x for the comment-heavy well-known-types package). If you need
proto comments at runtime, or a set scoped to specific files, build a
descriptor set directly with `protoc --include_source_info` or `buf build`.

Because the embedded set covers the whole codegen run, a multi-package run
duplicates the same bytes once per package — for large proto trees that
duplication dominates crate size. **`shared_descriptor_pool`** embeds the set
once instead: a single `__buffa_fds` module at the module-tree root holds the
one `FILE_DESCRIPTOR_SET_BYTES` copy and the one lazily-built pool, and every
package's `descriptor_pool()` / `FILE_DESCRIPTOR_SET_BYTES` delegates to it —
the per-package API is unchanged, but all packages observe the same pool
instance, which also lets `DynamicMessage` values from different packages be
compared and composed (those operations require one pool). From `build.rs`,
enable it with `.shared_descriptor_pool(true)` (requires `.include_file(...)`
and reflection; the descriptor set is written as a `*.descriptor_set.binpb`
sidecar and `include_bytes!`-d, so commit the sidecar alongside a checked-in
`out_dir`, and mark it `binary` in `.gitattributes`). Consume the tree through
the include file: each package delegates to the shared root by a fixed number
of `super::` hops, so `include_proto!` per package does not compile in this
mode, and two shared-pool `compile()` calls need separate enclosing modules.
Packages generated with the option *off* silently keep building their own
separate pools — set it uniformly across a tree.

Two Cargo notes:

- The consuming crate must depend on `buffa-descriptor` with the `reflect`
Expand Down
Loading