Skip to content

reflect: shared_descriptor_pool_root — external path override for shared-pool mode - #399

Merged
iainmcgin merged 3 commits into
anthropics:mainfrom
th0114nd:th0114nd/shared-fds-cross-crate
Sep 2, 2026
Merged

iainmcgin merged 3 commits into
anthropics:mainfrom
th0114nd:th0114nd/shared-fds-cross-crate

Conversation

@th0114nd

@th0114nd th0114nd commented Aug 30, 2026 •

Copy link
Copy Markdown
Contributor

Closes #397.

What this does

#310's shared_descriptor_pool assumes one crate hosts the whole package tree as nested modules, so a super::-relative path can reach the shared __buffa_fds root. That doesn't hold for a workspace that generates one independent Cargo crate per proto package (each produced by its own separate generate() call, linked by path/registry deps rather than nested modules) — there's no module tree for super:: to climb. See #397 for the concrete case that motivated this (a ~2900-package, one-crate-per-package workspace) and the measured impact (generated source 84GB -> 1.7GB for the full corpus).

Adds CodeGenConfig::shared_descriptor_pool_root: Option<proc_macro2::TokenStream>. When set, generate() splices this value in verbatim as the path to the shared root instead of computing the super:: chain. Buffa doesn't need to know anything about "external crates" or similar — it just stops asserting an opinion about where the root module lives, and the caller (who already assembled shared_descriptor_root_module's output somewhere) supplies the resolved path directly.

None (the default) is byte-for-byte identical to today's super::-relative behavior — this is purely additive, no change to any existing consumer.

Two misuse cases are rejected at generate() time rather than left to silently no-op or fail downstream:

  • Setting shared_descriptor_pool_root without shared_descriptor_pool — the override only means something in shared-pool mode, so (mirroring the existing shared_descriptor_pool requires generate_reflection check) it errors immediately instead of silently doing nothing.
  • A malformed override — since the value is spliced into generated source verbatim, a bad TokenStream would otherwise only surface as a confusing compile error in the downstream generated crate. generate() now parses it as a syn::Path up front and errors immediately if it doesn't parse. This is a single small-token-stream parse per call, no different in cost from the syn::parse_str calls codegen already does per field type path — it doesn't scale with corpus size, so it doesn't need caching across the ~2900 calls in a one-crate-per-package workspace.

Why a TokenStream value instead of a trait

Considered a trait (fn root_path(&self, package: &str) -> TokenStream) so the path could vary per package, but nothing anyone's described actually needs that, and it costs real API surface: CodeGenConfig is currently plain #[derive(Debug, Clone)] data, and a Box<dyn Trait> field breaks that (needs dyn_clone/Arc for Clone, a Debug supertrait, forecloses future dyn-safety-constrained changes). A plain TokenStream costs nothing structurally and is still strictly more general than the crate-name-string version this replaced in earlier iteration — buffa never learns what's on the other end.

Scope

This is the buffa-codegen mechanism only, matching how #310/#311 shipped the mechanism ahead of front-end wiring (#369/#370). I don't think the plugin path (protoc-gen-buffa/protoc-gen-buffa-packaging) can support this at all — it emits one crate's mod.rs from one process invocation, with no notion of a second, separately-assembled crate. buffa-build could plausibly expose this as Config::shared_descriptor_pool_root if there's interest — happy to scope that as a follow-up rather than bundling it here.

