[wasmparser] Add [implements=<I>]L component name support#2453
[wasmparser] Add [implements=<I>]L component name support#2453ricochet wants to merge 5 commits intobytecodealliance:mainfrom
[implements=<I>]L component name support#2453Conversation
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
e875d9d to
6944e34
Compare
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
ce9d98b to
9334ce8
Compare
Support the `import label: iface;` and `export label: iface;` WIT
syntax, which encodes as `[implements=<I>]label` in the binary format.
This allows importing or exporting the same interface multiple times
under different names.
Changes include:
- Add `implements: Option<InterfaceId>` field to `WorldItem::Interface`
- Parse `NamedPath` variant in the WIT AST with disambiguation against
fully-qualified `namespace:package/interface` paths
- Decode `[implements=<I>]label` names in all binary decoding paths
via a new `decode_world_instance` helper
- Thread `implements` through world elaboration and ID remapping
- Add `name_world_key_with_item` for binary encoding
On the API change for `name_world_key_with_item`, I opted to introduce
this new fn that is used only at the few call sites that need it vs updating
the ~50 sites for name_world_key.
9334ce8 to
3c7046d
Compare
Update component encoding to use `name_world_key_with_item` at sites that produce component-level extern names, so that `implements` imports and exports are encoded as `[implements=<I>]L` in the binary format. Five call sites are changed from `name_world_key` to the implements-aware variant: import_map key construction, component export names, ImportedResourceDrop lookups, and both direct and indirect InterfaceFunc lookups.
Teach wit-smith to generate `ImplementsInterface` items in worlds, producing `%label: path;` WIT syntax which encodes as `[implements=<I>]L` in the component binary. This enables fuzzing of the implements feature through the existing roundtrip_wit fuzzer.
3c7046d to
7c5a1ba
Compare
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
alexcrichton
left a comment
There was a problem hiding this comment.
Thanks again for this! I've left some comments below, the most consequential one being about the AST structure of wit-parser itself. I'd like to still look more closely at some details once that's settled, but also wanted to give some initial feedback. I'll need to catch up on the wasmtime/wit-bindgen discussions too
|
|
||
| // implements names | ||
| assert!(parse_kebab_name("[implements=<a:b/c>]name").is_some()); | ||
| assert!(parse_kebab_name("[implements=<a:b/[email protected]>]name").is_some()); | ||
| assert!(parse_kebab_name("[implements=<ns:pkg/iface>]my-label").is_some()); | ||
| // invalid: not a valid interface name (no colon/slash) | ||
| assert!(parse_kebab_name("[implements=<not-valid>]name").is_none()); | ||
| // invalid: empty interface name | ||
| assert!(parse_kebab_name("[implements=<>]name").is_none()); | ||
| // invalid: missing label | ||
| assert!(parse_kebab_name("[implements=<a:b/c>]").is_none()); | ||
| // invalid: label not kebab | ||
| assert!(parse_kebab_name("[implements=<a:b/c>]NOT_KEBAB").is_none()); |
There was a problem hiding this comment.
In addition to unit tests here, could you add some unit tests as *.wast files in tests/cli/component-model/*.wast? Maybe something like tests/cli/component-model/implements.wast or similar. It's ok to move these tests outright there where necessary, but by having a *.wast test we can help other implementations' tests as well in the future by eventually sharing wast tests
There was a problem hiding this comment.
Thanks for adding wit-smith support! I always forget to do that...
Can you run the fuzzer locally for a few minutes to make sure nothing crops up? You can do that with:
$ FUZZER=roundtrip_wit cargo +nightly fuzz run -s none run| serialize_with = "serialize_optional_id" | ||
| ) | ||
| )] | ||
| implements: Option<InterfaceId>, |
There was a problem hiding this comment.
AST-wise I think I might bikeshed this a bit. I find this a bit confusing where there's two InterfaceIds listed here, one above with id and one for implements, and for import foo: bar; I'd assume implements would point to bar but it's not clear to me what id above would point to.
Additionally foundationally I'd ideally like to keep things such that name_world_key is sufficient for generating the import name of a component/instance import.
Given all that, WDYT about augmenting WorldKey instead of augmenting WorldItem? What I'm imagining is:
- Add a new
WorldKey::Implements { name: String, interface: InterfaceId }- this'd corresponds toimport foo: barwithnamebeingfooandinterfacepointing tobar - Reuse
WorldItem::Interfacefor theWorldItem, in this case withidalso pointing tobar
To me that feels like it'll mesh better with the existing generators and such, but want to double-check with how you're thinking about this.
| /// - `[implements=<I>]label` — named import/export implementing interface I | ||
| /// - `ns:pkg/iface` — qualified interface name, keyed by `InterfaceId` | ||
| /// - `plain-name` — unqualified name for an inline or local interface | ||
| fn decode_world_instance<'a>( |
There was a problem hiding this comment.
Thanks for doing the refactor here to split this out!
| /// Parses an `[implements=<interface_name>]label` name, returning | ||
| /// the interface name and label if the name matches this pattern. | ||
| fn parse_implements_name(name: &str) -> Option<(&str, &str)> { | ||
| let rest = name.strip_prefix("[implements=<")?; |
There was a problem hiding this comment.
I'm not sure if it'd make sense, but one option here would be to use ComponentName from wasmparser to do the parsing here (I think that's what it's called) which could alleviate the string processing from here
Add parsing, validation, and uniqueness rules for the new
[implements=<interface>]labelextern name form from the componentmodel
implementsproposal. An implements name labels an instanceimport/export that implements a named interface.
Uniqueness:
[implements=<I>]Lconflicts with bare labelLand with[method]L.L/[static]L.L(the existing l.l edge case), but isstrongly unique from interface names, constructors, and normal
method/static names.
See implements feature:
WebAssembly/component-model#613