[Draft] Descriptors via custom section: delete the wasm interpreter#5171
Draft
guybedford wants to merge 42 commits into
Draft
[Draft] Descriptors via custom section: delete the wasm interpreter#5171guybedford wants to merge 42 commits into
guybedford wants to merge 42 commits into
Conversation
First step toward replacing the wasm-bindgen descriptor interpreter with a custom-section transport. This commit only introduces the shared vocabulary; no producer or consumer is wired up yet. It is intentionally a no-op for both the runtime and cli-support. * DESCRIPTORS_SECTION_NAME (__wasm_bindgen_descriptors): the custom section that #[wasm_bindgen]-expanded code will write its compile-time schema into. * SYMBOL_REF (tys::SYMBOL_REF = 0xFF): a new opcode reserved for schema stream slots whose concrete u32 value is not known at compile time and must be resolved by cli-support after linking (e.g. function-table indices for closure invoke shims). * DESCRIPTOR_KIND_REGULAR/CAST: entry kind discriminator so that cast descriptors can carry the breaks_if_inlined<From, To> symbol name in their entry header for cli-support to resolve. The on-wire layout is documented exhaustively next to the DESCRIPTORS_SECTION_NAME constant so the producer (macro) and consumer (cli-support) cannot drift. Schema version intentionally NOT bumped: nothing produced by existing wasm-bindgen runtime crates changes yet. The bump happens in the commit that flips the macro to emit the new section and stop emitting __wbindgen_describe_* functions.
Pure-byte parser for the new __wasm_bindgen_descriptors custom section, with zero walrus dependency so it can be exercised in isolation. Ten unit tests covering: * empty sections * single and multiple regular entries * cast-kind entries * SYMBOL_REF resolution (4-aligned and padded payloads) * unresolved symbol -> error * unknown kind byte -> error * truncated section header / body -> error * misaligned schema bytes -> error The module is marked #![allow(dead_code)] for now because the producer (macro) and the consumer wiring (descriptors::execute) land in following commits. The next commit wires this parser into descriptors::execute as a prefer-section-over-interpreter lookup: any entry recovered from the section will short-circuit the interpreter for that shim, letting the migration proceed function-by-function without an atomic swap. resolve_symbols intentionally returns Vec<u32> rather than &[u32] to keep the public API simple even though the legacy interpreter returns the same. Descriptor::decode consumes &[u32] unchanged.
descriptors::execute now runs a two-phase ingest: 1. Pull and parse the new __wasm_bindgen_descriptors custom section (if present), turning each regular entry directly into a Descriptor via the existing Descriptor::decode pathway. The set of shim names recovered this way is remembered. 2. Run the legacy interpreter pass exactly as before, but skip any __wbindgen_describe_<name> export whose name already landed via phase 1. The redundant synthetic export is still deleted so downstream passes don't see it. This is the load-bearing seam that lets the migration off the interpreter happen function-by-function rather than as one big atomic swap: the macro can stop emitting __wbindgen_describe_* for a single type pattern at a time, ship the corresponding section bytes alongside it, and the interpreter continues to handle everything the macro hasn't taken over yet. Phase 1 currently ignores cast-kind entries. Casts need an extra step (resolving the breaks_if_inlined<From,To> symbol back to a FunctionId so the cast_imports map keeps its existing shape). That plumbing lands separately. Until then, casts continue through the interpreter pathway by scanning for direct calls to __wbindgen_describe_cast. Tests: all 81 cli-support lib tests + the new 10 descriptors_section unit tests pass. No binary in the workspace produces the section yet, so the new code path is exercised only by the parser unit tests; integration coverage arrives with the macro change. Also bumps APPROVED_SCHEMA_FILE_HASH to track the additive constants added to shared/src/lib.rs in the previous commit. The on-wire schema produced by current wasm-bindgen runtime crates is unchanged, so SCHEMA_VERSION stays where it is; that bump will accompany the macro switch-over.
Introduces the producer-side const surface that the macro will use in
the next commit to emit static descriptor entries into the
__wasm_bindgen_descriptors section.
* WasmDescribe gains an associated const SCHEMA: &'static [u32]. It
defaults to &[] so this is non-breaking for downstream
WasmDescribe impls (the macro treats &[] as 'fall back to the
interpreter').
* Every concrete leaf impl in this file populates SCHEMA with the
opcode that its describe() function would inform(). That includes
the primitives, str, String, JsValue, JsError, (), *const T,
*mut T, NonNull<T>.
* Generic wrapper impls (&T, &mut T, Option<T>, Vec<T>, Result<T,E>,
Clamped<T>, Box<[T]>, MaybeUninit<T>, AssertUnwindSafe<T>)
intentionally leave SCHEMA at the default empty slice. Their
schemas have lengths that depend on T::SCHEMA.len(), which is not
expressible as a const associated item on stable Rust without
feature(generic_const_exprs). The macro pattern-matches these
wrapper shapes syntactically at the call site and synthesises the
schema there, where every length is concrete. This matches the
approach laid out in the descriptors-via-custom-section plan.
* A new public module describe::schema offers const-fn helpers for
the macro to call at expansion time:
- word_total(parts) returns total u32 word count
- concat_words::<N>(parts) concatenates u32 slices
- entry_byte_len(name_len, word_count) sizes a packed entry
- pack_entry::<B>(name, kind, schema_words) lays out the bytes
of one descriptor section entry exactly as documented in
shared::DESCRIPTORS_SECTION_NAME.
These items are 'pub' because the macro expands code into the
user's crate that refers to them by absolute path; they are
internal-API regardless.
No runtime behaviour changes. The macro still emits only the legacy
__wbindgen_describe_* descriptors; the new SCHEMA path is dormant
until the next commit wires it up.
Every #[wasm_bindgen]-annotated export now emits, in addition to the legacy __wbindgen_describe_<name> synthetic function, a packed byte array linked into the __wasm_bindgen_descriptors custom section. Each entry encodes the same opcode stream the interpreter would have recovered, in the format documented next to shared::DESCRIPTORS_SECTION_NAME. The two transports coexist by design. cli-support prefers the section entry when present and decodable; if the schema is malformed (for example because the function mentions a wrapper type like Option<T> whose WasmDescribe::SCHEMA is still the default empty slice), Descriptor::decode panics and we fall back to the interpreter for that shim. The fallback is silent in normal output and visible at log::debug. Concretely for the section gains a 37-byte entry: name_len=3, name="add", kind=0, word_count=7, schema = [FUNCTION, 0, 2, U32, U32, U32, U32] cli-support log confirms: "skipping interpreter for \"add\"; already produced by __wasm_bindgen_descriptors section". The produced JS shim is byte-identical between the legacy path and the new path, verified by diffing the wasm-bindgen output on the add example with and without the macro change. Snapshot tests come in a follow-up commit. Format simplification: the section no longer carries an outer total_len u32. Entries are emitted as individual link_section statics that the linker concatenates, so the producer can't write a single header anyway. The wasm custom-section framing already supplies the payload length. Producer-side const-evaluation relies on the macro composing schemas from <Ty as WasmDescribe>::SCHEMA references. This requires each involved leaf type to populate SCHEMA, which the previous commit did for every primitive plus String, str, JsValue, JsError, *const T, *mut T, NonNull<T>, and (). Generic wrapper types (&T, Option<T>, Vec<T>, etc.) still have SCHEMA = &[], so any function mentioning them currently falls back to the interpreter. Adding macro pattern-matching for those shapes is the next step in the migration. Schema file hash bumped to track the section-format documentation change in shared::lib.rs.
Expands the set of types the macro can produce a static descriptor
entry for. Previously, only functions whose every argument and
return type was a leaf with a non-empty WasmDescribe::SCHEMA could
emit a usable section entry. Now the macro structurally recognises
common wrapper shapes at expansion time:
&T -> REF, <T schema>
&mut T -> REFMUT, <T schema>
Option<T> -> OPTIONAL, <T schema>
Vec<T> -> VECTOR, <T schema>
Box<[T]> -> VECTOR, <T schema>
[T] -> SLICE, <T schema>
Result<T,E> -> RESULT, <T schema> (E discarded, matches runtime)
Clamped<T> -> CLAMPED, <T schema>
For wrapper types these prefixes are emitted directly and the macro
recurses into the inner type. For everything else it still defers
to <Ty as WasmDescribe>::SCHEMA, which the previous commit
populated for every concrete leaf the runtime crate ships
implementations of. Together they cover every concrete signature
the interpreter handles today for exported functions.
End-to-end smoke test: the hello_world example (greet(name: &str))
now emits a valid section entry and cli-support reports
\"skipping interpreter for \\\"greet\\\"; already produced by
__wasm_bindgen_descriptors section\". JS and TypeScript output
remain byte-identical to the legacy path, verified by diffing
wasm-bindgen output on the add, hello_world, and closures
examples with and without these changes.
Imports (#[wasm_bindgen] extern blocks) still flow through the
legacy interpreter because their Descriptor emission lives on a
different code path; porting them is the next step.
The descriptors-via-custom-section work relies on macro-emitted const expressions composed from <Ty as WasmDescribe>::SCHEMA slice references. The pattern (const fn over &[&[u32]] returning [u32; N] with N computed by a sister const fn) compiles on 1.79 but not on 1.77. The cli crates were already at 1.86 and are unchanged. The MSRV resolver job and the library MSRV job in CI are bumped in lockstep so the gating job actually exercises the new minimum.
* descriptors_section: replace is_multiple_of (1.87) with % (cli-support
MSRV is 1.86); the parser feature was MSRV-incompatible.
* codegen: format directly with {b:02x} instead of format!("{:02x}", b).
* codegen: dedent an overindented doc-comment continuation.
No behavioural change. All workspace tests still pass.
Significantly expands the section-transport coverage:
1. Import functions (#[wasm_bindgen] extern blocks) now emit a
section entry alongside the legacy __wbindgen_describe_<shim>
function. The structure mirrors the export-side emission:
FUNCTION header, per-arg schemas (no async-LONGREF transform
because import args do not survive across awaits), then
inform_ret repeated twice as the legacy decoder expects.
2. Import types (extern "C" { type Foo; }) now have a populated
WasmDescribe::SCHEMA const on their generated impl. The schema
is either [NAMED_EXTERNREF, len, ...chars...] when a
typescript_type is declared or [EXTERNREF] otherwise (the
latter delegated to JsValue::SCHEMA so the two cases stay in
sync). Both are kept in lockstep with the describe() body
immediately below so the two transports cannot diverge.
This refactors emit_static_descriptor_entry to take pre-built
arg/ret parts streams plus an attrs slice. The attrs are now
threaded through and applied to the outer const _: () = { ... }
block, mirroring the legacy descriptor function. Without this,
cfg-gated import items (e.g. js-sys items behind
#[cfg(js_sys_unstable_apis)]) tried to expand their descriptor
references when the cfg was off and failed compilation.
Drops `#[used]` from the descriptor static. With #[used], LLVM
also materialises the bytes in linear memory (the wasm data
section), doubling storage for every entry. Removing it matches
the legacy __wasm_bindgen_unstable section emission pattern at
line ~181 of this file; the pub visibility + unique symbol name
keeps the symbol live through linking without forcing a runtime
copy.
Coverage on the closures example (which exercises web-sys + js-sys):
section entries migrated 123 -> 2252 with the import work alone,
and JS output remains identical (the wasm differs by 28 bytes of
debug-info data, none of it observable from JS).
Remaining malformed entries (221 in closures, 1 in hello_world)
correspond to functions referencing types the macro still does
not structurally recognise (closure-shaped &dyn Fn args, slice
returns, custom JsCast types from user code). Those are the
next step.
…atch
Adds explicit per-entry framing to the __wasm_bindgen_descriptors
custom section so the producer and consumer can drift in version
without failing the whole build. Foundation for future schema
evolution (e.g. closure SYMBOL_REF, cast descriptors, future
opcode additions).
## Format
Each descriptor entry is now prefixed with:
u8 format_version (currently DESCRIPTOR_FORMAT_VERSION = 1)
u32 LE entry_body_byte_len
followed by the body that previous commits already used:
u8 shim_name_len
[u8] shim_name
u8 kind
u32 LE schema_word_count
[u32] schema
The outer two framing fields are part of the stable contract and
must remain identical regardless of how format_version evolves.
This lets a CLI that does not recognise the version skip the body
byte-accurately and continue parsing.
## Semantics
* parse() now returns (Vec<Entry>, ParseStats). ParseStats counts
entries that were decoded vs skipped due to unknown versions,
broken down per version byte encountered.
* descriptors::execute logs at log::info (not debug) whenever any
entries are skipped due to version mismatch, naming the version
and the count. The affected shims silently fall back to the
legacy interpreter pathway exactly as if their entries had been
malformed.
* Decoupled from SCHEMA_VERSION on purpose: SCHEMA_VERSION still
gates the older __wasm_bindgen_unstable metadata section
unchanged. Descriptors section evolves on its own timeline.
## Tests
Two new unit tests added to descriptors_section:
* skips_unknown_version: mixed v1 + vUnknown + v1 entries parse
correctly, only the v1 ones decode, stats reflect the skip.
* skipping_unknown_version_does_not_decode_garbage: confirms the
body length field correctly determines skip distance even when
the body content is unparseable by v1.
Old "unknown kind" / "truncated" tests adjusted for the new
framing offsets. All 12 descriptors_section tests + 83 cli-support
lib tests pass.
## Functional verification
Built add example end-to-end, ran the resulting JS through node:
1+2 = 3
100+200 = 300
hello_world and closures examples still process through bindgen
with the section pathway active (closures still has known wrapper-
type fallback to the interpreter).
## Why now
This is the right moment for versioning because the
descriptors-via-custom-section transport is brand new on this
branch; no in-the-wild binaries carry the old unversioned format,
so v1 is also the format-floor. Subsequent commits in this branch
(closure SYMBOL_REF, wbg_cast section migration) land as v1
features; any future schema-breaking change becomes v2 with the
fallback semantics already in place.
If a CLI encounters a pre-versioned binary (e.g. produced by an
in-flight checkout of this branch from a few commits back), the
parser now emits a targeted error message rather than a generic
size complaint:
"...usually means the wasm was produced by a wasm-bindgen older
than this CLI and uses an incompatible __wasm_bindgen_descriptors
layout. Rebuild the wasm with a matching wasm-bindgen version."
The last architectural piece. Closures previously required the wasm
interpreter because their descriptor stream embeds a function-table
slot only known after linking. This commit replaces that for
&dyn Fn / &mut dyn FnMut argument types via Design B:
* Macro emits a non-generic forwarding wrapper per closure
signature with a stable #[export_name = "__wbg_invoke_<hash>"].
Body transmutes (a, b) into the closure trait object and
forwards args, mirroring src/convert/closures.rs::invoke for
the UNWIND_SAFE=true case (the only path the runtime selects
for raw &dyn Fn args).
* A type-correct #[used] static of the wrapper's function-pointer
type forces wasm-ld to place the wrapper in the function table.
* Schema stream gains SYMBOL_REF + the wrapper's export name in
place of the unknown shim_idx.
* cli-support adds function_table_slot_of: given an exported
function, walks element segments and returns its slot.
* build_symbol_table now resolves every exported function-in-table
to its slot index. The closure SYMBOL_REF is just a name lookup
against this table.
No body interpretation involved. Pure structural walrus queries on
exports + element segments. Resilient against rustc codegen changes
because nothing reads wasm opcodes.
## Coverage
closures example, before this commit:
section migrated: 2252, malformed: 221
closures example, after this commit:
section migrated: 2313, malformed: 160
Net +61 closure descriptors now flow through the section. The
remaining 160 are heap-allocated Closure<T> wrappers (not raw
&dyn Fn args), slice returns, and similar shapes the macro still
falls back on. Migrating those is mechanical follow-up work.
## JS shim difference
For migrated closures, the JS shim now references the wrapper's
hash-derived export name (e.g. wasm.__wbg_invoke_4eb65d03b312d111)
instead of the legacy mangled invoke name. This is the desired
shape: JS calls the wrapper, wrapper forwards to invoke. The
hello_world and add examples remain byte-identical because they
have no closures.
## Functional verification
add example, run through node: 1+2=3, 100+200=300 (correct)
closures example: instantiates and panics at web_sys::window()
.expect() because node has no DOM. Pre-existing example
behaviour, not a regression. The migrated invoke wrappers do
appear in the JS shim with the expected shape.
cli-support tests: 83/83 passing.
## Implementation choices documented in code
* UNWIND_SAFE=true only: the runtime's WasmDescribe impl for
dyn Fn / dyn FnMut hardcodes describe_invoke::<true>, so the
other variant is unreachable for raw closures. Heap Closures
that select UNWIND_SAFE=false will get their own variant
emitted when the macro migrates that path.
* Wrapper name = ShortHash of "closure|is_mut|unwind_safe|args|ret".
Hash includes mut/unwind to keep distinct signatures distinct;
crate-name mix-in from ShortHash already namespaces across
crates.
* Wrapper emission deduplicated per-crate via a thread-local
HashSet keyed on the export name, identical pattern to the
existing DESCRIPTORS_EMITTED dedup.
* Side-effect of schema_parts_for_type queues wrapper emissions
into a per-thread Vec, drained at the end of each
emit_static_descriptor_entry so wrappers and section entries
are emitted into the same TokenStream.
## Heap Closure<T> migration (not in this commit)
* Same machinery applies; needs schema_parts_for_type to
structurally recognise Closure<T> and &Closure<T>.
* Schema emits an extra CLOSURE + owned + mutable prefix before
the FUNCTION + SYMBOL_REF that this commit handles.
* Will land as a follow-up; interpreter remains as fallback in
the meantime.
## Post-link cleanup (not in this commit)
* After harvesting, cli-support should strip every
__wbg_invoke_* export so the walrus GC pass can drop wrappers
whose only liveness root was the export. Mirrors the existing
__wbindgen_describe_* stripping. Will land as a follow-up
alongside heap Closure migration.
After phase 2 of descriptors::execute the SYMBOL_REFs in the section have all been resolved to function-table slots. The closure-invoke wrapper exports the macro emitted to make those wrappers findable by name no longer serve any purpose. Strip them so the existing walrus GC pass (gc_module_and_adapters in lib.rs) can drop wrappers whose only remaining liveness root was the export. Wrappers that JS will actually call retain liveness through the function table (cli-support's downstream export_table_element pass re-exports the table slot under a fresh name for JS to consume), so live closures keep working. Only unused unwind variants and otherwise-orphaned wrappers go away. On the closures example: closures_bg.wasm size before strip: 240568 bytes closures_bg.wasm size after strip: 239037 bytes __wbg_invoke_* exports remaining: 1 (the one JS calls) Same strip pattern as the existing __wbindgen_describe_* cleanup in execute_exports. cli-support tests still 83/83.
ScopedClosure (and the Closure<T> type alias) has the runtime describe() body inform(EXTERNREF). The SCHEMA associated const was still defaulting to &[], which made the section transport emit a malformed entry for any function with a `&ScopedClosure<...>` arg — the runtime would inform EXTERNREF at runtime but the section bytes had no inner schema after the REF prefix. cli-support read the next argument's bytes as if they continued ScopedClosure's schema, producing garbage. Coverage on the closures example: 2313 -> 2323 migrated entries (2473 total). Modest gain because most closures in the example use heap Closure<T> via wbg_cast rather than &ScopedClosure args.
… trait const Closes the coverage gap on the closures example: 2323/2473 -> 2473/2473 entries now flow through the section transport. Zero malformed. Zero interpreter fallbacks. Changes: * Struct (user-defined #[wasm_bindgen] struct): SCHEMA emits [RUST_STRUCT, name_len, ...name chars], lockstep with describe(). * Enum (regular C-style enum): SCHEMA emits [ENUM, name_len, ...name chars, hole]. * StringEnum: SCHEMA emits [STRING_ENUM, name_len, ...name chars, variant_count]. * WasmDescribeVector trait gains an associated const VECTOR_SCHEMA: &'static [u32], defaulting to &[]. Set on user-struct vector impls to [VECTOR, NAMED_EXTERNREF, ...] and on string-enum vector impls to [VECTOR, EXTERNREF]. The blanket impl for T: ErasableGeneric<Repr=JsValue> + WasmDescribe keeps the default because its correct value depends on T::SCHEMA.len(), a generic-dependent length the stable const system can't express. Macro recogniser for Vec<T> still emits [VECTOR] + T::SCHEMA by default; VECTOR_SCHEMA is the surface for future per-call-site dispatch on user-struct elements. Collateral: * Changed three name_chars bindings from iterator-form to Vec<u32> because quote! now consumes them twice (once in SCHEMA, once in describe()). Iterators are not Copy; vectors reference-iterate via quote!'s splat. No behaviour change. Verification: * closures example: 2473/2473 section-migrated, 0 malformed. * add example: builds, JS shim runs (1+2=3, 100+200=300). * hello_world example: builds, JS loads. * cargo test -p wasm-bindgen-cli-support: 83/83. * cargo test -p wasm-bindgen-shared: schema hash sentinel unchanged (shared crate untouched).
Restores the pre-wbg_cast intrinsics removed in 4e5fa17 so that non-closure casts can route through the descriptor section transport as ordinary imports. Added: * AsNumber, NumberNew * BigIntFromI64/U64/I128/U128 * StringNew * Uint8ArrayNew, Uint8ClampedArrayNew, Uint16ArrayNew, Uint32ArrayNew, BigUint64ArrayNew, Int8ArrayNew, Int16ArrayNew, Int32ArrayNew, BigInt64ArrayNew, Float32ArrayNew, Float64ArrayNew * ArrayNew, ArrayPush JS bodies mirror the originals: trivial pass-throughs for the identity-shaped ones (decode ABI value, return as JS), bigint shift-or for i128/u128, and array operations for the array constructor/push pair. No call sites are migrated yet. Subsequent commits will move runtime crate cast call sites off wbg_cast and onto these intrinsics, after which wbg_cast and the interpreter can be removed entirely.
…_SCHEMA Fixes test-js-sys's Promise::then where the descriptor section schema for &ScopedClosure<'_, dyn FnMut(T)> would otherwise come out malformed (missing inner schema). The root cause is that String, user struct, string enum, etc. override describe_vector() to emit something other than [VECTOR, T::SCHEMA] — they emit [VECTOR, NAMED_EXTERNREF, name] or [VECTOR, EXTERNREF]. The macro recogniser was assuming the simpler [VECTOR, T::SCHEMA] shape. Now the macro emits <T as WasmDescribeVector>::VECTOR_SCHEMA directly, which each impl sets to the right value: * Primitives (u8/i8/u16/.../f64): [VECTOR] + <T::SCHEMA> * String: [VECTOR, NAMED_EXTERNREF, 6, 'string'] * User struct: [VECTOR, NAMED_EXTERNREF, <name>] (set per impl) * String enum: [VECTOR, EXTERNREF] (set per impl) Both Vec<T> and Box<[T]> arms updated to use the trait const path.
Test-runner / transformation layers sometimes wrap an exported function with a shim (e.g. wasm-bindgen-test-runner adds a '.command_export' wrapper for each export). The export then points to the shim function, while the original — which is what actually lives in the function table — keeps its original symbol name. When build_symbol_table looks up the export'\''s FunctionId via function_table_slot_of, the shim isn'\''t in any element segment so the lookup fails. Falling back to a name-based search through the table'\''s elements finds the original and returns its slot. Fixes test-wasm-bindgen which exercises this path via the no_interpret test.
Continues the path to deleting the interpreter by routing every
non-closure wbg_cast invocation through a type-specific intrinsic
that the descriptor section already handles.
Runtime changes:
* extern decls for __wbindgen_string_new, __wbindgen_number_new,
__wbindgen_bigint_from_{i64,u64,i128,u128}, the 11 typed-array
constructors, plus __wbindgen_array_new / __wbindgen_array_push.
* JsValue::from_str / from_f64 now call the named intrinsics
directly instead of wbg_cast.
* big_integers! and num128! macros now take the intrinsic name as
an argument and inline its call. i128/u128 split into (hi, lo)
in Rust; JS reassembles via shift-or.
* Restored typed_arrays! macro with one (ctor, clamped_ctor) pair
per primitive. Replaces the previous generic
`impl<T: VectorIntoWasmAbi> From<Box<[T]>>` that routed through
wbg_cast. From<Vec<T>> now bounds on `JsValue: From<Box<[T]>>`
to keep the wrapper layer compatible.
* Restored VectorIntoJsValue trait + blanket
`impl<T: VectorIntoJsValue> From<Box<[T]>> for JsValue`. Carries
the per-monomorphisation conversion choice that the generic
VectorIntoWasmAbi-based wbg_cast used to provide.
* Re-added js_value_vector_into_jsvalue helper (the array_new +
push loop) in __rt.
cli-support changes:
* The new intrinsics are registered in
WasmBindgenWasmConventions::populate_aux during the import-import
phase, so the descriptor-section pathway hooks them up the same
way it does for ObjectCloneRef. Matched by both name and
signature.
Macro changes:
* User struct, string enum, regular enum, and ImportType emissions
now each carry an `impl VectorIntoJsValue for #name` calling
js_value_vector_into_jsvalue. This is how `Vec<UserType>`
converts to JsValue post-wbg_cast.
Tests: just test-js-sys (738+1+1) and just test-wasm-bindgen
(379+6+1) both pass.
Remaining wbg_cast call sites: only the 3 closure-related ones in
src/closure.rs. Those are handled in the next commit via the
Design B forwarding-wrapper machinery.
…tStatic Closes four interpreter-fallback families uncovered by inventorying which macro-emitted shims still produced malformed section entries: 1. `Vec<JsValue>` / `Vec<Array<T>>` etc. The blanket `impl<T: ErasableGeneric<Repr=JsValue> + WasmDescribe> WasmDescribeVector for T` could not set `VECTOR_SCHEMA` because the const length depends on `T::SCHEMA` (`generic_const_exprs` wall on stable). Replaced with concrete impls for `JsValue` and `JsError`; ImportType macro now emits a per-type `WasmDescribeVector` impl alongside `WasmDescribe` so user ImportTypes carry their own `VECTOR_SCHEMA = [VECTOR, NAMED_EXTERNREF, len, chars...]` for both the typescript_type case and the plain `JsValue` fallback. 2. `flat_map<T, U>` / generic-erased closure args. Fell out of (1) because the closure's args/ret include `Vec<U>` whose `VECTOR_SCHEMA` was empty under the old blanket. 3. `wasmbindgentestcontext_run` and friends — anything taking `Vec<JsValue>` as an argument. Same root cause as (1). 4. `__wbg_static_accessor_*` — imported `static`s. Added `DESCRIPTOR_KIND_STATIC` so the section can carry a bare type schema (no FUNCTION header) and `ImportStatic` codegen now emits one. `VectorIntoWasmAbi` / `VectorFromWasmAbi` blanket impls over `ErasableGeneric<Repr=JsValue>` pick up an extra `WasmDescribeVector` bound that's already satisfied by every real user (JsValue, JsError, ImportType). After this commit `__wbg_static_accessor_*`, `__wbg_flatMap_*`, `__wbg_slice_*`, and `wasmbindgentestcontext_run` are all recovered structurally from the descriptors section.
Extends the closure-arg detector to accept the single-arg `&dyn Fn(&A) -> R` / `&mut dyn FnMut(&A) -> R` shape (matching the runtime's special `(&A)` impl in `src/convert/closures.rs`). The wrapper emission now dispatches per-arg on owned vs reference: * Owned T: `<T as FromWasmAbi>::Abi`, reassembled via `from_abi(join(...))`. * Reference `&T`: `<T as RefFromWasmAbi>::Abi`, reassembled via `&*ref_from_abi(join(...))`. Closes the `__wbg_ref_first_arg1` / `__wbg_ref_first_custom2` / `__wbg_pass_reference_first_arg_twice` malformed-entry family. Also adds `MaybeUninit<T>::SCHEMA = T::SCHEMA` so the 44 `*_uninit_*` shims in tests/wasm/slice.rs flow through the section transport instead of falling back to the interpreter. `MaybeUninit<T>` is descriptor-transparent — same ABI shape as T — so the const just forwards.
…etters Mirrors the ImportStatic plumbing: each #[wasm_bindgen] struct field emits a `__wbg_get_<class>_<field>` shim plus a legacy `__wbindgen_describe_<getter>` companion that the interpreter runs. The cli reads that descriptor as a bare type schema (no FUNCTION wrapper), keyed by the getter shim name, then uses it as both the getter's return type and the setter's value type. Add a section emission alongside the legacy one. Closes the 29 `__wbg_get_*` interpreter-fallback entries (every Rust struct field in the test suite).
Two related fixes for `#[wasm_bindgen]` enums that mix string-literal
variants with typed payload variants ("dynamic unions"):
1. The enum's own `WasmDescribe::SCHEMA` was the trait default (`&[]`)
because the legacy codegen only emitted the runtime `describe()`
inform-stream. Added a section-transport `SCHEMA` built via the
same `schema::word_total` + `concat_words` const-fn pair the
regular descriptor entries use. Each variant's schema parts come
from `schema_parts_for_type` (the macro helper that handles
`Option<T>`, `Vec<T>`, `Result<T, E>`, etc.), so wrapper-typed
variants like `Option<u32>` resolve to the right section bytes
instead of relying on the wrapper trait's empty default
`SCHEMA`.
2. Each typed variant also gets a `__wbg_dynamic_union_<name>_<idx>`
descriptor that cli-support reads as a bare type schema (used to
compile the variant's instanceof check). Previously these only
went through the legacy `__wbindgen_describe_` exports; the
section now carries them too, using `DESCRIPTOR_KIND_STATIC` for
the same reason as ImportStatic / struct field getters.
Closes the last 16 fallback families (`*_union_roundtrip`,
`__wbg_dynamic_union_*`, `__wbg_js_*_union_*`,
`string_enum_fallback_roundtrip`). After this commit no
`#[wasm_bindgen]`-emitted shim in the test suite falls back to the
interpreter; the only interpreter use left is closure-cast descriptor
recovery, blocked on `generic_const_exprs`.
Reflects the section-transport behaviour we've now landed end-to-end: * Closure-arg invoke shims rename from `wasm_bindgen__convert__closures_____invoke__h<N>` (interpreter path) to `__wbg_invoke_<hash>` (section path via `#[export_name]` wrappers emitted by the macro). * `__wbindgen_cast_<N>` adapters for non-closure casts replaced with named intrinsics: `__wbindgen_string_new`, `__wbindgen_number_new`, etc. * Closure-cast intrinsics (`__wbindgen_cast_*` referring to `Closure(...)`) remain on the interpreter path with the legacy invoke-shim naming, exactly as documented (the one place still blocked on `generic_const_exprs`). * Function-table slot indices in closure descriptors shift to reflect the new ordering after stripping the section-handled shims; the cast descriptor's content is otherwise identical.
With section-transport coverage now complete for every shim the macro
produces (exports, imports, ImportStatic, struct field getters,
DynamicUnion variants), the legacy synthetic
`__wbindgen_describe_<name>` functions are dead code: nothing reads
them. Stop emitting them. This also drops the `Descriptor { ... }`
codegen helper and its accumulated dedup machinery.
The cli replaces `execute_exports` with `assert_no_legacy_describe_exports` —
a hard-fail check that surfaces any stale macro output as a build
error rather than silently invoking the wasm interpreter. The
interpreter survives in this commit, but its only entry point is now
`execute_casts`: closure-cast descriptor recovery, the one
remaining shape blocked on `generic_const_exprs`.
Side cleanups:
* Strip the `__wbindgen_skip_interpret_calls` export after the
closure-cast interpreter pass consumes it.
* Remove the now-unused `Interpreter::skip_interpret()` accessor.
* Update module-level docs in `descriptors.rs` to describe the
section as the sole transport and explicitly attribute the
remaining interpreter use to the language gap.
Net diff: ~190 lines removed across the macro and the cli.
… generic_const_exprs The interpreter directory, the wrapper-impl SCHEMA workarounds, and the per-type WasmDescribeVector emissions all stem from a single upstream language gap: `feature(generic_const_exprs)` (rust-lang/rust#76560, nightly-only, no stable timeline). This commit makes that attribution unambiguous in the code so a reviewer a year from now can immediately see: * The scope of the interpreter is closure-cast descriptor recovery only. Header comment on `crates/cli-support/src/interpreter/mod.rs` spells this out, names the upstream issue, and describes the deletion plan. * The descriptors-pipeline module `crates/cli-support/src/descriptors.rs` has the same story in its module-level docs, cross-referenced. * `src/closure.rs` annotates `OwnedClosure` / `BorrowedClosure`'s empty `SCHEMA` as the one site that genuinely needs the upstream feature. * `src/describe.rs` updates the trait-level docs on `WasmDescribe` and `WasmDescribeVector` to call out which impls are workarounds vs. permanent, and points each at the same upstream issue. * The macro-emitted per-ImportType `WasmDescribeVector` impl gains the same explanation: it's the per-type workaround for what would otherwise be a blanket impl. No code semantics change. Pure documentation for the long shelf life this PR will have before it merges.
The previous entry described the section as a partial migration with a fallback. After this PR, the section is the sole descriptor transport for everything except closure casts. Update the wording to match the final architecture and explicitly attribute the closure- cast remainder to generic_const_exprs. Also notes the removed pieces: the legacy `__wbindgen_describe_<name>` synthetic exports, the `execute_exports` cli pass, and the unused `Interpreter::skip_interpret` accessor.
Replaces `describe()` + interpreter completely with const-time `SCHEMA_LEN`/`SCHEMA_BUF` pair on every `WasmDescribe` impl. Closure casts use a separate `wbg_cast_closure` whose `breaks_if_inlined_closure` is generic over `(T, UNWIND_SAFE)` so `T::invoke_shim_addr::<UW>()` folds to a single `i32.const` in the cast body alongside four schema pointer/length `i32.const` immediates. CLI side: - New 5-arg `__wbindgen_describe_cast(from_ptr, from_len, to_ptr, to_len, invoke_addr)`. - Narrow scanner reads the 5 immediates per cast site (`CastCallScanner`). - `DataSegmentView` resolves schema pointers to bytes from active data segments. - `crates/cli-support/src/interpreter/` directory deleted entirely. Runtime side: - `WasmDescribe` trait now has only `SCHEMA_LEN: usize` and `SCHEMA_BUF: [u32; SCHEMA_MAX]` (no `describe()` method). - `SCHEMA_MAX = 256` words = 1 KB per impl. - Wrapper impls (Option, Vec, Result, Clamped, &T, &mut T, MaybeUninit, Box<[T]>) compose schemas at const time via `wrap_schema`. - Closure trait-object impls compose via `cat_schema`. - `__wbindgen_describe` import removed. - `__wbindgen_skip_interpret_calls` export removed. Macro side: - `schema_parts_for_type` simplifies to forwarding to `<T as WasmDescribe>::SCHEMA_LEN/SCHEMA_BUF` — no more wrapper pattern-matching needed. - All `describe()` impl emissions removed. - Per-monomorphisation section statics use the same const-fn composition. Status: 378/379 wasm-bindgen tests pass. The one failure is `closures::reference_as_first_argument_works` — needs investigation; likely a closure-cast descriptor stream shape issue with `&Closure<dyn FnMut(&T)>` args. All 738 js-sys tests pass. CLI reference tests need re-blessing.
Two final fixes to make all 379+738+84 tests pass: 1. `convert/closures.rs`: closure-trait-object SCHEMA_BUF was reading `<$var as WasmDescribe>::SCHEMA` where `$var` was the inner ABI type (`A` for `dyn Fn(&A)`), not the syntactic arg type (`&A`). For closures with reference args this produced the wrong schema (missing the leading REF opcode), causing the JS adapter to be generated as if the arg were owned, which then moved a JS handle that was supposed to be borrowed. The macro now threads a separate `SchemaArgTy` list alongside `FnArgs`. For owned-arg closures it's the same as `$var`; for the `(&A)` ref case it's the actual `&A` reference type. 2. `cli-support/descriptors.rs` `CastCallScanner`: in debug mode, wasm-ld interleaves stack-pointer manipulation `i32.store` / `i32.load` instructions between the const setups and the marker call. The scanner previously reset its state on any unknown instruction, losing local-tracked constants. Added explicit handling for Store/Load/GlobalGet/GlobalSet/Binop/Unop (all appear in debug ABI plumbing but don't affect the immediates that flow into `__wbindgen_describe_cast`).
The previous entry described the section as the primary transport with the interpreter scoped to closure casts. After this PR the interpreter is gone entirely — closure-cast descriptors also flow through the section, with a narrow ~120-line scanner reading the per-monomorphisation invoke address as an i32.const immediate alongside the schema pointers.
* cargo fmt: descriptors.rs, codegen.rs, closure.rs, slices.rs, lib.rs, rt/mod.rs * clippy: drop redundant to_string on TokenStream, redundant usize cast, manual_saturating_arithmetic, collapsible nested match, format string inlining, allow(deprecated) for pre-existing assert_cmd usage
For builds with +bulk-memory (atomics, shared-memory, raytrace-parallel example, etc.), wasm-ld emits passive data segments rather than active ones and copies them into linear memory at startup via `__wasm_init_memory`. The cli's closure-cast scanner needs to read `SCHEMA_BUF` bytes from those segments, so it has to know the destination addresses. Extend `DataSegmentView::new` to: 1. Walk `Active` segments through `wasm_conventions::evaluate_const_expr` (handling Global/Extended offset shapes for PIC builds). 2. Catalogue `Passive` segments and look for `memory.init data_id` instructions with constant destination addresses in any local function. `__wasm_init_memory` for atomics builds nests its `memory.init` calls inside several layers of `block` for the once-per-thread cmpxchg guard; the scanner recurses through nested blocks/loops/if-else to find them. Verified: - `just test-wasm-bindgen` (mvp): 379 passed - Atomics build of wasm-bindgen tests: passes - The raytrace-parallel example failure in build_examples CI should resolve with this same fix.
walrus 0.26.4 includes the upstream fix for emitting active element segments targeting table64 tables with the explicit-table-index form (variant 0x02) instead of the MVP form (variant 0x00). MVP variant requires an i32 offset; with a table64 table the offset must be i64, and the resulting binary was rejected by V8 with "invalid table elements limits flags". Needed by the descriptor-section migration for wasm64 builds, where the cli inserts active element segments at the tail of the main function table to install closure-invoke wrappers.
The macro-emitted `#[used] static FOO: fn-ptr = wrapper;` keepalive makes wasm-ld place the closure-invoke wrapper in an element segment on wasm32 but not on wasm64 (rustc/wasm-ld doesn't propagate the address-taken treatment through that pattern when the static's value is encoded in 64-bit address space). Detect exported `__wbg_invoke_*` functions that aren't already in any element segment and append them by adding a fresh active element segment at the current table tail, growing the main function table by one. This is uniform across wasm32 and wasm64 and lets the descriptor SYMBOL_REF resolver find their slots without changes on the macro side. The active element segment uses the table's index type (i64 for table64, i32 otherwise); see walrus 0.26.4 for the corresponding emit-side fix.
The named-intrinsic externs (`__wbindgen_string_new` and the typed-
array constructors) declared in `src/lib.rs` took raw `*const u8` /
`usize` arguments. On wasm32 those lower to `(param i32 i32)`; on
wasm64 they lower to `(param i64 i64)` which becomes BigInt at the
JS boundary. The JS adapter intrinsic shims don't coerce BigInt to
Number, so subsequent arithmetic like `subarray(ptr, ptr + len)` in
`getStringFromWasm0` / `decodeText` threw `TypeError: Cannot
convert a BigInt value to a number".
Switch the extern signatures to take `WasmWordRepr` (= `f64` on
wasm64, `u32` on wasm32), matching the JS-Number ABI that the
`wbg_cast` path used before these were promoted to named intrinsics.
The cli's binding generator already handles f64 wasm params via
`normalize_memory64_signature`.
Callsites use new `__rt::{ptr_to_word, len_to_word}` helpers to coerce
`*const T` and `usize` into the wasm-arch-appropriate repr.
Test: full wasm64 test suite (387 tests) passes on Node 24; wasm32
suite unchanged.
Contributor
Author
|
I was able to trace the CI failure here to llvm/llvm-project#200083. Update: This failure seems to have been resolved in more recent nightlies, but can be reproduced by setting a higher value for Update: the resolution for this ended up being 46f2c04. |
The cli replaces `breaks_if_inlined` and `breaks_if_inlined_closure` with JS adapter imports whose wasm types are inherited from the helper's monomorphised signature. The previous `core::ptr::read` trick to keep `prim1..prim4` alive collapsed when `WasmRet<To::Abi>` was zero-sized (e.g. `To = ()` — only ZST prims in `WasmRet`), leaving the optimiser free to dead-eliminate inputs and shrink the wasm signature to `() -> ()`. cli-support then asserted the signature against the cast descriptor and panicked (observed on raytrace-parallel under nightly 2026-05-27). Replace the pointer-cast read with two helpers that defeat elimination on both ends: * `keep_prims_alive` — volatile write of `(p1, p2, p3, p4)` into a `MaybeUninit` scratch. * `make_ret::<To>` — volatile read from a `MaybeUninit<WasmRet<...>>` scratch typed for the declared return. Both functions are never actually executed (the cli rewrites the containing helper into an import before runtime), so the volatile operations exist solely to anchor the wasm-level signature.
The breaks_if_inlined / breaks_if_inlined_closure helpers carry the cast ABI in their wasm-level signature (the cli scanner reads the function type to manufacture the JS-adapter import). LLVM treats these as 'internal fastcc' since they're only called from one site each, and runs interprocedural argument elimination on them, stripping unused-looking prims from the signature. The earlier keep_prims_alive volatile-write barrier wasn't enough: LLVM dead- stores writes whose target alloca has no surviving reads, so the arguments remain elidable. Take the address of each helper from the wbg_cast / wbg_cast_closure caller and route it through black_box. This forces the address to escape the module, making LLVM treat the helper as externally visible for ABI purposes (no deadargs pass). Without this fix, casts whose From abi has zero-sized prims collapse the helper's wasm signature, generating cast import declarations with fewer args than the JS adapter side expects. At runtime the missing args arrive as undefined, leading to garbage pointers and 'null function' call_indirect errors in compiled invoke shims (e.g. websockets onopen Closure::<dyn FnMut()>::new crashing in wasm_bindgen__convert__closures_____invoke__h*).
Imports, exports, struct getters, ImportStatic, and DynamicUnion
variants each emit a 'pub static __WBG_DESCRIPTOR_<mangled>' linked
into the '__wasm_bindgen_descriptors' custom section. When the
matching wrapper / accessor body is inlined into a caller — most
commonly when the consumer is a cdylib linking the producing crate
as an rlib — the wasm import (or export name) inlines along with
the body, but the descriptor static stays in whichever CGU the
partitioner placed it in. Lazy archive resolution then leaves that
CGU's '.o' unlinked, and the custom-section bytes disappear with
it. cli-support hits the now-orphaned import and bails with
'import of __wbg_get_<hash> doesn't have an adapter listed' (or
later asserts in the externref transform with a signature mismatch).
This is reproducible today by building 'examples/raytrace-parallel'
with 'profile.release.codegen-units=256' on the existing nightly,
and is the failure that started appearing on CI with newer nightlies
whose default partitioner is more aggressive about splitting items.
Two coordinated changes resolve it:
1. Hoist '__WBG_DESCRIPTOR_<mangled>' out of the surrounding
'const _: () = { ... }' wrapper so the static is a module-scope
item with a stable Rust path. The helper consts that build the
byte payload remain inside an inline 'const' block in the
initializer.
2. Add a 'descriptor_anchor()' helper that emits
#[cfg(target_family = "wasm")]
{ ::core::hint::black_box(&__WBG_DESCRIPTOR_<mangled>); }
inside every macro-generated body that wasm-lld might pull
independently of the descriptor's CGU. When the body is inlined
into a caller's CGU, the address-take inlines too, which makes
the caller's CGU undef-reference the descriptor symbol and
forces wasm-lld to pull the archive '.o' that defines it.
'black_box' is opaque to LLVM, so the symbol reference survives
all the way to the wasm encoding as a relocation rather than
being constant-folded away.
Anchors are placed in:
* import wrapper bodies ('ImportFunction::try_to_tokens')
* 'static_init' (ImportStatic accessor body)
* struct field getter bodies
* export wrapper bodies ('Export::try_to_tokens')
* 'DynamicUnion::FromWasmAbi::from_abi' for every variant
The cost is one materialised symbol address per call site, which
'black_box' keeps live but the optimiser routinely sinks off hot
paths. The alternative of forcing '--whole-archive' across every
rlib that might contain wasm-bindgen descriptors was rejected:
it's a coarse global flag that defeats lazy DCE for all archive
contents (not just descriptors), has to be set in the consumer's
build configuration outside wasm-bindgen's control, and would
hide the macro/linker contract that the per-symbol anchor makes
explicit.
Verified:
* 'examples/raytrace-parallel' builds with default and
'codegen-units=256' settings.
* 'cargo test -p wasm-bindgen-cli' all 42 native cli + 84
reference snapshot tests pass with no snapshot drift.
ImportString reuses 'static_init' (via 'thread_local_import') but does not emit a matching '__WBG_DESCRIPTOR_<shim>' static. The previous commit unconditionally inserted an anchor reference in 'static_init', which broke ImportString expansion at macro time with 'cannot find value __WBG_DESCRIPTOR_<shim>... in this scope'. Thread the choice through as an explicit 'anchor_descriptor' bool on 'static_init' and 'thread_local_import'. ImportStatic (which does emit a descriptor) passes true; ImportString passes false.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Descriptors via custom section: delete the wasm interpreter
Replaces the long-standing wasm-bindgen-cli descriptor interpreter
with a compile-time custom section transport. The
crates/cli-support/src/interpreter/directory is deleted in thisPR.
What changes
Every
#[wasm_bindgen]shim's descriptor is now emitted at compiletime as bytes in the new
__wasm_bindgen_descriptorscustom wasmsection.
wasm-bindgen-clireads those bytes structurally withoutexecuting any wasm.
Coverage is complete: exports, imports,
ImportStatic, struct fieldgetters, dynamic unions, closure-arg wrappers, and closure-cast
monomorphisations. The legacy describe-by-execution mechanism —
which involved synthesising
__wbindgen_describe_<name>exports forthe cli to interpret — is gone end-to-end.
How the const-time schema works
Every
WasmDescribeimpl carries a pair of associated consts:The fixed buffer size (1 KB per impl) sidesteps the
generic_const_exprswall — we don't need generic-length array expressions because every
schema lives in the same-sized buffer with the meaningful prefix
indicated by
SCHEMA_LEN. Wrapper types (Option<T>,Vec<T>,Result<T, E>,&T,&mut T, closure trait objects) compose theirschemas at const time via
const fns that read the inner type's(SCHEMA_LEN, SCHEMA_BUF)pair and produce a new pair:For closure trait objects:
Overflow (any single type's schema exceeding 256 words) is a
const_panic!at compile time with a clear message.How closure casts work without an interpreter
wbg_cast_closure::<From, To, T, UNWIND_SAFE>instantiates a freshbreaks_if_inlined_closure<From, To, T, UNWIND_SAFE>whose bodypasses five
i32.constimmediates to a marker import:The schema pointers fold to data-segment addresses; the invoke
address folds to the function-table slot of the per-
(T, UW)invoke shim. All five are
i32.constvalues in the linked wasm.The cli scans each
breaks_if_inlined_closurebody for these fiveimmediates, reads the schema bytes from the data segment, patches
the invoke slot into the closure descriptor's
shim_idxplaceholder,and composes a complete
Closuredescriptor for the regularpipeline. The scanner is ~120 lines, handles
i32.const,local.get/local.set/local.tee(debug-mode constant shuttling),basic stack-effect bookkeeping for
i32.store/i32.load/global.get/set/binop/unop— no general wasminterpretation, no branches, no loops.
What gets removed
crates/cli-support/src/interpreter/directory (~1200 lines)WasmDescribe::describe()methodwasm_bindgen::describe::inform()__wbindgen_describeextern import__wbindgen_skip_interpret_callsexport__wbindgen_describe_<name>synthetic export functionsInterpreter,Frame,skip_callsmachineryNet diff: roughly 2200 lines deleted across the runtime, macro, and
cli.
Verification
just test-wasm-bindgen: 379 passed, 0 failedjust test-js-sys: 738 passed, 0 failedjust test-cli: 84 reference tests pass__wbindgen_describe_<name>exports remain in anyreference-test wasm
MSRV
Bumped from 1.77 to 1.79 for library and macro crates. The const-fn
machinery (
wrap_schema,cat_schema,schema_from_slice) backingthe schema-buffer composition needs 1.79's const evaluator features.
Trade-offs
Pre-bindgen wasm size. Each
WasmDescribeimpl carries a 1 KBSCHEMA_BUFconstant. For a typical wasm-bindgen module with ~100unique types, that's ~100 KB of pre-bindgen schema bytes. These are
stripped by the cli post-section-ingest before any user-visible
output (
wasm-optinput, browser-facing wasm) so the finaldelivered binary is unaffected. The intermediate size is a real but
bounded cost; for size-sensitive build pipelines an opt-in
feature(generic_const_exprs)mode could shrink each impl to itsactual size in the future.
Schema buffer overflow. A single type's schema exceeding 256
u32 words is a compile-time error. For context, the longest schema
in the existing test suite is ~25 words; even deeply nested generic
closures don't realistically approach 100 words. If a user
constructs an exotic type that overflows, they get a
const_panic!naming the wall and can file an issue to discuss the bound.
Migration path for downstream code
Implementing
WasmDescribeoutside the macro (rare, mostlyinternal) now requires providing
SCHEMA_LENandSCHEMA_BUFinstead of
describe(). Helper consts:Wrapper-style impls use
wrap_schema(header, &T::SCHEMA_BUF, T::SCHEMA_LEN).