Testing

  • reflect_pool_module_shared_with_root_override_uses_it_verbatim: low-level unit test — override path parses, is used verbatim, and the output never falls back to super::.
  • shared_mode_with_root_override_allows_reserved_root_package: end-to-end — additionally asserts the actual generate() output contains the override path verbatim, not just that generation succeeds. The low-level unit test above bypasses generate_package_mod's wiring (ctx.config.shared_descriptor_pool_root.as_ref()), so this closes the gap between "the helper function works in isolation" and "the real call path uses it."
  • shared_mode_with_root_override_requires_shared_descriptor_pool: setting the override without shared_descriptor_pool errors instead of silently no-op'ing.
  • shared_mode_with_malformed_root_override_is_rejected: a non-path TokenStream is rejected at generate() time with a clear error.
  • reflect_pool_module_shared_delegates_without_embedding_bytes and both shared_pool_compile.rs end-to-end compile tests (shared_pool_tree_compiles_inline / _include_bytes_sidecar) — the existing single-crate/super:: path — still pass unmodified, confirming no regression to Deduplicate the embedded reflection descriptor pool across packages #310's original behavior.
  • Full correctness of the downstream cross-crate use case verified separately via hash-diff against a known-good baseline and cargo check --workspace on real generated output (not part of this repo, described in shared_descriptor_pool: support a caller-supplied root path for non-module-tree consumers #397).
  • cargo fmt, cargo clippy --workspace --all-targets -- -D warnings, and cargo doc --all-features all clean; MSRV (1.75, workspace --all-targets) and the 32-bit (i686-unknown-linux-gnu) workspace check both verified locally as well.

@github-actions

Copy link
Copy Markdown

All contributors have signed the CLA ✍️ ✅
Posted by the CLA Assistant Lite bot.

@th0114nd

Copy link
Copy Markdown
Contributor Author

Measured on the real ~2900-package, one-crate-per-package workspace this was built for, comparing the default super::-relative mode against shared_descriptor_pool_root, same buffa commit, same machine, /usr/bin/time for wall/user/sys:

metric non-shared (default) shared (this PR) delta
codegen wall time (2895 crates) 260.8s 157.8s 1.65x faster
codegen user time 3338.9s 2174.1s —
codegen sys time 104.4s 21.4s —
generated source tree size 84 GB 1.7 GB 49x smaller
representative crate's compile time (release, full dep closure, clean build) 257.0s 245.6s ~comparable
representative crate's target/ size (release) 14 GB 2.1 GB 6.7x smaller

("Representative crate" = a real, large service's dependency closure in this workspace, ~40 crates; not named here.)

Note on the compile-time row: an earlier pass of this same measurement showed a 2x regression, which had me worried the shared crate might be acting as a serialization bottleneck for downstream compiles. Re-ran it clean (previous attempt coincided with the machine running low on battery, which corrupted the wall-clock reading — cargo's own internally-reported build duration didn't match /usr/bin/time's wall clock, a clear tell). Confirmed reproducible after that: compile time is a wash, not a regression.

…red-pool mode

buffa#310's shared_descriptor_pool assumes one crate hosts the whole
package tree as nested modules, so a super::-relative path can reach the
shared __buffa_fds root. That doesn't hold for a one-crate-per-package
workspace (idl-gen's layout: ~2900 independent, path-dependent crates,
each generated by its own generate() call) — there is no shared module
tree to walk up.

Add CodeGenConfig::shared_descriptor_pool_root: Option<TokenStream>. When
set, its value is spliced in verbatim as the path to the shared root,
instead of computing the super:: chain. Buffa has no opinion on what's on
the other end — a separate crate, a re-exported alias, anything else — it
only needs the tokens to resolve wherever the caller assembled
shared_descriptor_root_module's output.

Chose a plain TokenStream value over a trait (some `SharedPoolLocation`
with a `fn root_path(&self, package: &str) -> TokenStream`) deliberately:
a trait would need CodeGenConfig to hold `Box<dyn Trait>`, which breaks
its current #[derive(Debug, Clone)] plain-data shape (Clone needs
dyn-clone or Arc, Debug needs a supertrait) for generality nothing today
needs — every known use case (ours included) wants the same fixed path
regardless of package. A TokenStream costs nothing structurally (it
already derives Debug + Clone) and is more general than the crate-name
string this replaced: buffa never learns "crate" is a concept, the caller
can point it at literally anything.

`None` (the default) is byte-for-byte identical to before this change —
verified via shared_pool_compile.rs's existing single-crate/super:: tests,
still passing unmodified, plus a new pair of unit tests for the override
path (parses correctly, uses the override verbatim, never falls back to
super::).

Downstream (idl-gen, not in this repo): assembles the shared root module
once for a whole codegen run via encode_descriptor_set +
shared_descriptor_root_module, writes it into its own generated crate, and
passes that crate's path here for every one of its ~2900 per-package
generate() calls. Measured on a real 2895-crate/2897-package corpus:
generated source 84GB -> 1.7GB, one dependency's-worth of duplicated
FileDescriptorSet bytes instead of one per crate. Full correctness
verified via hash-diff against a known-good baseline (only the expected
per-crate delegation-shape differences) and cargo check --workspace on
the real output.

Not upstreamed yet — idl-gen's one-crate-per-package topology doesn't fit
any of buffa's three existing front-ends (protoc-gen-buffa /
protoc-gen-buffa-packaging can't emit a second crate at all; buffa-build
assumes one crate). Landing this locally first to see the real shape and
size before deciding whether/how to propose it.
@th0114nd
th0114nd force-pushed the th0114nd/shared-fds-cross-crate branch from 1940aec to 852e624 Compare August 31, 2026 14:14
@iainmcgin

Copy link
Copy Markdown
Collaborator

[claude code]

The mechanism is right and None is byte-identical: shared_root_path early-returns the override and otherwise runs the previous body, the override only substitutes the #root prefix in the two delegation items, and validate_shared_root_name keeps its old condition. Codegen-only is also genuinely enough for #397, since buffa_build::Config can't reach the state where it emits its own root and the packages point elsewhere. The blocker is the field's type, and there are a few doc/validation gaps around it.

Option<proc_macro2::TokenStream> makes CodeGenConfig !Send + !Sync. TokenStream carries PhantomData<Rc<()>> on purpose, so every consumer loses the auto traits on CodeGenConfig and, by containment, on buffa_build::Config, whether or not they set the field. That is a silent semver break on a published crate's central config type (a build tool holding a config in a OnceLock, a rayon pool, or across an .await gets Rc<()> cannot be sent between threads pointing at a type it may not depend on). It also breaks the crate's convention for path-valued config (StringRepr::Custom(String), BytesRepr::Custom, MapRepr::Custom, type_attributes are all strings parsed at generate time), requires callers to add version-matched proc-macro2/quote deps to spell one path, and can't thread through a string-valued buffa_build::Config setter or plugin option, which is the announced follow-up. Option<String> parsed with syn::parse_str::<syn::Path> keeps everything you want; idents::rust_path_to_tokens already exists for this and handles keyword segments (google::r#type::…), where the doc's format_ident! suggestion panics.

Validation accepts paths that can't compile. syn::Path parses generic and parenthesised arguments, so ::k::__buffa_fds<T> passes and then splices into pub use ::k::__buffa_fds<T>::FILE_DESCRIPTOR_SET_BYTES;, the downstream error the check exists to prevent. Reject non-PathArguments::None segments, require a leading :: or crate (the one value is spliced at every package depth, so a relative path is wrong for all but one), and splice the parsed Path rather than the raw stream so the validated and emitted values are the same object.

Docs. "Buffa has no opinion on what's there" is the opposite of the contract: the target must be shared_descriptor_root_module's output, pub and nameable from every generated package, gated the same way as reflect_feature_gate if set, built against a semver-compatible buffa-descriptor, and contain every message this generate() run emits (otherwise reflect() panics with the cross-crate lookup message; the guide's two-plugin paragraph already warns about this). State that, plus both rejections, in the field doc; add an "unless shared_descriptor_pool_root is set" clause to shared_descriptor_pool's "wire by hand → leave it false" sentence; and the changelog fragment shouldn't use a rustdoc intra-doc target ([shared-pool mode](CodeGenConfig::shared_descriptor_pool) renders as a dead link in CHANGELOG.md), should say the knob is reachable only by direct buffa-codegen callers this release, and should carry (#399).

Tests. Nothing runs the override across packages of different depth, which is the property that matters; a generate() test over "", "beta", "alpha.v1" asserting each module contains the override and none contains a super-chained root. Also: the empty stream, a crate::gen::__buffa_fds value, the generics case once rejected, and the positive assertion at shared_pool.rs:139 should match the full ::my_shared_fds_crate::__buffa_fds rather than the suffix. Extending tests/shared_pool_compile.rs with a third mode that passes crate::gen::__buffa_fds against the locally emitted root would compile-prove it at every depth without a second crate.

Smaller: drop the "parsing a handful of tokens costs nothing" sentence; the reservation-skip comment at the validate_shared_root_name call should say it rests on the caller's assertion that the root is external.

Re-review on push; this pairs with the #369/#370 shared-pool work already in 0.9.2, so I'd like to land it there.

…d/Sync tests

- shared_descriptor_pool_root becomes Option<String> (was Option<TokenStream>):
  TokenStream carries PhantomData<Rc<()>> by design, so the field made
  CodeGenConfig (and buffa_build::Config by containment) lose Send/Sync for
  every consumer regardless of whether they set it. Parse via the existing
  rust_path_to_tokens (already used ~20 places in this crate for exactly this
  keyword-escaping need) instead of splicing a raw TokenStream.
- validate_shared_root_path rejects generic/parenthesized path segments
  (syn::Path alone accepts them, which would splice into uncompilable
  generated code) and requires a leading :: or crate, since the same value
  is spliced verbatim at every package depth.
- Field doc now states the actual contract (must be pub, nameable from every
  package, gated consistently, contain every message this run reflects on —
  else reflect() panics at runtime, not at generate() or cargo build time),
  matching the equivalent warning already documented for the plugin path's
  shared_descriptor_pool in docs/guide.md.
- New tests: depth-independence across 0/1/2 segment packages, an exact
  (not substring) match on the spliced path, malformed-path rejection
  (generics, empty, relative), a crate-relative acceptance case, a third
  shared_pool_compile.rs real-cargo-compile mode proving the override
  resolves without a second crate, and a CodeGenConfig Send+Sync pin.
- Changelog fragment: drop the rustdoc intra-doc link (renders as a dead
  link in plain CHANGELOG.md), add (anthropics#399), note the knob is reachable only
  by direct buffa-codegen callers this release.
@th0114nd

th0114nd commented Aug 31, 2026 •

Copy link
Copy Markdown
Contributor Author

Thanks @iainmcgin, addressed in 8819e5a. Agree Option<String> is simpler. Full description from the bot:

⏺ All done and pushed. Summary of what changed on th0114nd/shared-fds-cross-crate (commit
8819e5a) in response to Iain's review:

  1. Field type: shared_descriptor_pool_root is now Option<String>, not Option<TokenStream> —
    fixes the Send/Sync break on CodeGenConfig/buffa_build::Config, confirmed by direct
    compile test and now pinned by code_gen_config_is_send_and_sync.
  2. Validation: validate_shared_root_path rejects generic/parenthesized segments and requires
    a leading ::/crate — the earlier syn::Path-based check would have let
    ::k::__buffa_fds<T> through.
  3. Docs: field doc now states the real contract (pub, nameable everywhere, gated
    consistently, must contain every reflected message or reflect() panics at runtime)
    instead of "buffa has no opinion"; shared_descriptor_pool's doc gets the "unless
    overridden" caveat; changelog fragment drops the dead rustdoc-link, adds (reflect: shared_descriptor_pool_root — external path override for shared-pool mode #399), and
    notes it's codegen-only this release.
  4. Tests: depth-independence across 0/1/2-segment packages, an exact (not substring) match,
    malformed-path rejection cases (generics/empty/relative), a crate-relative acceptance
    case, a third real-cargo-compile mode in shared_pool_compile.rs proving the override
    resolves without a second crate (ran it — all 3 modes pass), and the Send/Sync pin worded
    generically per your note (not tied to this one field).

625 unit tests pass (up from 622), all 3 ignored real-compile tests pass, MSRV (1.75),
32-bit, buffa-build's own test suite, and the full local pre-push CI-equivalent all verified
clean before push.

…e depth test's negative assertion live

The validator accepted any identifier-shaped segment, so '::_::x' and
'crate::super::x' passed and spliced into uncompilable 'pub use' items
— the failure class the check exists to prevent. '_', 'self', 'Self',
'super', and a non-leading 'crate' are now rejected by name; '::' alone
gets its own message instead of 'must not be empty', and the segment
error no longer blames generics for a non-ASCII character. The depth
test asserted the absence of 'super ::', a spelling prettyplease never
emits; it now checks for 'super::__buffa_fds'. The rejection table
gains 'super::' and 'self::' spellings and the keyword cases; the
compile-test docs mention the override and the third ignored test.
@iainmcgin

Copy link
Copy Markdown
Collaborator

[claude code]

The Option<String> change and the validation land as asked: CodeGenConfig is Send + Sync again with a pinned test, None is byte-identical, and the field doc carries the whole contract including the runtime-panic mode. Pushed b22c0ff for the small residue: the validator accepted any identifier-shaped segment, so ::_::x and crate::super::x passed and spliced into uncompilable pub use items — _/self/Self/super and a non-leading crate are now rejected by name, :: alone gets its own message, and the segment error no longer blames generics for a non-ASCII character; the depth test's !contains("super ::") could never fire (prettyplease emits super:: unspaced), so it checks super::__buffa_fds now; the rejection table gains super::/self:: spellings and the keyword cases; the compile-test docs mention the override and the third ignored test. One thing left as-is for Iain: shared_pool_compile is not run by CI (only feature_gating_compile is), so the override's compile proof is manual-only — a one-line sibling step in ci.yml would close it, but that is a workflow change. Ready to queue once Iain stamps it.

@iainmcgin
iainmcgin added this pull request to the merge queue Sep 2, 2026
Merged via the queue into anthropics:main with commit 9747e37 Sep 2, 2026
11 checks passed
@github-actions github-actions Bot locked and limited conversation to collaborators Sep 2, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

shared_descriptor_pool: support a caller-supplied root path for non-module-tree consumers

2 participants