From 330d680dee474ef3852dec80eb83876a03951dd8 Mon Sep 17 00:00:00 2001 From: Alex Crichton Date: Fri, 29 May 2026 15:00:05 -0700 Subject: [PATCH] Finish bindings generation for `(implements ..)` This commit is the final step in finishing up the bindings generation story for `bindgen!` w.r.t. the `(implement ...)` component model feature. Namely `bindgen!` now has the option `named_imports: { ... }` which generates a new top-level `named_imports` module with nested modules within it. These nested modules are similar to the default-generated ones except that they additionally take an extra "id" parameter in all trait methods. The `add_to_linker` function additionally takes both a `&Component` and a `lookup` function. Putting all this together the expected flow is: * Host decide they want to accept any number of `import a: b:c/d;` from components at runtime. Bindings are generated with `named_imports: { ... }` in the `bindgen!` invocation. * Hosts switch from linker-per-engine to instead using a linker-per-component. After the typical population of items within the `Linker` next the host uses the `named_imports::b::c::d::add_to_linker`, for example. Here the host performs lookup from whatever string was found in the component to an ID key that the host knows about. Upon success this key will be closed over in all `add_to_linker` functions. Upon failure the `add_to_linker` closure fails. * This per-component linker is used to instantiate and create instances as usual. At runtime when a named import is invoked the closed-over ID is passed to the trait implementation as a parameter. Hosts can thus disambiguate through which interface the component made a call through. Some light tests are added throughout this showcasing the various capabilities. This notably added a new `ComponentExtern::is_implements` method to peform semver-matching automatically so hosts can generate bindings with one version and still accept `implements` with other slightly different but compatible versions. --- crates/component-macro/src/bindgen.rs | 11 + crates/component-macro/tests/codegen.rs | 65 ++++ crates/environ/src/component/names.rs | 2 +- crates/wasmtime/src/runtime/component/mod.rs | 33 ++ .../wasmtime/src/runtime/component/types.rs | 26 +- crates/wit-bindgen/src/lib.rs | 289 +++++++++++++++--- tests/all/component_model/aot.rs | 12 + tests/all/component_model/bindgen.rs | 155 ++++++++++ 8 files changed, 547 insertions(+), 46 deletions(-) diff --git a/crates/component-macro/src/bindgen.rs b/crates/component-macro/src/bindgen.rs index 3f742c6e9267..b651b867f34e 100644 --- a/crates/component-macro/src/bindgen.rs +++ b/crates/component-macro/src/bindgen.rs @@ -135,6 +135,7 @@ impl Parse for Config { opts.only_interfaces = true; } Opt::With(val) => opts.with.extend(val), + Opt::NamedImports(val) => opts.named_imports.extend(val), Opt::AdditionalDerives(paths) => { opts.additional_derive_attributes = paths .into_iter() @@ -252,6 +253,7 @@ mod kw { syn::custom_keyword!(ownership); syn::custom_keyword!(interfaces); syn::custom_keyword!(with); + syn::custom_keyword!(named_imports); syn::custom_keyword!(except_imports); syn::custom_keyword!(only_imports); syn::custom_keyword!(additional_derives); @@ -278,6 +280,7 @@ enum Opt { Ownership(Ownership), Interfaces(syn::LitStr), With(HashMap), + NamedImports(HashMap), AdditionalDerives(Vec), Stringify(bool), SkipMutForwardingImpls(bool), @@ -383,6 +386,14 @@ impl Parse for Opt { let fields: Punctuated<(String, String), Token![,]> = contents.parse_terminated(with_field_parse, Token![,])?; Ok(Opt::With(HashMap::from_iter(fields))) + } else if l.peek(kw::named_imports) { + input.parse::()?; + input.parse::()?; + let contents; + let _lbrace = braced!(contents in input); + let fields: Punctuated<(String, String), Token![,]> = + contents.parse_terminated(with_field_parse, Token![,])?; + Ok(Opt::NamedImports(HashMap::from_iter(fields))) } else if l.peek(kw::additional_derives) { input.parse::()?; input.parse::()?; diff --git a/crates/component-macro/tests/codegen.rs b/crates/component-macro/tests/codegen.rs index a45806c56d10..4b2eed85abe2 100644 --- a/crates/component-macro/tests/codegen.rs +++ b/crates/component-macro/tests/codegen.rs @@ -820,3 +820,68 @@ mod anyhow_with_custom_error { struct MyCustomError; } + +mod named_imports { + + mod sync { + wasmtime::component::bindgen!({ + inline: " + package foo:foo; + + interface handler { + handle: func(req: u32) -> u32; + ping: func(); + } + + world the-world { + import handler; + } + ", + named_imports: { + "foo:foo/handler": String, + }, + }); + + struct MyHost; + + // The normal trait is generated as usual... + impl foo::foo::handler::Host for MyHost { + fn handle(&mut self, req: u32) -> u32 { + req + } + fn ping(&mut self) {} + } + + // ...and the named-imports trait has the extra id parameter. + impl named_imports::foo::foo::handler::Host for MyHost { + fn handle(&mut self, _id: String, req: u32) -> u32 { + req + } + fn ping(&mut self, _id: String) {} + } + } + + mod async_store { + #[derive(Clone)] + pub struct MyId(u32); + + wasmtime::component::bindgen!({ + inline: " + package foo:foo; + + interface handler { + handle: func(req: u32) -> u32; + ping: func(); + } + + world the-world { + import handler; + } + ", + named_imports: { + "foo:foo/handler": MyId, + }, + imports: { default: async | store }, + }); + } +} diff --git a/crates/environ/src/component/names.rs b/crates/environ/src/component/names.rs index 963fb00b074c..64c564d42b5f 100644 --- a/crates/environ/src/component/names.rs +++ b/crates/environ/src/component/names.rs @@ -290,7 +290,7 @@ impl NameMapIntern for StringPool { /// This alternate lookup key is intended to serve the purpose where a /// semver-compatible definition can be located, if one is defined, at perhaps /// either a newer or an older version. -fn alternate_lookup_key(name: &str) -> Option<(&str, Version)> { +pub fn alternate_lookup_key(name: &str) -> Option<(&str, Version)> { let at = name.find('@')?; let version_string = &name[at + 1..]; let version = Version::parse(version_string).ok()?; diff --git a/crates/wasmtime/src/runtime/component/mod.rs b/crates/wasmtime/src/runtime/component/mod.rs index 093ada9cddc5..31264342186f 100644 --- a/crates/wasmtime/src/runtime/component/mod.rs +++ b/crates/wasmtime/src/runtime/component/mod.rs @@ -447,6 +447,39 @@ pub(crate) use self::store::ComponentStoreData; /// "wasi:filesystem/types.descriptor": MyDescriptorType, /// }, /// +/// // Generate an additional set of "named imports" bindings for the listed +/// // interfaces, used together with the component model's +/// // `(implements "...")` annotation. +/// // +/// // For each interface listed here an extra `Host` trait is generated +/// // under a top-level `named_imports` module (mirroring the interface's +/// // normal module path) whose methods each take an additional first +/// // argument: a reference to the host-chosen "id" type given as the value +/// // (here `MyHandlerId`). Alongside the trait a reflection-based +/// // `add_to_linker` is generated: +/// // +/// // ```ignore +/// // fn add_to_linker( +/// // linker: &mut Linker, +/// // component: &Component, +/// // lookup: impl FnMut(&str) -> Result, +/// // host_getter: fn(&mut T) -> D::Data<'_>, +/// // ) -> Result<()>; +/// // ``` +/// // +/// // This inspects `component`'s imports, and for each one annotated with +/// // `(implements "wasi:http/handler")` calls `lookup` with the import's +/// // name to obtain an id. That id is then cloned into each linker closure +/// // and passed as the first argument to every method call, letting a +/// // single `Host` implementation distinguish between multiple imports +/// // of the same interface. +/// // +/// // The id type must be `Clone + Send + Sync + 'static`. Interfaces that +/// // define a resource are not supported here and cause a compile error. +/// named_imports: { +/// "wasi:http/handler": MyHandlerId, +/// }, +/// /// // Additional derive attributes to include on generated types (structs or enums). /// // /// // These are deduplicated and attached in a deterministic order. diff --git a/crates/wasmtime/src/runtime/component/types.rs b/crates/wasmtime/src/runtime/component/types.rs index d559aae9ad0f..94b0ce023107 100644 --- a/crates/wasmtime/src/runtime/component/types.rs +++ b/crates/wasmtime/src/runtime/component/types.rs @@ -13,7 +13,7 @@ use wasmtime_environ::component::{ TypeComponentInstanceIndex, TypeDef, TypeEnumIndex, TypeFlagsIndex, TypeFuncIndex, TypeFutureIndex, TypeFutureTableIndex, TypeListIndex, TypeMapIndex, TypeModuleIndex, TypeOptionIndex, TypeRecordIndex, TypeResourceTable, TypeResourceTableIndex, TypeResultIndex, - TypeStreamIndex, TypeStreamTableIndex, TypeTupleIndex, TypeVariantIndex, + TypeStreamIndex, TypeStreamTableIndex, TypeTupleIndex, TypeVariantIndex, alternate_lookup_key, }; pub use crate::component::resources::ResourceType; @@ -1135,6 +1135,30 @@ impl<'a> ComponentExtern<'a> { ty: ComponentItem::from(engine, &env.ty, instance_ty), } } + + /// Returns whether this item is tagged with `(implements "..")` with an + /// interface that's compatible with `name`. + /// + /// This function will return `false` if `(implements "...")` is not + /// present. If it is present, and it's equal to `name`, then `true` is + /// returned. Failing that, this attempts to perform version-matching to see + /// if a compatible version of this item is implemented. For example if + /// `(implements "a:b/c@1.1.0")` is specified then this will return `true` + /// for `a:b/c@1.0.0` and `a:b/c@1.2.0` as well. + pub fn is_implements(&self, name: &str) -> bool { + let implements = match self.implements { + Some(s) => s, + None => return false, + }; + if name == implements { + return true; + } + + match (alternate_lookup_key(implements), alternate_lookup_key(name)) { + (Some((alt_implements, _)), Some((alt_name, _))) => alt_implements == alt_name, + _ => false, + } + } } /// Type of an item contained within the component diff --git a/crates/wit-bindgen/src/lib.rs b/crates/wit-bindgen/src/lib.rs index b7add61260e8..4eec0f19dc00 100644 --- a/crates/wit-bindgen/src/lib.rs +++ b/crates/wit-bindgen/src/lib.rs @@ -80,6 +80,8 @@ struct Wasmtime { // Track the with options that were used. Remapped interfaces provided via `with` // are required to be used. used_with_opts: HashSet, + // Modules generated for `named_imports` options. + named_import_modules: Vec<(String, InterfaceName)>, world_link_options: LinkOptionsBuilder, interface_link_options: HashMap, } @@ -94,7 +96,7 @@ struct ImportInterface { #[derive(Default)] struct Exports { fields: BTreeMap, - modules: Vec<(InterfaceId, String, InterfaceName)>, + modules: Vec<(String, InterfaceName)>, funcs: Vec, } @@ -143,6 +145,10 @@ pub struct Opts { /// TODO: is there a better type to use for the value of this map? pub with: HashMap, + /// Interfaces for which to generate an additional set of "named imports" + /// bindings. + pub named_imports: HashMap, + /// Additional derive attributes to add to generated types. If using in a CLI, this flag can be /// specified multiple times to add multiple attributes. /// @@ -226,6 +232,13 @@ impl Opts { } } +#[derive(Copy, Clone, PartialEq)] +enum InterfaceKind { + Import, + Export, + Named, +} + impl Wasmtime { fn populate_world_and_interface_options(&mut self, resolve: &Resolve, world: WorldId) { self.world_link_options.add_world(resolve, &world); @@ -241,16 +254,19 @@ impl Wasmtime { } } } - fn name_interface( + + fn generate_interface_name( &mut self, resolve: &Resolve, id: InterfaceId, name: &WorldKey, - is_export: bool, - ) -> bool { + interface_kind: InterfaceKind, + ) -> Vec { let mut path = Vec::new(); - if is_export { - path.push("exports".to_string()); + match interface_kind { + InterfaceKind::Import => {} + InterfaceKind::Export => path.push("exports".to_string()), + InterfaceKind::Named => path.push("named_imports".to_string()), } match name { WorldKey::Name(name) => { @@ -264,13 +280,26 @@ impl Wasmtime { path.push(to_rust_ident(iface.name.as_ref().unwrap())); } } - let entry = if let Some(name_at_root) = self.lookup_replacement(resolve, name, None) { + path + } + + fn name_interface( + &mut self, + resolve: &Resolve, + id: InterfaceId, + name: &WorldKey, + interface_kind: InterfaceKind, + ) -> bool { + let local_path = self.generate_interface_name(resolve, id, name, interface_kind); + let entry = if !matches!(interface_kind, InterfaceKind::Named) + && let Some(name_at_root) = self.lookup_replacement(resolve, name, None) + { InterfaceName::Remapped { name_at_root, - local_path: path, + local_path, } } else { - InterfaceName::Path(path) + InterfaceName::Path(local_path) }; let remapped = matches!(entry, InterfaceName::Remapped { .. }); @@ -387,6 +416,17 @@ impl Wasmtime { *v = name; } + // Similarly bring the `named_imports` id types into scope under an + // anonymous name at the root so they can be referenced from the deeply + // nested `named_imports` module. + let mut named_imports = self.opts.named_imports.iter_mut().collect::>(); + named_imports.sort(); + for (i, (_k, v)) in named_imports.into_iter().enumerate() { + let name = format!("__named_import_name{i}"); + uwriteln!(self.src, "#[doc(hidden)]\npub use {v} as {name};"); + *v = name; + } + let world = &resolve.worlds[id]; for (name, import) in world.imports.iter() { if !self.opts.only_interfaces || matches!(import, WorldItem::Interface { .. }) { @@ -399,9 +439,79 @@ impl Wasmtime { self.export(resolve, name, export); } } + self.generate_named_imports(resolve)?; self.finish(resolve, id) } + /// Generates the extra "named imports" bindings requested via the + /// `named_imports` configuration option. + /// + /// For each configured interface this generates, into a body stored in + /// `named_import_modules`, a `Host`/`HostWithStore` trait whose methods take + /// an extra `&Id` parameter alongside a reflection-based `add_to_linker`. + /// These are emitted underneath a top-level `named_imports` module by + /// `finish`. + fn generate_named_imports(&mut self, resolve: &Resolve) -> anyhow::Result<()> { + // For all named imports see what interface that lines up with in the + // `Resolve` which will have bindings generated. + 'outer: for (interface_name, id_type) in self.opts.named_imports.clone() { + for (id, _iface) in resolve.interfaces.iter() { + for (key, projection) in lookup_keys( + resolve, + &WorldKey::Interface(id), + LookupItem::InterfaceNoPop, + ) { + assert!(projection.is_empty()); + if key == interface_name { + self.generate_named_import(resolve, id, &interface_name, &id_type)?; + continue 'outer; + } + } + } + + bail!("named imports key {interface_name:?} not found") + } + + Ok(()) + } + + fn generate_named_import( + &mut self, + resolve: &Resolve, + id: InterfaceId, + named_import_key: &str, + id_type: &str, + ) -> anyhow::Result<()> { + // Resources are not supported for named imports just yet, it's a bit + // weird with the resource traits. + if get_resources(resolve, id).next().is_some() { + bail!( + "the interface {named_import_key:?} was specified in \ + `named_imports` but defines a resource, which is not \ + supported" + ); + } + + let key = WorldKey::Interface(id); + let mut generator = InterfaceGenerator::new(self, resolve); + generator.current_interface = Some((id, &key, InterfaceKind::Named)); + let path_to_root = generator.path_to_root(); + generator.named_import_id = Some(format!("{path_to_root}{id_type}")); + let wt = generator.generator.wasmtime_path(); + generator.src.push_str(&format!( + "#[allow(unused_imports)] use {wt}::component::__internal::Box;\n" + )); + let key_name = resolve.name_world_key(&key); + generator.generate_add_to_linker(id, &key_name); + let body = String::from(mem::take(&mut generator.src)); + let interface_name = resolve.interfaces[id].name.as_ref().unwrap(); + let body = format!("pub mod {interface_name} {{\n{body}\n}}"); + let path = self.generate_interface_name(resolve, id, &key, InterfaceKind::Named); + self.named_import_modules + .push((body, InterfaceName::Path(path))); + Ok(()) + } + fn import(&mut self, resolve: &Resolve, name: &WorldKey, item: &WorldItem) { match item { WorldItem::Function(func) => { @@ -448,7 +558,7 @@ impl Wasmtime { ) { let mut generator = InterfaceGenerator::new(self, resolve); - generator.current_interface = Some((id, name, false)); + generator.current_interface = Some((id, name, InterfaceKind::Import)); let snake = to_rust_ident(&match name { WorldKey::Name(s) => s.to_snake_case(), WorldKey::Interface(id) => resolve.interfaces[*id] @@ -457,7 +567,10 @@ impl Wasmtime { .unwrap() .to_snake_case(), }); - let module = if generator.generator.name_interface(resolve, id, name, false) { + let module = if generator + .generator + .name_interface(resolve, id, name, InterfaceKind::Import) + { // If this interface is remapped then that means that it was // provided via the `with` key in the bindgen configuration. // That means that bindings generation is skipped here. To @@ -559,8 +672,10 @@ impl Wasmtime { } WorldItem::Type { .. } => unreachable!(), WorldItem::Interface { id, .. } => { - generator.generator.name_interface(resolve, *id, name, true); - generator.current_interface = Some((*id, name, true)); + generator + .generator + .name_interface(resolve, *id, name, InterfaceKind::Export); + generator.current_interface = Some((*id, name, InterfaceKind::Export)); generator.types(*id); let struct_name = "Guest"; let iface = &resolve.interfaces[*id]; @@ -719,7 +834,7 @@ pub fn new<_T>( }; self.exports .modules - .push((*id, module, self.interface_names[id].clone())); + .push((module, self.interface_names[id].clone())); let (path, method_name) = match pkgname { Some(pkgname) => ( @@ -1039,13 +1154,16 @@ impl<_T: Send + 'static> {camel}Pre<_T> {{ self.emit_modules( imports .into_iter() - .map(|(id, i)| (id, i.contents, i.name)) + .map(|(_, i)| (i.contents, i.name)) .collect(), ); let exports = mem::take(&mut self.exports.modules); self.emit_modules(exports); + let named_imports = mem::take(&mut self.named_import_modules); + self.emit_modules(named_imports); + let mut src = mem::take(&mut self.src); if self.opts.rustfmt { let mut child = Command::new("rustfmt") @@ -1074,14 +1192,14 @@ impl<_T: Send + 'static> {camel}Pre<_T> {{ Ok(src.into()) } - fn emit_modules(&mut self, modules: Vec<(InterfaceId, String, InterfaceName)>) { + fn emit_modules(&mut self, modules: Vec<(String, InterfaceName)>) { #[derive(Default)] struct Module { submodules: BTreeMap, contents: Vec, } let mut map = Module::default(); - for (_, module, name) in modules { + for (module, name) in modules { let path = match name { InterfaceName::Remapped { local_path, .. } => local_path, InterfaceName::Path(path) => path, @@ -1622,8 +1740,11 @@ struct InterfaceGenerator<'a> { src: Source, generator: &'a mut Wasmtime, resolve: &'a Resolve, - current_interface: Option<(InterfaceId, &'a WorldKey, bool)>, + current_interface: Option<(InterfaceId, &'a WorldKey, InterfaceKind)>, all_func_flags: FunctionFlags, + + /// The type that represents the embedder-chosen "id" for named imports. + named_import_id: Option, } impl<'a> InterfaceGenerator<'a> { @@ -1634,13 +1755,14 @@ impl<'a> InterfaceGenerator<'a> { resolve, current_interface: None, all_func_flags: FunctionFlags::empty(), + named_import_id: None, } } fn types_imported(&self) -> bool { match self.current_interface { - Some((_, _, is_export)) => !is_export, - None => true, + Some((_, _, InterfaceKind::Export)) => false, + _ => true, } } @@ -2401,11 +2523,19 @@ impl<'a> InterfaceGenerator<'a> { "" }; + // For named imports the per-instance helper additionally accepts the + // host-chosen `id` (by value, cloned into each closure). + let id_param = match &self.named_import_id { + Some(named) => format!("id: {named},"), + None => String::new(), + }; + uwriteln!( self.src, " pub fn add_to_linker_instance( inst: &mut {wt}::component::LinkerInstance<'_, T>, + {id_param} {options_param} host_getter: fn(&mut T) -> D::Data<'_>, ) -> {wt}::Result<()> @@ -2435,24 +2565,71 @@ impl<'a> InterfaceGenerator<'a> { uwriteln!(self.src, "Ok(())"); uwriteln!(self.src, "}}"); - uwriteln!( - self.src, + match &self.named_import_id { + Some(id_ty) => { + let (id, _, _) = self.current_interface.unwrap(); + let wit_name = self.resolve.id_of(id).unwrap(); + uwriteln!( + self.src, + " +pub fn add_to_linker( + linker: &mut {wt}::component::Linker, + component: &{wt}::component::Component, + mut lookup: impl FnMut(&str) -> {wt}::Result<{id_ty}>, + {options_param} + host_getter: fn(&mut T) -> D::Data<'_>, +) -> {wt}::Result<()> + where + D: HostWithStore, + for<'a> D::Data<'a>: {sync_bounds}, + T: 'static {opt_t_send_bound}, +{{ + // Collect matching imports up front: iterating `imports` + // borrows the engine (via `linker`) immutably while + // `linker.instance(..)` needs a mutable borrow. + let engine = linker.engine().clone(); + let component_ty = component.component_type(); + let mut matched = {wt}::component::__internal::Vec::new(); + for (name, item) in component_ty.imports(&engine) {{ + if item.is_implements({wit_name:?}) {{ + matched.push((name, lookup(name)?)); + }} + }} + for (name, id) in matched {{ + let mut inst = linker.instance(name)?; + add_to_linker_instance::( + &mut inst, + id, + {options_param_forward} + host_getter, + )?; + }} + Ok(()) +}} " - pub fn add_to_linker( - linker: &mut {wt}::component::Linker, - {options_param} - host_getter: fn(&mut T) -> D::Data<'_>, - ) -> {wt}::Result<()> - where - D: HostWithStore, - for<'a> D::Data<'a>: {sync_bounds}, - T: 'static {opt_t_send_bound}, - {{ - let mut inst = linker.instance(\"{name}\")?; - add_to_linker_instance(&mut inst, {options_param_forward} host_getter) - }} + ); + } + None => { + uwriteln!( + self.src, + " +pub fn add_to_linker( + linker: &mut {wt}::component::Linker, + {options_param} + host_getter: fn(&mut T) -> D::Data<'_>, +) -> {wt}::Result<()> + where + D: HostWithStore, + for<'a> D::Data<'a>: {sync_bounds}, + T: 'static {opt_t_send_bound}, +{{ + let mut inst = linker.instance(\"{name}\")?; + add_to_linker_instance(&mut inst, {options_param_forward} host_getter) +}} " - ); + ); + } + } } fn import_resource_drop_flags(&mut self, name: &str) -> FunctionFlags { @@ -2483,7 +2660,13 @@ impl<'a> InterfaceGenerator<'a> { }, func.name ); + if self.named_import_id.is_some() { + self.src.push_str("{ let id = id.clone(); "); + } self.generate_guest_import_closure(owner, func, flags); + if self.named_import_id.is_some() { + self.src.push_str("}\n"); + } uwriteln!(self.src, ")?;"); gate.close(&mut self.src); } @@ -2545,6 +2728,9 @@ impl<'a> InterfaceGenerator<'a> { func.name, ); } + if self.named_import_id.is_some() { + self.src.push_str("let id = id.clone(); "); + } if flags.contains(FunctionFlags::ASYNC) { let ctor = if flags.contains(FunctionFlags::STORE) { @@ -2628,6 +2814,10 @@ impl<'a> InterfaceGenerator<'a> { uwrite!(self.src, "let r = {host_trait}::{func_name}(host, "); } + if self.named_import_id.is_some() { + self.src.push_str("id, "); + } + for (i, _) in func.params.iter().enumerate() { uwrite!(self.src, "arg{},", i); } @@ -2716,6 +2906,9 @@ impl<'a> InterfaceGenerator<'a> { } else { self.push_str("(&mut self, "); } + if let Some(id) = &self.named_import_id { + uwrite!(self.src, "id: {id}, "); + } self.generate_function_params(func); self.push_str(")"); self.push_str(" -> "); @@ -2937,7 +3130,11 @@ impl<'a> InterfaceGenerator<'a> { fn path_to_root(&self) -> String { let mut path_to_root = String::new(); - if let Some((_, key, is_export)) = self.current_interface { + if let Some((_, key, kind)) = self.current_interface { + match kind { + InterfaceKind::Export | InterfaceKind::Named => path_to_root.push_str("super::"), + InterfaceKind::Import => {} + } match key { WorldKey::Name(_) => { path_to_root.push_str("super::"); @@ -2946,9 +3143,6 @@ impl<'a> InterfaceGenerator<'a> { path_to_root.push_str("super::super::super::"); } } - if is_export { - path_to_root.push_str("super::"); - } } path_to_root } @@ -3145,6 +3339,9 @@ fn convert_{snake}(&mut self, err: {root}{custom_name}) -> "{trait_name}::{}(*self,", rust_function_name(func) ); + if self.named_import_id.is_some() { + self.src.push_str("id,"); + } for param in func.params.iter() { uwrite!(self.src, "{},", to_rust_ident(¶m.name)); } @@ -3233,8 +3430,12 @@ impl<'a> RustGenerator<'a> for InterfaceGenerator<'a> { } fn path_to_interface(&self, interface: InterfaceId) -> Option { - if let Some((cur, _, _)) = self.current_interface { - if cur == interface { + if let Some((cur, _, kind)) = self.current_interface { + // If `interface` is `cur`, then we're in the same module and need + // to path to the interface. If we're generating for a named import, + // however, that's not true since the types live elsewhere, so skip + // that case. + if cur == interface && kind != InterfaceKind::Named { return None; } } @@ -3262,9 +3463,9 @@ impl<'a> RustGenerator<'a> for InterfaceGenerator<'a> { } fn is_imported_interface(&self, interface: InterfaceId) -> bool { - if let Some((cur, _, is_export)) = self.current_interface { + if let Some((cur, _, kind)) = self.current_interface { if cur == interface { - return !is_export; + return kind != InterfaceKind::Export; } } self.generator.import_interfaces.contains_key(&interface) diff --git a/tests/all/component_model/aot.rs b/tests/all/component_model/aot.rs index 53adee2048a7..45207d2010fd 100644 --- a/tests/all/component_model/aot.rs +++ b/tests/all/component_model/aot.rs @@ -221,6 +221,8 @@ fn implements_shows_up() -> Result<()> { (component (import "a" (implements "a1:b1/c1") (instance $a)) (export "b" (implements "a2:b2/c2") (instance $a)) + + (import "v" (implements "a:b/c@1.2.0") (instance)) ) "#, )?; @@ -229,9 +231,19 @@ fn implements_shows_up() -> Result<()> { let mut imports = ty.imports(&engine); let (_, a) = imports.next().unwrap(); assert_eq!(a.implements.as_deref(), Some("a1:b1/c1")); + assert!(a.is_implements("a1:b1/c1")); + assert!(!a.is_implements("a:b/c")); + assert!(!a.is_implements("a1:b1/c1@1.0.0")); let mut exports = ty.exports(&engine); let (_, b) = exports.next().unwrap(); assert_eq!(b.implements.as_deref(), Some("a2:b2/c2")); + let (_, a) = imports.next().unwrap(); + assert_eq!(a.implements.as_deref(), Some("a:b/c@1.2.0")); + assert!(!a.is_implements("a:b/c")); + assert!(a.is_implements("a:b/c@1.2.0")); + assert!(a.is_implements("a:b/c@1.3.0")); + assert!(a.is_implements("a:b/c@1.0.0")); + Ok(()) } diff --git a/tests/all/component_model/bindgen.rs b/tests/all/component_model/bindgen.rs index 27feccc45b1b..37939515d750 100644 --- a/tests/all/component_model/bindgen.rs +++ b/tests/all/component_model/bindgen.rs @@ -1136,3 +1136,158 @@ mod implements { Ok(()) } } + +mod named_imports { + use super::*; + use std::collections::HashMap; + use wasmtime::component::HasSelf; + + /// Host-chosen id type threaded into every method call. + #[derive(Clone)] + pub struct MyId(u32); + + wasmtime::component::bindgen!({ + inline: " + package demo:pkg; + + interface store { + get: func(key: u32) -> u32; + set: func(key: u32, value: u32); + } + + world cache { + import store; + + export run: func(); + } + ", + named_imports: { + "demo:pkg/store": MyId, + }, + }); + + // A component which imports the `store` interface twice, under arbitrary + // names `a` and `b`, each annotated as implementing `demo:pkg/store`. The + // exported `run` writes through each import and reads the value back, so a + // mix-up in id routing would cause a trap. + const COMPONENT: &str = r#" + (component + (import "a" (implements "demo:pkg/store") (instance $a + (export "get" (func (param "key" u32) (result u32))) + (export "set" (func (param "key" u32) (param "value" u32))) + )) + (import "b" (implements "demo:pkg/store") (instance $b + (export "get" (func (param "key" u32) (result u32))) + (export "set" (func (param "key" u32) (param "value" u32))) + )) + + (core module $m + (import "a" "get" (func $a-get (param i32) (result i32))) + (import "a" "set" (func $a-set (param i32 i32))) + (import "b" "get" (func $b-get (param i32) (result i32))) + (import "b" "set" (func $b-set (param i32 i32))) + + (func (export "run") + (call $a-set (i32.const 0) (i32.const 10)) + (call $b-set (i32.const 0) (i32.const 20)) + + (if (i32.ne (call $a-get (i32.const 0)) (i32.const 10)) + (then unreachable)) + (if (i32.ne (call $b-get (i32.const 0)) (i32.const 20)) + (then unreachable)) + ) + ) + (core func $a-get-l (canon lower (func $a "get"))) + (core func $a-set-l (canon lower (func $a "set"))) + (core func $b-get-l (canon lower (func $b "get"))) + (core func $b-set-l (canon lower (func $b "set"))) + (core instance $i (instantiate $m + (with "a" (instance + (export "get" (func $a-get-l)) + (export "set" (func $a-set-l)) + )) + (with "b" (instance + (export "get" (func $b-get-l)) + (export "set" (func $b-set-l)) + )) + )) + + (func (export "run") (canon lift (core func $i "run"))) + ) + "#; + + fn implements_engine() -> Engine { + let mut config = Config::new(); + config.wasm_component_model(true); + config.wasm_component_model_implements(true); + Engine::new(&config).unwrap() + } + + #[derive(Default)] + struct MyHost { + kv: HashMap<(u32, u32), u32>, + calls: Vec<(u32, &'static str)>, + } + + impl named_imports::demo::pkg::store::Host for MyHost { + fn get(&mut self, id: MyId, key: u32) -> u32 { + self.calls.push((id.0, "get")); + *self.kv.get(&(id.0, key)).unwrap() + } + + fn set(&mut self, id: MyId, key: u32, value: u32) { + self.calls.push((id.0, "set")); + self.kv.insert((id.0, key), value); + } + } + + #[test] + fn ids_are_threaded_through() -> Result<()> { + let engine = implements_engine(); + let component = Component::new(&engine, COMPONENT)?; + let mut linker = Linker::new(&engine); + named_imports::demo::pkg::store::add_to_linker::<_, HasSelf>( + &mut linker, + &component, + |name| match name { + "a" => Ok(MyId(1)), + "b" => Ok(MyId(2)), + other => wasmtime::bail!("unexpected import: {other}"), + }, + |s| s, + )?; + let mut store = Store::new(&engine, MyHost::default()); + let cache = Cache::instantiate(&mut store, &component, &linker)?; + + cache.call_run(&mut store)?; + + let calls = &store.data().calls; + assert!(calls.contains(&(1, "set")), "calls: {calls:?}"); + assert!(calls.contains(&(2, "set")), "calls: {calls:?}"); + assert!(calls.contains(&(1, "get")), "calls: {calls:?}"); + assert!(calls.contains(&(2, "get")), "calls: {calls:?}"); + Ok(()) + } + + #[test] + fn lookup_error_is_propagated() -> Result<()> { + let engine = implements_engine(); + let component = Component::new(&engine, COMPONENT)?; + let mut linker = Linker::new(&engine); + let result = named_imports::demo::pkg::store::add_to_linker::<_, HasSelf>( + &mut linker, + &component, + |name| match name { + "a" => Ok(MyId(1)), + _ => wasmtime::bail!("bad import"), + }, + |s| s, + ); + let err = result.expect_err("expected lookup error to propagate"); + assert!( + err.to_string().contains("bad import"), + "unexpected error: {err:?}" + ); + Ok(()) + } +}