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
3 changes: 3 additions & 0 deletions .changes/unreleased/added-20260830-210426.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
kind: Added
body: '**`CodeGenConfig::shared_descriptor_pool_root`** (#399): override the computed `super::`-relative path to the shared `__buffa_fds` root in shared-pool mode (`shared_descriptor_pool`) with a caller-supplied path, for workspaces where one crate does not host the whole package tree as nested modules (e.g. one independent Cargo crate per proto package). `None` (the default) keeps the existing `super::`-relative behavior — purely additive, no change to any existing consumer. Reachable only by direct `buffa-codegen` callers this release; `buffa-build` and the `protoc-gen-buffa*` plugins do not expose it yet.'
time: 2026-08-30T21:04:26.915917-04:00
126 changes: 119 additions & 7 deletions buffa-codegen/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1457,9 +1457,12 @@ pub struct CodeGenConfig {
/// The shared root module itself is emitted by the module-tree builder
/// (`buffa-build`, or `protoc-gen-buffa-packaging` with its matching
/// `shared_descriptor_pool=true`), 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 the self-contained per-package embedding.
/// requires one of those front-ends to assemble the tree — unless
/// [`shared_descriptor_pool_root`](Self::shared_descriptor_pool_root) is
/// set, in which case the root can live anywhere the caller assembled it.
/// Consumers that wire the per-package modules by hand should leave both
/// `false`/`None` (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
Expand All @@ -1469,6 +1472,34 @@ pub struct CodeGenConfig {
/// Defaults to `false`. [`generate`] errors when this is on without
/// `generate_reflection`.
pub shared_descriptor_pool: bool,
/// When [`shared_descriptor_pool`](Self::shared_descriptor_pool) is on,
/// use this path to the shared `__buffa_fds` module verbatim instead of
/// computing a `super::`-relative one.
///
/// The default `super::` path assumes one crate hosts the whole package
/// tree as nested modules. A workspace with one crate per package has no
/// such tree to climb; this field lets the caller point at wherever they
/// assembled [`shared_descriptor_root_module`]'s output instead — a
/// separate crate, a re-exported alias, anything.
///
/// The target is a contract, not a free-form value: it must be
/// [`shared_descriptor_root_module`]'s output, `pub` and nameable from
/// every package this and every other codegen run in the tree emits,
/// gated the same way as [`reflect_feature_gate`](Self::reflect_feature_gate)
/// if that's set, built against a semver-compatible `buffa-descriptor`,
/// and — like the plugin path's `shared_descriptor_pool` (see the guide's
/// "spans both plugins" note) — contain every message this `generate()`
/// run reflects on. A root missing a type fails at runtime with a
/// `reflect()` lookup panic, not at `generate()` or `cargo build` time.
///
/// Must be an absolute (`::`-prefixed) or `crate`-relative path of plain
/// `::`-separated identifiers — no generic or parenthesized arguments —
/// since the same string is spliced verbatim at every package depth.
/// [`generate`] rejects a malformed value immediately, before splicing it
/// into generated source. Example: `"::my_shared_fds_crate::__buffa_fds"`.
///
/// `None` (the default) keeps the `super::`-relative behavior.
pub shared_descriptor_pool_root: Option<String>,
/// Gate the reflection impls behind a `reflect` crate feature, *without*
/// gating json/views/text (unlike
/// [`gate_impls_on_crate_features`](Self::gate_impls_on_crate_features),
Expand Down Expand Up @@ -1681,6 +1712,7 @@ impl Default for CodeGenConfig {
generate_reflection: false,
generate_reflection_vtable: false,
shared_descriptor_pool: false,
shared_descriptor_pool_root: None,
gate_reflect_on_crate_feature: false,
idiomatic_enum_aliases: true,
idiomatic_imports: false,
Expand Down Expand Up @@ -2593,6 +2625,23 @@ pub fn generate_with_diagnostics(
));
}

// shared_descriptor_pool_root only overrides how the shared root is
// reached, so — like shared_descriptor_pool without generate_reflection
// above — it is meaningless, and would silently no-op, without
// shared_descriptor_pool itself. Reject it up front rather than
// dropping the override.
if let Some(root) = &config.shared_descriptor_pool_root {
if !config.shared_descriptor_pool {
return Err(CodeGenError::Other(
"shared_descriptor_pool_root requires shared_descriptor_pool to be \
enabled (it overrides the path to the shared root, which only exists \
in shared-pool mode)"
.into(),
));
}
validate_shared_root_path(root)?;
}

// Idiomatic imports place `use` directives in the package-root scope,
// which is only single-writer (collision-free by construction) when the
// whole package is one generated file.
Expand Down Expand Up @@ -2723,11 +2772,16 @@ pub fn generate_with_diagnostics(
// doesn't scale with the package count. In the default mode each package
// embeds this copy; in shared-pool mode the bytes are embedded once by the
// module-tree builder, not per package, so `generate` needs no copy at all.
// In shared mode the tree root gains a `pub mod __buffa_fds`; reserve that
// name against user packages/types the same way `__buffa` is reserved, so a
// In shared mode (without a `shared_descriptor_pool_root` override) the
// tree root gains a `pub mod __buffa_fds`; reserve that name against user
// packages/types the same way `__buffa` is reserved, so a
// `package __buffa_fds;` (or a root-package type named `__buffa_fds`) fails
// with a clear error instead of a duplicate-module collision at the root.
if config.shared_descriptor_pool {
// With an override the reservation is skipped for both absolute and
// crate-relative roots: buffa cannot verify where the override points,
// so a package/type actually named `__buffa_fds` in this tree would go
// undetected rather than erroring here.
if config.shared_descriptor_pool && config.shared_descriptor_pool_root.is_none() {
validate_shared_root_name(file_descriptors, files_to_generate)?;
}

Expand Down Expand Up @@ -2962,6 +3016,61 @@ pub fn shared_descriptor_root_module(
prettyplease::unparse(&file)
}

/// Validate [`CodeGenConfig::shared_descriptor_pool_root`]'s string form: an
/// absolute (`::`-prefixed) or `crate`-relative path of plain `::`-separated
/// identifiers, no generic or parenthesized arguments.
///
/// `syn::Path` alone isn't strict enough here — it happily parses
/// `::k::__buffa_fds<T>`, which would then splice into uncompilable
/// generated code (`pub use ::k::__buffa_fds<T>::FILE_DESCRIPTOR_SET_BYTES;`),
/// exactly the downstream failure this check exists to prevent. A leading
/// `::` or `crate` is required because the same string is spliced verbatim
/// at every package depth in the tree — a plain relative path could only
/// ever be correct from one depth.
fn validate_shared_root_path(path: &str) -> Result<(), CodeGenError> {
let is_absolute = path.starts_with("::");
let is_crate_relative = path == "crate" || path.starts_with("crate::");
if !is_absolute && !is_crate_relative {
return Err(CodeGenError::Other(format!(
"shared_descriptor_pool_root must be an absolute (`::`-prefixed) or \
crate-relative (`crate`-prefixed) path — the same value is spliced \
verbatim at every package depth, so a plain relative path would only \
be correct from one depth: {path:?}"
)));
}
let rest = path.strip_prefix("::").unwrap_or(path);
if rest.is_empty() {
return Err(CodeGenError::Other(format!(
"shared_descriptor_pool_root has no path after the `::` prefix: {path:?}"
)));
}
for (i, segment) in rest.split("::").enumerate() {
let mut chars = segment.chars();
let is_ident = chars
.next()
.is_some_and(|c| c.is_ascii_alphabetic() || c == '_')
&& chars.all(|c| c.is_ascii_alphanumeric() || c == '_');
if !is_ident {
return Err(CodeGenError::Other(format!(
"shared_descriptor_pool_root segment {segment:?} is not a plain \
identifier (no generic or parenthesized arguments, ASCII only) — \
the value is spliced verbatim into generated code: {path:?}"
)));
}
// `_`, `self`, `Self` and `super` are identifiers to the scan above but
// not usable path segments; `crate` is only legal first.
let is_path_keyword =
matches!(segment, "_" | "self" | "Self" | "super") || (i > 0 && segment == "crate");
if is_path_keyword {
return Err(CodeGenError::Other(format!(
"shared_descriptor_pool_root segment {segment:?} is a path keyword \
and cannot be spliced into generated code: {path:?}"
)));
}
}
Ok(())
}

/// Reject user names that would collide with the shared descriptor root module
/// (`reflect::SHARED_ROOT_MOD`) placed at the module-tree root in shared-pool
/// mode. Mirrors how [`SENTINEL_MOD`](context::SENTINEL_MOD)/`__buffa` is
Expand Down Expand Up @@ -3782,7 +3891,10 @@ fn generate_package_mod(
// Shared mode: delegate to the single root `__buffa_fds` module
// instead of embedding this package's own byte copy.
let pool_module = if ctx.config.shared_descriptor_pool {
reflect::reflect_pool_module_shared(current_package)
reflect::reflect_pool_module_shared(
current_package,
ctx.config.shared_descriptor_pool_root.as_deref(),
)
} else {
reflect::reflect_pool_module(fds_bytes)
};
Expand Down
47 changes: 39 additions & 8 deletions buffa-codegen/src/reflect.rs
Original file line number Diff line number Diff line change
Expand Up @@ -358,10 +358,19 @@ fn shared_pool_supers(package: &str) -> usize {
segments + 2
}

/// Build the relative path from a package's `__buffa::reflect` module to the
/// root `__buffa_fds` module, e.g. `super::super::super::super::__buffa_fds`
/// for a two-segment package.
fn shared_root_path(package: &str) -> TokenStream {
/// Build the path from a package's `__buffa::reflect` module to the root
/// `__buffa_fds` module.
///
/// `root_override` (see
/// [`crate::CodeGenConfig::shared_descriptor_pool_root`]), when set, is used
/// verbatim — the root may not be reachable via `super::` at all. Already
/// validated as a plain identifier path before `generate()` reaches here, so
/// this always succeeds. Otherwise builds the default `super::`-relative
/// path, one hop per package segment plus two fixed hops.
fn shared_root_path(package: &str, root_override: Option<&str>) -> TokenStream {
if let Some(root) = root_override {
return crate::idents::rust_path_to_tokens(root);
}
let root = quote::format_ident!("{SHARED_ROOT_MOD}");
let mut path = quote! { #root };
for _ in 0..shared_pool_supers(package) {
Expand All @@ -378,9 +387,13 @@ fn shared_root_path(package: &str) -> TokenStream {
/// keep their names and package-relative paths, so every consumer path that
/// worked against the per-package embedding still resolves — it just aliases
/// the one shared copy. `package` is the proto package this module belongs to,
/// used only to compute the `super::` depth to the root.
pub(crate) fn reflect_pool_module_shared(package: &str) -> TokenStream {
let root = shared_root_path(package);
/// used only to compute the `super::` depth to the root (ignored when
/// `root_override` is set — see [`shared_root_path`]).
pub(crate) fn reflect_pool_module_shared(
package: &str,
root_override: Option<&str>,
) -> TokenStream {
let root = shared_root_path(package, root_override);
quote! {
/// Reflection support: this package's view onto the crate-wide
/// descriptor pool. In shared-pool mode the bytes and pool live once
Expand Down Expand Up @@ -611,7 +624,7 @@ mod tests {

#[test]
fn reflect_pool_module_shared_delegates_without_embedding_bytes() {
let tokens = reflect_pool_module_shared("foo.v1");
let tokens = reflect_pool_module_shared("foo.v1", None);
let parsed = syn::parse2::<syn::ItemMod>(tokens.clone());
assert!(parsed.is_ok(), "generated module must parse: {tokens}");
let rendered = tokens.to_string();
Expand All @@ -636,6 +649,24 @@ mod tests {
assert!(rendered.contains("descriptor_pool"));
}

/// Override path is used verbatim, not as a `super::` chain.
#[test]
fn reflect_pool_module_shared_with_root_override_uses_it_verbatim() {
let tokens =
reflect_pool_module_shared("foo.v1", Some("::my_shared_fds_crate::__buffa_fds"));
let parsed = syn::parse2::<syn::ItemMod>(tokens.clone());
assert!(parsed.is_ok(), "generated module must parse: {tokens}");
let rendered = tokens.to_string();
assert!(
rendered.contains(":: my_shared_fds_crate :: __buffa_fds"),
"root override must be used verbatim, not super::: {rendered}"
);
assert!(
!rendered.contains("super ::"),
"external-crate mode must not climb the (nonexistent) module tree: {rendered}"
);
}

#[test]
fn shared_root_module_inline_uses_a_byte_string_literal() {
let bytes = encode_fds_once(&[FileDescriptorProto {
Expand Down
Loading
Loading