feat: Add support for implements syntax#12698
feat: Add support for implements syntax#12698ricochet wants to merge 1 commit intobytecodealliance:mainfrom
Conversation
|
I'm excited to see this happening after reading through WebAssembly/component-model#287 -- thanks! A question on the host-side API design: the PR description says
This is somewhat surprising to me as, at least naively, I'd expect one WIT interface to correspond to one Rust trait; rather than statically generating new Rust traits per separate import of an interface. This is sort of getting back to the issue I described here, but in host form: if I have a generic implementation (say of I don't know if this means that there needs to be an |
|
You're raising a good point. There are a few different ways to handle the Rust-side trait story for With what I have in draft, each labeled import gets its own The use-case where I really don't like this approach is the VFS one, where it's different contained filesystem volume mounts, and I will want to treat them all exactly the same. In a lot of ways, I see Rust doesn't allow implementing the same trait twice on the same type with different behavior. So the design choice is really about which usage pattern gets us to the most ergonomic path. Some alternatives considering a world that imports the same key-value store interface twice; one backed by Redis, one by Memcached: interface store {
get: func(key: string) -> option<string>;
set: func(key: string, value: string);
}
world my-cache {
import hot-cache: store; // backed by memcached
import durable: store; // backed by redis
}With Option A (current PR): // hot_cache::Host and durable::Host are separate traits with identical signatures
impl hot_cache::Host for MyState {
fn get(&mut self, key: String) -> Option<String> { self.memcached.get(&key) }
fn set(&mut self, key: String, value: String) { self.memcached.set(&key, &value); }
}
impl durable::Host for MyState {
fn get(&mut self, key: String) -> Option<String> { self.redis.get(&key) }
fn set(&mut self, key: String, value: String) { self.redis.set(&key, &value); }
}
hot_cache::add_to_linker(&mut linker, |s| s)?;
durable::add_to_linker(&mut linker, |s| s)?;Option B: Shared trait, label-parameterized registrationOne // Library types implement store::Host directly, no shims, so we could easily share a wasmtime-keyvalue-backend
// RedisBackend: impl store::Host { ... }
// MemcachedBackend: impl store::Host { ... }
struct MyState {
hot_cache: MemcachedBackend,
durable: RedisBackend,
}
store::add_to_linker(&mut linker, "hot-cache", |s: &mut MyState| &mut s.hot_cache)?;
store::add_to_linker(&mut linker, "durable", |s: &mut MyState| &mut s.durable)?;What I like about this is that it's easy to reason about, "interfaces are shapes, labels are binding sites" Option C: Shared trait, per-label convenience modulesGenerate one shared Host trait in the first module. Subsequent label modules re-export it (no shadowing) and provide thin // Generated: shared trait
mod store { pub trait Host { ... } }
// Generated: per-label wrappers with the label baked in
mod hot_cache {
pub use super::store::*; // includes Host, not shadowed
pub fn add_to_linker<T, D: Host>(...) -> Result<()> {
store::add_to_linker(linker, "hot-cache", get)
}
}
mod durable {
pub use super::store::*;
pub fn add_to_linker<T, D: Host>(...) -> Result<()> {
store::add_to_linker(linker, "durable", get)
}
}
// User code: library types work directly:
struct MyState {
hot_cache: MemcachedBackend, // impl store::Host
durable: RedisBackend, // impl store::Host
}
hot_cache::add_to_linker(&mut linker, |s: &mut MyState| &mut s.hot_cache)?;
durable::add_to_linker(&mut linker, |s: &mut MyState| &mut s.durable)?;I like that a per-label The change to do either of these options is fairly contained, and I am very open to any of these approaches. Are there other options I haven't considered? |
|
Thanks for sketching out the decision-space you've considered a bit more! I think that Option B is the clear answer at least to me; a few reasons:
I do think there is an interesting question about how to build a more statically-typed API around this. Taking the "world composition site must be separate from interfaces" requirement again, I think it would have to look something like defining a type for a world that is a builder for a linker, and takes the properly typed thing for each named (or default unnamed) interface that is imported. Something like becoming or something like that, and then One could use this strategy with today's worlds (without named interface instances) as well -- then you'd have In any case, that seems like a much higher bar / stretch goal, and as mentioned above I don't see the "label string" approach as fundamentally worse than setting up a core Wasm environment with string names for provided functions, so that (Option B) is probably good enough for now! |
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/store@1.0.1>]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/store@1.0.0>]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
DO NOT MERGE until wasm-tools release with
bytecodealliance/wasm-tools#2453
Points wasm-tools to PR branch
wasmparser-implementsAdd support for the component model
[implements=<I>]L(spec PR #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:
Guest code sees two separate namespaces with identical shapes:
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.
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:
Semver matching works inside the implements annotation, just like
regular interface imports:
Changes
Runtime name resolution
[implements=<I>]LImport codegen (crates/wit-bindgen/src/lib.rs)
InterfaceIdHosttrait per interface;generate_add_to_linkertakesnamed: bool— when true, emitsname: &strparameter instead ofhardcoding the instance name
implementsimports: just record the label inimplements_labels, no module generationworld_add_to_linker: iterate overimplements_labelsto emit oneadd_to_linkercall per label, passing label as name argumentpopulate_world_and_interface_optionswithentry()to avoidoverwriting link options for duplicate interfaces
Export codegen (crates/wit-bindgen/src/lib.rs)
pub use super::{first}::*,generate fresh
Guest/GuestIndices, plus regenerate resource wrapperstructs to reference the local
Guesttypename_world_key_with_itemfor export instance name lookups