Add [implements=<I>]L plainname for multiple imports#613
Add [implements=<I>]L plainname for multiple imports#613
Conversation
lukewagner
left a comment
There was a problem hiding this comment.
Awesome, thanks for writing this up! The PR looks great; just minor nits. I think what'll be important for this change is seeing how it "plays out" in the producer and consumer tooling, so I think we'll want to keep this PR open until we get some experience from tool implementations, but so far so good from my perspective.
Add parsing, validation, and uniqueness rules for the new `[implements=<interface>]label` extern name form from the component model `implements` proposal. An implements name labels an instance import/export that implements a named interface. Uniqueness: `[implements=<I>]L` conflicts with bare label `L` and with `[method]L.L` / `[static]L.L` (the existing l.l edge case), but is strongly unique from interface names, constructors, and normal method/static names. See implements feature: WebAssembly/component-model#613
Extend the WIT `extern-type` grammar to allow `use-path` as a third case, enabling `import id: use-path` and `export id: use-path` to create plain-named imports/exports whose instance type matches a named interface. This allows importing the same interface multiple times under different plain names (e.g., `import primary: store; import secondary: store;`), encoded using the `[implements=<interfacename>]label` annotation pattern. Fixes #287 Co-authored-by: Luke Wagner <[email protected]>
DO NOT MERGE until wasm-tools release with bytecodealliance/wasm-tools#2453 Points wasm-tools to PR branch `wasmparser-implements` Add support for the component model `[implements=<I>]L` (spec PR [bytecodealliance#613](WebAssembly/component-model#613)), which allows components to import/export the same interface multiple times under different plain names. A component can import the same interface twice under different labels, each bound to a distinct host implementation: ```wit import primary: wasi:keyvalue/store; import secondary: wasi:keyvalue/store; ``` Guest code sees two separate namespaces with identical shapes: ```rust let val = primary::get("my-key"); // calls the primary store let val = secondary::get("my-key"); // calls the secondary store ``` From the host, wit-bindgen generates a separate Host trait per label: ```rust impl primary::Host for MyState { fn get(&mut self, key: String) -> String { self.primary_db.get(&key).cloned().unwrap_or_default() } } impl secondary::Host for MyState { fn get(&mut self, key: String) -> String { self.secondary_db.get(&key).cloned().unwrap_or_default() } } primary::add_to_linker(&mut linker, |state| state)?; secondary::add_to_linker(&mut linker, |state| state)?; ``` The linker also supports registering by plain label without knowing the annotation: ```rust // Component imports [implements=<wasi:keyvalue/store>]primary // but the host just registers "primary" — label fallback handles it linker.root().instance("primary")?.func_wrap("get", /* ... */)?; ``` Users can also register to the linker with the full encoded `implements` name ```rust let mut linker = Linker::<()>::new(engine); linker .root() .instance("[implements=<wasi:keyvalue/store>]primary")? .func_wrap("get", |_, (key,): (String,)| Ok((String::new(),)))?; ``` Semver matching works inside the implements annotation, just like regular interface imports: ```rust // Host provides v1.0.1 linker .root() .instance("[implements=<wasi:keyvalue/[email protected]>]primary")? .func_wrap("get", |_, (key,): (String,)| Ok((String::new(),)))?; // Component requests v1.0.0, matches via semver let component = Component::new(&engine, r#"(component (type $store (instance (export "get" (func (param "key" string) (result string))) )) (import "[implements=<wasi:keyvalue/[email protected]>]primary" (instance (type $store))) )"#)?; linker.instantiate(&mut store, &component)?; // works, 1.0.1 is semver-compatible with 1.0.0 ``` ## Changes ### Runtime name resolution - Add three-tier lookup in NameMap::get: exact → semver → label fallback - Add implements_label_key() helper for extracting plain labels from `[implements=<I>]L` - Add unit tests for all lookup tiers ### Code generation for multi-import/export - Track first-seen implements imports/exports per `InterfaceId` - Duplicate imports: re-export types via `pub use super::{first}::*`, generate fresh Host trait + add_to_linker - Duplicate exports: same pattern with fresh Guest/GuestIndices, plus regenerate resource wrapper structs to reference the local Guest type - Use `name_world_key_with_item` for export instance name lookups - Guard `populate_world_and_interface_options` with `entry()` to avoid overwriting link options for duplicate interfaces
DO NOT MERGE until wasm-tools release with bytecodealliance/wasm-tools#2453 Points wasm-tools to PR branch `wasmparser-implements` Add support for the component model `[implements=<I>]L` (spec PR [bytecodealliance#613](WebAssembly/component-model#613)), which allows components to import/export the same interface multiple times under different plain names. A component can import the same interface twice under different labels, each bound to a distinct host implementation: ```wit import primary: wasi:keyvalue/store; import secondary: wasi:keyvalue/store; ``` Guest code sees two separate namespaces with identical shapes: ```rust let val = primary::get("my-key"); // calls the primary store let val = secondary::get("my-key"); // calls the secondary store ``` Host Import-side codegen: shared trait + label-parameterized add_to_linker For imports, wit-bindgen generates one Host trait per interface (not per label). The add_to_linker function takes a name: &str parameter so the same trait implementation can be registered under different instance labels. Duplicate implements imports don't generate separate modules — only the first import produces bindings. ```rust struct PrimaryBackend; impl primary::Host for PrimaryBackend { fn get(&mut self, key: String) -> String { self.primary_db.get(&key).cloned().unwrap_or_default() } } struct SecondaryBackend; impl primary::Host for SecondaryBackend { fn get(&mut self, key: String) -> String { self.secondary_db.get(&key).cloned().unwrap_or_default() } } // Same add_to_linker, different labels and host_getter closures primary::add_to_linker(&mut linker, "primary", |s| &mut s.primary)?; primary::add_to_linker(&mut linker, "secondary", |s| &mut s.secondary)?; ``` Export-side codegen: per-label modules with shared types For exports, each label gets its own module with fresh Guest/GuestIndices types but re-exports shared interface types from the first module via `pub use super::{first}::*`. Runtime name resolution The linker supports registering by plain label without knowing the annotation: ```rust // Component imports [implements=<wasi:keyvalue/store>]primary // but the host just registers "primary" — label fallback handles it linker.root().instance("primary")?.func_wrap("get", /* ... */)?; Users can also register to the linker with the full encoded implements name: linker .root() .instance("[implements=<wasi:keyvalue/store>]primary")? .func_wrap("get", |_, (key,): (String,)| Ok((String::new(),)))?; ``` Semver matching works inside the implements annotation, just like regular interface imports: ```rust // Host provides v1.0.1 linker .root() .instance("[implements=<wasi:keyvalue/[email protected]>]primary")? .func_wrap("get", |_, (key,): (String,)| Ok((String::new(),)))?; // Component requests v1.0.0, matches via semver let component = Component::new(&engine, r#"(component (type $store (instance (export "get" (func (param "key" string) (result string))) )) (import "[implements=<wasi:keyvalue/[email protected]>]primary" (instance (type $store))) )"#)?; linker.instantiate(&mut store, &component)?; // works, 1.0.1 is semver-compatible with 1.0.0 ``` - Add three-tier lookup in NameMap::get: exact → semver → label fallback - Add implements_label_key() helper for extracting plain labels from `[implements=<I>]L` - Add unit tests for all lookup tiers - Track first-seen implements imports per `InterfaceId` - One `Host` trait per interface; `generate_add_to_linker` takes `named: bool` — when true, emits `name: &str` parameter instead of hardcoding the instance name - Duplicate `implements` imports: just record the label in `implements_labels`, no module generation - `world_add_to_linker`: iterate over `implements_labels` to emit one `add_to_linker` call per label, passing label as name argument - Guard `populate_world_and_interface_options` with `entry()` to avoid overwriting link options for duplicate interfaces - Duplicate exports: re-export types via `pub use super::{first}::*`, generate fresh `Guest`/`GuestIndices`, plus regenerate resource wrapper structs to reference the local `Guest` type - Use `name_world_key_with_item` for export instance name lookups
| @@ -0,0 +1,78 @@ | |||
| ;; RUN: wast --assert default --snapshot tests/snapshots % | |||
There was a problem hiding this comment.
| ;; RUN: wast --assert default --snapshot tests/snapshots % |
Since test is new and not wasm-tools-specific, could you move it to a new directory test/validation/implements.wast?
| )) | ||
| ) | ||
|
|
||
| ;; Invalid: [implements=<...>] on func (must be instance) |
There was a problem hiding this comment.
Could you also add a failing test for attempting to export value type and a resource type? And second: could you also add copies of these tests for interfacenames (given that recent addition to the validation rules)?
|
Oh, I forgot: we probably should have an emoji-gate for this associated with the new rules in the grammar in Explainer.md and the descriptions in WIT.md. |
| } | ||
|
|
||
| world extended { | ||
| include base with { cache as my-cache } |
There was a problem hiding this comment.
Thinking more about this example/rule in the context of duplicate-name-conflicts: could you expand this example to have two includes that contain a conflict for the plain-name cache and explicitly mention that this would be an error without the with (b/c we can't de-duplicate plain names like we can with pure interface names).
Add
[implements=<I>]L plainnamefor multiple imports of the same interfaceExtend the WIT
extern-typegrammar to allowuse-pathas a third case,enabling
import id: use-pathandexport id: use-pathto createplain-named imports/exports whose instance type matches a named interface.
This allows importing the same interface multiple times under different
plain names (e.g.,
import primary: store; import secondary: store;),encoded using the
[implements=<interfacename>]labelannotation pattern.Fixes #287