Skip to content

Reflection: view-type vtable + dynamic container plumbing#148

Merged
iainmcgin merged 7 commits into
mainfrom
reflect/view-vtable
May 26, 2026
Merged

Reflection: view-type vtable + dynamic container plumbing#148
iainmcgin merged 7 commits into
mainfrom
reflect/view-vtable

Conversation

@iainmcgin
Copy link
Copy Markdown
Collaborator

@iainmcgin iainmcgin commented May 23, 2026

First of three stacked PRs adding vtable-mode reflection — generated types implement ReflectMessage directly, so reflective field access reads struct fields in place with no DynamicMessage round-trip. This PR lands the view-type path and the shared plumbing.

What's here

  • Container reflection traits in buffa-descriptor: ReflectElement (element → ValueRef) and ReflectMapKey (key → MapKeyRef), with generic ReflectList / ReflectMap impls over RepeatedView / MapView. ValueRef::List / Map now carry &dyn ReflectList / &dyn ReflectMap.
  • View vtable codegen: impl ReflectMessage for FooView<'a> (get / has / for_each_set / to_dynamic) plus a memoized per-message MessageIndex, behind an internal mode flag.
  • WKT reflection: a reflect feature on buffa-types so well-known-type views implement ReflectMessage (needed by any message that embeds a WKT).
  • Conformance: a new BUFFA_VIA_VTABLE run that decodes a view, walks its ReflectMessage surface to rebuild a DynamicMessage, and serializes that to JSON.
  • Benchmarks: vtable read cases added to the reflect bench.

Validation

  • The via-vtable conformance suite passes all 1246 binary→JSON cases across proto2/proto3/editions with zero failures (12 CONFORMANCE SUITE PASSED lines total).
  • cargo test --workspace --all-features, clippy -D warnings, and markdownlint all green.

