ricochet opened PR #12698 from ricochet:implements to bytecodealliance:main:
DO NOT MERGE until wasm-tools release with
https://github.com/bytecodealliance/wasm-tools/pull/2453
Points wasm-tools to PR branchwasmparser-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:import primary: wasi:keyvalue/store; import secondary: wasi:keyvalue/store;Guest code sees two separate namespaces with identical shapes:
let val = primary::get("my-key"); // calls the primary store let val = secondary::get("my-key"); // calls the secondary storeFrom the host, wit-bindgen generates a separate Host trait per label:
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:
// 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
implementsnamelet 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:
// 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.0Changes
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
InterfaceIdDuplicate imports: re-export types via
pub use super::{first}::*,
generate fresh Host trait + add_to_linkerDuplicate exports: same pattern with fresh Guest/GuestIndices,
plus regenerate resource wrapper structs to reference the local Guest typeUse
name_world_key_with_itemfor export instance name lookups- Guard
populate_world_and_interface_optionswithentry()to avoid
overwriting link options for duplicate interfaces
cfallin commented on PR #12698:
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
From the host, wit-bindgen generates a separate Host trait per label:
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
wasi-http's various interfaces) I might have to write a whole set of newtype shims on top to label them as "the first HTTP" or "the second HTTP". From a perspective of dependency injection and interfaces (even fully statically resolved ones like C++ templates or ML modules) I'd expect to be able to programmatically plug in various (say) HTTP-shaped things into the N static HTTP slots just like I'd pass (strongly typed) function parameters.I don't know if this means that there needs to be an
add_to_linkervariant that takes a label, or something like that? I haven't read through this PR or tried to sketch a full design delta, apologies; just reacting to the lack of "WIT interface == Rust interface" and the implications that might have.
ricochet commented on PR #12698:
You're raising a good point. There are a few different ways to handle the Rust-side trait story for
implements, and they sit on a real tradeoff axis.With what I have in draft, each labeled import gets its own
Hosttrait in its own module. Types are shared across labels (viapub use), but the traits are distinct. The mental for this case is that each label implies significant behavioral differences. The reason I opted for this is that there is a key reason a world may have opted to use animplementslabel, like one cache for remote session management and the other is a local cache. In my use-case, I have a host that connects to many different types of stores and desire the ergonomics of N impl blocks on one type.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
implementsas a first step towards wit templates, and for that, this is a poor fit.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 registration
One
Hosttrait per interface. Each label'sadd_to_linkertakes a label name (or wraps it), and the user provides separate values/types that all impl the same trait.// 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 modules
Generate one shared Host trait in the first module. Subsequent label modules re-export it (no shadowing) and provide thin
add_to_linkerwrappers with the label baked in:// 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
add_to_linkermeans no stringly-typed labels, although I'm not overly concerned that we require type checking here when pre_instantiate guarantees imports are fulfilled. The only downside I see with this is that both options B and C require separate types.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?
cfallin commented on PR #12698:
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:
One has to consider the separation between provider code (the implementation of an interface) and a particular world. For example the
wasi-httpcrate exports itsadd_to_linker; it knows nothing about how some particular embedding might choose to define a world that accepts two instances of its interface. So we can't bake that distinction into the traits themselves (option A) nor have statically differentadd_to_linkers (option C).At least the way I think about it, I see the "lego-block plumbing" philosophy of component composition writ large reflected down into the world-definition space here: one should be able to define a world that imports N interfaces (some of the same type) and exports M interfaces, and wire up a bunch of different modules on the host side that came from different places, plugging them in in just one place that does that wiring. Thinking of what has to be defined in each separable module hopefully crystalizes some of the choices (?).
I also see this as kind of analogous to the core Wasm function import/export universe. Interfaces today are distinguished only by their type; one can only have one of each type. That'd be akin to saying that one can only import one function of signature
(param i32) (result i32). We use stringly-typed names to allow there to be more than one, so in that sense I don't see it as worse that we would use textual labels to name the specific slots we plug instances of interfaces into, either.
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
world example-world { import first: my-interface; import second: my-interface; }becoming
struct ExampleWorldBuilder { ... } impl ExampleWorldBuilder { fn new() -> Self { ... } fn first(&mut self, impl_: impl MyInterfaceBuilder) { ... } fn second(...) { } fn build(self, linker: &mut Linker) { ... } } trait MyInterfaceBuilder { fn add_to_linker(...); }or something like that, and then
MyInterfaceBuilderis provided by wit-bindgen in the same wayadd_to_linkeris today -- details elided wrt generics for store-data types,HasDatamagic and all that. (I have a vague sense that a naive impl of the trait methodadd_to_linkerwill run into HKT issues but maybe parameterizing the trait could get around it.)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!
cfallin edited a comment on PR #12698:
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:
One has to consider the separation between provider code (the implementation of an interface) and a particular world. For example the
wasi-httpcrate exports itsadd_to_linker; it knows nothing about how some particular embedding might choose to define a world that accepts two instances of its interface. So we can't bake that distinction into the traits themselves (option A) nor have statically differentadd_to_linkers (option C).At least the way I think about it, I see the "lego-block plumbing" philosophy of component composition writ large reflected down into the world-definition space here: one should be able to define a world that imports N interfaces (some of the same type) and exports M interfaces, and wire up a bunch of different modules on the host side that came from different places, plugging them in in just one place that does that wiring. Thinking of what has to be defined in each separable module hopefully crystalizes some of the choices (?).
I also see this as kind of analogous to the core Wasm function import/export universe. Interfaces today are distinguished only by their type; one can only have one of each type. That'd be akin to saying that one can only import one function of signature
(param i32) (result i32). We use stringly-typed names to allow there to be more than one, so in that sense I don't see it as worse that we would use textual labels to name the specific slots we plug instances of interfaces into, either.
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
world example-world { import first: my-interface; import second: my-interface; }becoming
struct ExampleWorldBuilder { ... } impl ExampleWorldBuilder { fn new() -> Self { ... } fn first(&mut self, impl_: impl MyInterfaceBuilder) { ... } fn second(...) { } fn build(self, linker: &mut Linker) { ... } } trait MyInterfaceBuilder { fn add_to_linker(...); }or something like that, and then
MyInterfaceBuilderis provided by wit-bindgen in the same wayadd_to_linkeris today -- details elided wrt generics for store-data types,HasDatamagic and all that. (I have a vague sense that a naive impl of the trait methodadd_to_linkerwill run into HKT issues but maybe parameterizing the trait could get around it.)One could use this strategy with today's worlds (without named interface instances) as well -- then you'd have
fn http(...), fn filesystem(...), etc for a standard WASI world. I like this better than today's "litany ofadd_to_linkercalls" approach -- the ability to forget one of those calls is a kind of dynamism/lack of static checking, too.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!
ricochet updated PR #12698.
ricochet edited PR #12698:
DO NOT MERGE until wasm-tools release with
https://github.com/bytecodealliance/wasm-tools/pull/2453
Points wasm-tools to PR branchwasmparser-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:import primary: wasi:keyvalue/store; import secondary: wasi:keyvalue/store;Guest code sees two separate namespaces with identical shapes:
let val = primary::get("my-key"); // calls the primary store let val = secondary::get("my-key"); // calls the secondary storeHost 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.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:
// 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:// 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.0Changes
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
Import codegen (crates/wit-bindgen/src/lib.rs)
- Track first-seen implements imports per
InterfaceId- One
Hosttrait per interface;generate_add_to_linkertakes
named: bool— when true, emitsname: &strparameter instead of
hardcoding the instance name- Duplicate
implementsimports: just record the label in
implements_labels, no module generationworld_add_to_linker: iterate overimplements_labelsto emit one
add_to_linkercall per label, passing label as name argument- Guard
populate_world_and_interface_optionswithentry()to avoid
overwriting link options for duplicate interfacesExport codegen (crates/wit-bindgen/src/lib.rs)
- Duplicate exports: re-export types via
pub use super::{first}::*,
generate freshGuest/GuestIndices, plus regenerate resource wrapper
structs to reference the localGuesttype- Use
name_world_key_with_itemfor export instance name lookups
ricochet commented on PR #12698:
I've implemented Option B and updated the commit message and PR description.
Right now the
world_add_to_linkergenerates a flat sequence ofadd_to_linkercalls, and forgetting one is a runtime error (component instantiation fails). A builder could make that a compile-time error. I'm working on sketching out the builder pattern now, potentially as and additive PR to this one.
tschneidereit commented on PR #12698:
One has to consider the separation between provider code (the implementation of an interface) and a particular world. For example the
wasi-httpcrate exports itsadd_to_linker; it knows nothing about how some particular embedding might choose to define a world that accepts two instances of its interface.The implements syntax would allow taking that even one step further, where the embedding itself doesn't have to make a fixed choice of how many times an interface is implemented, either.
That addresses a slightly different use case from the one discussed here so far, but one that feels at least as important to me: capability isolation within arbitrary component compositions. That use case isn't so much about different implementations of the interface, but about providing one implementation with access to per-import metadata. E.g., for the
storeinterface, it might always use the same backing implementation, but with different sets of credentials / using a different connection based on the metadata.That way, this WIT could be used by components that import an arbitrary number of
storeinstances:interface store { get: func(key: string) -> option<string>; set: func(key: string, value: string); } world my-world { import store; }E.g., components targeting all of these worlds could be loaded:
world one-store { import store; } world two-stores { import foo: store; import bar: store; } world two-stores { import foo: store; import bar: store; import baz: store; }This should be implementable with a variant on B, where wasmtime-wit-bindgen generates an additional trait for each interface,
HostWithMetadata(presumably with a nicer coat of paint), which makes the import label available to all functions. (With a default label for unlabeled imports, so the implementation doesn't have to test for that scenario everywhere.)Slightly pseudo-code-y, that could look something like this (note the single
add_to_linkercall):use wasmtime::component::ImportLabel; pub struct Store<'a> { stores: &mut Map<ImportLabel, StoreBucket>, } impl store::HostWithMetadata for Store<'_> { pub fn get(&mut self, identifier: String, label: &ImportLabel) -> Option<String> { let store = self.stores.get(label); if store.is_none { // Trap, because this should've been ensured at component load time. } store.get(identifier) } } store::add_to_linker(&mut linker)?;Note that I'm not saying this should fully replace option B: that clearly has value in itself that isn't provided by this variant. But conversely this has value that otherwise isn't available at all.
alexcrichton commented on PR #12698:
I can try to offer some more bikeshed colors perhaps on this...
One major part I think worth considering is the possibility of loading a component into a host which did not have a static enumeration of all possible names when the host was compiled. For example in this PR and in the examples being discussed the WIT is a fixed quantity which is shared between the host and the guest, but I think there's a reasonable case to be made for use cases where the guest might use kebab-names the host didn't previously know about.
A rough example of this is importing
wasi:http/clientinto a component under multiple names. For a CDN-like service you could imagine that each wasm is configured with a different set of upstream providers. This would mean that each wasm loaded has a different set of names it can import and it's distinct-per-wasm. For example:// service A uses this world world a { import upstream: wasi:http/client; import my-name: wasi:http/client; } // service B uses this world world b { import upstream: wasi:http/client; // platform default import eu-upstream: wasi:http/client; }In this situation wouldn't be able to rely on many of the strategies proposed here, and the host would in theory be unable to specify a single fixed world for bindgen that it supports. Nevertheless this also seems like an important use case from historical discussions, and would be solved along the lines of what @tschneidereit is saying.
Another concern I'd have with the current design in this PR as-is is that it's going to be difficult to wrangle the
add_to_linkerimplementations I think. The design ofadd_to_linkeris always a really delicate balance of a lot of competing concerns, but specifying an extra closure some-of-the-time is going to make this difficult to compose with other initialization strategies. I'd ideally like to avoid adding extra arguments there since trait-based resolution I feel has worked out pretty well on the Rust bindings side of things.
To get to a color of a bikeshed, what I might propose is something like this. Given this:
interface store { get: func(key: string) -> option<string>; set: func(key: string, value: string); } world my-cache { import hot-cache: store; import durable: store; }something like this would be generated, by default:
mod store { trait Host { ... } } trait MyCache { type HotCache: store::Host; type Durable: store::Host; fn hot_cache(self) -> Self::HotCache; fn durable(self) -> Self::Durable; }This would avoid the extra "projection closure" and move the logic to the implementation of
MyCache, theworld-based trait, instead. This would reuse theHosttrait so these in theory could have the exact same type forHotCacheandDurable, just configured differently, too.This still doesn't solve the problem I was outlining with runtime-determined interfaces, however. For that what I'd propose is an extension of this where an opt-in option to
bindgen!is provided:bindgen!({ // ... variable_implements: ["wasi:keyvalue/store"], }); // ... generates ... mod store { trait Host { ... } } trait MyCache { type Store: store::Host; fn lookup_store(self, name: &str) -> Self::Store; }here this is manually configured to say "accept any import of
wasi:keyvalue/storeunder any name withimplementsand funnel it intolookup_store". This has the constraint that there's only one type to implementStore: Host, but that's sort of inherent here. Thelookup_storewould enable the host to attach thenameto whatever sort of configuration it wants.The rough idea is then the
Linker-assembling methods are a bit fancier in thevariable_implementsconfiguration, reflecting on aComponentloaded at runtime and inserting into the import map (or export map). This would dynamically populate aLinkerwith the names being used by that component.
In theory I think this would solve the problem at hand, still solve @cfallin's concerns of having a single
Hosttrait by default, and still leave the possibility open in the future to further configuration and such.
Last updated: Mar 23 2026 at 16:19 UTC