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(()) + } +}