Stack

  1. this PR — view-type vtable + dynamic containers
  2. owned-message vtable + ReflectMode public API + vtable-by-default (Reflection: owned-message vtable, ReflectMode, vtable by default #149)
  3. benchmarks, changelog, design docs, tooling (Reflection: benchmarks, changelog, design docs, and tooling #150)

Owned-type reflection still uses the bridge round-trip after this PR; that flips in PR 2.

iainmcgin added 5 commits May 23, 2026 00:09
Add the runtime half of vtable-mode reflection: ReflectList/ReflectMap
impls for the zero-copy view containers RepeatedView and MapView, so a
generated view's reflective get() can return a borrowed list/map without
materializing a Vec<Value>.

Introduce two helper traits, ReflectElement and ReflectMapKey, with
concrete impls for scalars, &str, &[u8], and EnumValue<E>. A trait-bound
blanket over ReflectMessage is not possible — it would overlap the scalar
impls under Rust's coherence rules — so codegen will emit a one-line
ReflectElement impl per generated view and enum in a later change.

Map reflection deduplicates duplicate wire entries (last-write-wins) via
MapView::iter_unique to match the bridge path's distinct-key semantics,
keeping len() and for_each() consistent across both reflection modes.

Also record the landed ValueRef::List/Map refactor and the coherence
constraint in the vtable design doc.
Generate `impl ReflectMessage for FooView<'a>` directly on view types when
both reflection and the internal vtable flag are set. get()/has() read view
struct fields through a per-field match — no encode/decode round-trip and no
DynamicMessage — covering scalars, string/bytes, enums, nested messages,
repeated, map, optional presence, and oneof members. for_each_set walks the
descriptor; to_dynamic falls back to a bridge-style snapshot.

Also emit `impl ReflectElement` for view types and generated enums so a
RepeatedView/MapView of messages or closed enums reflects through the generic
container impls. Per-message MessageIndex is memoized via an inherent
associated fn (collision-free across sibling views in a shared module).

Gated behind a new internal CodeGenConfig flag, exposed experimentally as
buffa_build::Config::generate_reflection_vtable. Bridge mode and the
reflect() call-site contract are unchanged.
Make the WKT view types (TimestampView, DurationView, AnyView, StructView,
ValueView, wrappers, etc.) implement ReflectMessage, so a message that has a
WKT field can reflect over it. Previously vtable reflection only worked for
protos that did not reference WKTs, because the WKT views lived in buffa-types
with no path to ReflectMessage.

The reflection surface pulls a buffa-descriptor dependency and requires std
(the embedded descriptor pool uses OnceLock), so it is gated behind a new
buffa-types `reflect` feature; views and text stay unconditional. This is
enabled by a targeted codegen flag, gate_reflect_on_crate_feature, which gates
only the reflection impls — unlike gate_impls_on_crate_features, which gates
json/views/text/reflect together and would have forced buffa-types consumers
to opt into views/text.

All seven WKT protos share the google.protobuf package, so one embedded
descriptor pool backs every WKT view's reflection.
Add a sixth conformance run that exercises the generated
`impl ReflectMessage for FooView`: decode a view, walk its vtable reflection
surface (for_each_set/get) to rebuild a DynamicMessage, and serialize that to
JSON. This reuses DynamicMessage's JSON serializer — which passes the corpus
cleanly under BUFFA_VIA_REFLECT — so any failure isolates a bug in the vtable
get/has surface rather than in JSON formatting. Binary and text output are
skipped (the reflect rebuild drops unknown fields, which JSON omits anyway).

The run passes 1246 binary->JSON conformance tests with zero failures across
proto2/proto3/editions, so known_failures_view_vtable.txt is empty.

Vtable reflection is enabled on the four view-bearing conformance protos and
gated behind the conformance `reflect` feature (via the new
buffa_build::Config::gate_reflect_on_crate_feature), so the no_std binary omits
it. Also fix a latent needless-return in process_editions surfaced by clippy
once the editions protos are present, and correct the stale conformance-run
count in CONTRIBUTING (four runs -> six, including the previously undocumented
via-reflect run).
Extend the reflection benchmark with the zero-copy view path so the vtable
ReflectMessage work can be measured against the alternatives:

- decode/view — decode_view alone (no reflection), the zero-copy floor.
- reflect/{vtable,bridge,dynamic}_read_{one,all} — from wire bytes, obtain a
  reflective handle and read one field / all set fields, comparing the view
  vtable path, the bridge round-trip, and pure DynamicMessage reflection.

Enables generate_reflection_vtable on the bench types so the view types
implement ReflectMessage. Measurements: vtable view reflection runs ~6-10x
faster than the bridge round-trip and ~4x faster than pure DynamicMessage
reflection, because it is dominated by decode_view, which is itself cheaper
than owned decode (borrowed strings/bytes, no per-field allocation).
@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 23, 2026

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

asacamano
asacamano previously approved these changes May 24, 2026
Resolves conflicts from the idiomatic enum aliases (#152) and module
collision fix (#145) landing on main while the vtable reflection stack
was open:

- enumeration.rs: keep both the vtable `ReflectElement` impl for closed
  enums and the idiomatic-alias doc-note wrapping of `enum_doc`.
- codegen lib.rs: keep all three new config fields
  (generate_reflection_vtable, gate_reflect_on_crate_feature,
  idiomatic_enum_aliases); move the vtable-config validation into the new
  generate_with_diagnostics so both entry points enforce it.
- buffa-build: keep both new builder methods.
- buffa-test/build.rs: keep the new modcollide/modrace build blocks and
  the vtable-aware proto2 comment.

Generated WKT/bootstrap code regenerated; matches the textual merge.
@iainmcgin iainmcgin merged commit c45a2ff into main May 26, 2026
7 checks passed
@iainmcgin iainmcgin deleted the reflect/view-vtable branch May 26, 2026 04:04
@github-actions github-actions Bot locked and limited conversation to collaborators May 26, 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.

2 participants