Repository navigation
Description
The wasm Component Model spec's Binary.md defines the byte format of canon-async builtins but is silent on how the resulting core funcs surface as imports for the host to satisfy. Today this is a tooling decision: wit-component emits a shim module ("wit-component:shim") with integer-named imports from the empty module namespace (("", "0"), ("", "1"), ...), recording the canon-op identity ("task.return", "stream.read", etc.) in the shim's function-name custom section.
This works for runtimes that compile-everything-statically (wasmtime), because they don't actually use the import names — they compile trampolines directly from the parsed canon entries. It works less well for name-based host-binding runtimes that satisfy imports by (module, name) lookup (interpreters, JIT engines that don't compile to native, runtimes like WACS).
I ran into this while implementing WASIp3 support in WACS
Per the spec, name sections are optional and strippable. Common release-pipeline tools strip them by default (wasm-opt --strip-debug, wasm-tools strip, jco bundling). Once the function-name subsection is stripped from a wit-component-emitted shim:
- The shim's structure (imports from
""with integer names, funcref table) is recoverable. - However, the per-shim canon-op identity is not — the position-to-op mapping is wit-component's internal emit order, not derivable from anything else in the binary.
The host runtime can recognize "this is a shim" but cannot bind it. i.e. Embedders that strip names from canon-async-using components have broken hostability for name-resolving runtimes.
This is structurally the same problem the resource handle-table convention solved by standardizing ("[export]<iface>", "[resource.{new,drop,rep}]<name>") import names: the canon-op identity moved into the wire-level import declaration where it can't be stripped without breaking module structure.
Proposal
Standardize a canon-async import convention at the spec level.
Reserved module name + semantic function names.
Reserve a module name (e.g., cm:canon, matching the wasm:js-string builtin namespace pattern) for canon-async imports. Function names match wasmtime's Trampoline::symbol_name() spellings (kebab-case: "task-return", "stream-new", "waitable-set-wait", etc.) with the typeidx encoded for ops that carry one (e.g., "stream-new$5"). The shim module's indirection becomes optional / vestigial. Strip-resistant by construction.
References
- wit-component emit:
crates/wit-component/src/encoding.rs:1147-1218(shim + fixups module construction). - wasmtime trampoline naming:
crates/environ/src/component/info.rs:1180+(Trampoline::symbol_name()— the closest existing canonical spelling). - Prior art: the
[resource.{new,drop,rep}]<name>convention already in Binary.md for handle-table intrinsics.
Activity
lukewagner commentedon May 19, 2026
You might want to have a look at #378 which was some initial work to standardize the core wasm import/export names. It was defined well before Preview 3, so doesn't include conventions for async functions and all the concurrency built-ins, but we'd like to revisit it after Preview 3 is released.
That being said, #378 is mostly useful for producer toolchains (as an interface between the compiler producing core modules and the linker (e.g.
wasm-component-ld) that generate components). For a host, components can contain multiple nested components and core modules that are linked in ways not expressible by a simple name convention. To allow a core engine to support the full set of components, we'd need something more like bytecodealliance/rfcs/#46.Lastly, just as a technical point, a component binary
.wasmdoes embed all the naming and linking information for a fully dynamic runtime to reflect on the component and see how the core module names (which are indeed arbitrary) connect to the WIT-defined names and CM built-ins, so this isn't really a "static-vs-dynamic" distinction.kelnishi commentedon May 20, 2026
I'll take another crack at walking the canon entries to resolve the slots.