From 2ad057d735edc43f8ba89428d483f2b2430c1068 Mon Sep 17 00:00:00 2001 From: Alex Crichton Date: Fri, 15 Sep 2023 16:40:22 -0500 Subject: [PATCH] Implement support for exported resources in `bindgen!` (#7050) * Implement support for exported resources in `bindgen!` This commit updates the `wasmtime::component::bindgen!` to support exported resources. Exported resources are themselves always modeled as `ResourceAny` at runtime and are required to perform dynamic type checks to ensure that the right type of resource is passed in. Exported resources have their own `GuestXXX` structure exported to namespace all of their methods. Otherwise, like imports, there's not a whole lot of special treatment of exported resources. * Work around `heck` issue Looks like `to_snake_case` behaves differently if the `unicode` feature is enabled or not so bypass that entirely. --- .../tests/codegen/resources-export.wit | 43 ++++ crates/wit-bindgen/src/lib.rs | 221 +++++++++++------ tests/all/component_model/bindgen.rs | 228 ++++++++++++++++++ 3 files changed, 422 insertions(+), 70 deletions(-) create mode 100644 crates/component-macro/tests/codegen/resources-export.wit diff --git a/crates/component-macro/tests/codegen/resources-export.wit b/crates/component-macro/tests/codegen/resources-export.wit new file mode 100644 index 000000000000..f0e943671c16 --- /dev/null +++ b/crates/component-macro/tests/codegen/resources-export.wit @@ -0,0 +1,43 @@ +package foo:foo + +world w { + export simple-export + export export-using-import + + export export-using-export1 + export export-using-export2 +} + +interface simple-export { + resource a { + constructor() + static-a: static func() -> u32 + method-a: func() -> u32 + } +} + +interface export-using-import { + use transitive-import.{y} + resource a { + constructor(y: y) + static-a: static func() -> y + method-a: func(y: y) -> y + } +} + +interface transitive-import { + resource y +} + +interface export-using-export1 { + resource a { + constructor() + } +} + +interface export-using-export2 { + use export-using-export1.{a} + resource b { + constructor(a: a) + } +} diff --git a/crates/wit-bindgen/src/lib.rs b/crates/wit-bindgen/src/lib.rs index 21ae6367b489..2a0d6d68b8aa 100644 --- a/crates/wit-bindgen/src/lib.rs +++ b/crates/wit-bindgen/src/lib.rs @@ -169,7 +169,13 @@ impl Opts { } impl Wasmtime { - fn name_interface(&mut self, resolve: &Resolve, id: InterfaceId, name: &WorldKey) -> bool { + fn name_interface( + &mut self, + resolve: &Resolve, + id: InterfaceId, + name: &WorldKey, + is_export: bool, + ) -> bool { let with_name = resolve.name_world_key(name); let entry = if let Some(remapped_path) = self.opts.with.get(&with_name) { let name = format!("__with_name{}", self.with_name_counter); @@ -193,6 +199,11 @@ impl Wasmtime { ) } }; + let path = if is_export { + format!("exports::{path}") + } else { + path + }; InterfaceName { remapped: false, path, @@ -239,7 +250,7 @@ impl Wasmtime { } WorldItem::Interface(id) => { gen.gen.interface_last_seen_as_import.insert(*id, true); - if gen.gen.name_interface(resolve, *id, name) { + if gen.gen.name_interface(resolve, *id, name, false) { return; } gen.current_interface = Some((*id, name, false)); @@ -301,7 +312,7 @@ impl Wasmtime { assert!(gen.src.is_empty()); self.exports.funcs.push(body); ( - func.name.to_snake_case(), + func_field_name(resolve, func), "wasmtime::component::Func".to_string(), getter, ) @@ -309,7 +320,7 @@ impl Wasmtime { WorldItem::Type(_) => unreachable!(), WorldItem::Interface(id) => { gen.gen.interface_last_seen_as_import.insert(*id, false); - gen.gen.name_interface(resolve, *id, name); + gen.gen.name_interface(resolve, *id, name, true); gen.current_interface = Some((*id, name, true)); gen.types(*id); let iface = &resolve.interfaces[*id]; @@ -320,18 +331,11 @@ impl Wasmtime { let camel = to_rust_upper_camel_case(iface_name); uwriteln!(gen.src, "pub struct {camel} {{"); for (_, func) in iface.functions.iter() { - match func.kind { - FunctionKind::Freestanding => { - uwriteln!( - gen.src, - "{}: wasmtime::component::Func,", - func.name.to_snake_case() - ); - } - // Resource methods are handled separately in - // `type_resource`. - _ => {} - } + uwriteln!( + gen.src, + "{}: wasmtime::component::Func,", + func_field_name(resolve, func) + ); } uwriteln!(gen.src, "}}"); @@ -346,16 +350,9 @@ impl Wasmtime { ); let mut fields = Vec::new(); for (_, func) in iface.functions.iter() { - match func.kind { - FunctionKind::Freestanding => { - let (name, getter) = gen.extract_typed_function(func); - uwriteln!(gen.src, "let {name} = {getter};"); - fields.push(name); - } - // Resource methods are handled separately in - // `type_resource`. - _ => {} - } + let (name, getter) = gen.extract_typed_function(func); + uwriteln!(gen.src, "let {name} = {getter};"); + fields.push(name); } uwriteln!(gen.src, "Ok({camel} {{"); for name in fields { @@ -363,18 +360,46 @@ impl Wasmtime { } uwriteln!(gen.src, "}})"); uwriteln!(gen.src, "}}"); + + let mut resource_methods = IndexMap::new(); + for (_, func) in iface.functions.iter() { match func.kind { FunctionKind::Freestanding => { gen.define_rust_guest_export(resolve, Some(name), func); } - // Resource methods are handled separately in - // `type_resource`. - _ => {} + FunctionKind::Method(id) + | FunctionKind::Constructor(id) + | FunctionKind::Static(id) => { + resource_methods.entry(id).or_insert(Vec::new()).push(func); + } } } + + for (id, _) in resource_methods.iter() { + let name = resolve.types[*id].name.as_ref().unwrap(); + let snake = name.to_snake_case(); + let camel = name.to_upper_camel_case(); + uwriteln!( + gen.src, + "pub fn {snake}(&self) -> Guest{camel}<'_> {{ + Guest{camel} {{ funcs: self }} + }}" + ); + } + uwriteln!(gen.src, "}}"); + for (id, methods) in resource_methods { + let resource_name = resolve.types[id].name.as_ref().unwrap(); + let camel = resource_name.to_upper_camel_case(); + uwriteln!(gen.src, "impl Guest{camel}<'_> {{"); + for method in methods { + gen.define_rust_guest_export(resolve, Some(name), method); + } + uwriteln!(gen.src, "}}"); + } + let module = &gen.src[..]; let snake = iface_name.to_snake_case(); @@ -841,6 +866,13 @@ impl<'a> InterfaceGenerator<'a> { } } + fn types_imported(&self) -> bool { + match self.current_interface { + Some((_, _, is_export)) => !is_export, + None => true, + } + } + fn types(&mut self, id: InterfaceId) { for (name, id) in self.resolve.interfaces[id].types.iter() { self.define_type(name, *id); @@ -887,51 +919,72 @@ impl<'a> InterfaceGenerator<'a> { .expect("resources are required to be named") .to_upper_camel_case(); - self.rustdoc(docs); - uwriteln!(self.src, "pub enum {camel} {{}}"); + if self.types_imported() { + self.rustdoc(docs); + uwriteln!(self.src, "pub enum {camel} {{}}"); - if self.gen.opts.async_.maybe_async() { - uwriteln!(self.src, "#[wasmtime::component::__internal::async_trait]") - } + if self.gen.opts.async_.maybe_async() { + uwriteln!(self.src, "#[wasmtime::component::__internal::async_trait]") + } - uwriteln!(self.src, "pub trait Host{camel} {{"); + uwriteln!(self.src, "pub trait Host{camel} {{"); + + let functions = match resource.owner { + TypeOwner::World(id) => self.resolve.worlds[id] + .imports + .values() + .filter_map(|item| match item { + WorldItem::Function(f) => Some(f), + _ => None, + }) + .collect(), + TypeOwner::Interface(id) => self.resolve.interfaces[id] + .functions + .values() + .collect::>(), + TypeOwner::None => { + panic!("A resource must be owned by a world or interface"); + } + }; - let functions = match resource.owner { - TypeOwner::World(id) => self.resolve.worlds[id] - .imports - .values() - .filter_map(|item| match item { - WorldItem::Function(f) => Some(f), - _ => None, - }) - .collect(), - TypeOwner::Interface(id) => self.resolve.interfaces[id] - .functions - .values() - .collect::>(), - TypeOwner::None => { - panic!("A resource must be owned by a world or interface"); - } - }; + for func in functions { + match func.kind { + FunctionKind::Method(resource) + | FunctionKind::Static(resource) + | FunctionKind::Constructor(resource) + if id == resource => {} + _ => continue, + } - for func in functions { - match func.kind { - FunctionKind::Method(resource) - | FunctionKind::Static(resource) - | FunctionKind::Constructor(resource) - if id == resource => {} - _ => continue, + self.generate_function_trait_sig(func); } - self.generate_function_trait_sig(func); - } + uwrite!( + self.src, + "fn drop(&mut self, rep: wasmtime::component::Resource<{camel}>) -> wasmtime::Result<()>;"); - uwrite!( - self.src, - "fn drop(&mut self, rep: wasmtime::component::Resource<{camel}>) -> wasmtime::Result<()>;" - ); + uwriteln!(self.src, "}}"); + } else { + let iface_name = match self.current_interface.unwrap().1 { + WorldKey::Name(name) => name.to_upper_camel_case(), + WorldKey::Interface(i) => self.resolve.interfaces[*i] + .name + .as_ref() + .unwrap() + .to_upper_camel_case(), + }; + self.rustdoc(docs); + uwriteln!( + self.src, + " + pub type {camel} = wasmtime::component::ResourceAny; - uwriteln!(self.src, "}}"); + pub struct Guest{camel}<'a> {{ + funcs: &'a {iface_name}, + }} + " + ); + } } fn type_record(&mut self, id: TypeId, _name: &str, record: &Record, docs: &Docs) { @@ -1668,7 +1721,7 @@ impl<'a> InterfaceGenerator<'a> { fn extract_typed_function(&mut self, func: &Function) -> (String, String) { let prev = mem::take(&mut self.src); - let snake = func.name.to_snake_case(); + let snake = func_field_name(self.resolve, func); uwrite!(self.src, "*__exports.typed_func::<("); for (_, ty) in func.params.iter() { self.print_ty(ty, TypeMode::AllBorrowed("'_")); @@ -1709,7 +1762,7 @@ impl<'a> InterfaceGenerator<'a> { uwrite!( self.src, "pub {async_} fn call_{}(&self, mut store: S, ", - func.name.to_snake_case(), + func.item_name().to_snake_case(), ); for (i, param) in func.params.iter().enumerate() { @@ -1758,10 +1811,14 @@ impl<'a> InterfaceGenerator<'a> { self.print_ty(ty, TypeMode::Owned); self.push_str(", "); } + let projection_to_func = match &func.kind { + FunctionKind::Freestanding => "", + _ => ".funcs", + }; uwriteln!( self.src, - ")>::new_unchecked(self.{})", - func.name.to_snake_case() + ")>::new_unchecked(self{projection_to_func}.{})", + func_field_name(self.resolve, func), ); self.src.push_str("};\n"); self.src.push_str("let ("); @@ -1923,6 +1980,30 @@ fn rust_function_name(func: &Function) -> String { } } +fn func_field_name(resolve: &Resolve, func: &Function) -> String { + let mut name = String::new(); + match func.kind { + FunctionKind::Method(id) => { + name.push_str("method-"); + name.push_str(resolve.types[id].name.as_ref().unwrap()); + name.push_str("-"); + } + FunctionKind::Static(id) => { + name.push_str("static-"); + name.push_str(resolve.types[id].name.as_ref().unwrap()); + name.push_str("-"); + } + FunctionKind::Constructor(id) => { + name.push_str("constructor-"); + name.push_str(resolve.types[id].name.as_ref().unwrap()); + name.push_str("-"); + } + FunctionKind::Freestanding => {} + } + name.push_str(func.item_name()); + name.to_snake_case() +} + fn get_resources<'a>(resolve: &'a Resolve, id: InterfaceId) -> impl Iterator + 'a { resolve.interfaces[id] .types diff --git a/tests/all/component_model/bindgen.rs b/tests/all/component_model/bindgen.rs index f0160dc92203..583e1b32bf59 100644 --- a/tests/all/component_model/bindgen.rs +++ b/tests/all/component_model/bindgen.rs @@ -410,3 +410,231 @@ mod async_config { let _ = t3.call_z(&mut *store).await; } } + +mod exported_resources { + use super::*; + use std::mem; + use wasmtime::component::Resource; + + wasmtime::component::bindgen!({ + inline: " + package foo:foo + + interface a { + resource x { + constructor() + } + } + + world resources { + export b: interface { + use a.{x as y} + + resource x { + constructor(y: y) + foo: func() -> u32 + } + } + + resource x + + export f: func(x1: x, x2: x) -> x + } + ", + }); + + #[derive(Default)] + struct MyImports { + hostcalls: Vec, + next_a_x: u32, + } + + #[derive(PartialEq, Debug)] + enum Hostcall { + DropRootX(u32), + DropAX(u32), + NewA, + } + + use foo::foo::a; + + impl ResourcesImports for MyImports {} + + impl HostX for MyImports { + fn drop(&mut self, val: Resource) -> Result<()> { + self.hostcalls.push(Hostcall::DropRootX(val.rep())); + Ok(()) + } + } + + impl a::HostX for MyImports { + fn new(&mut self) -> Result> { + let rep = self.next_a_x; + self.next_a_x += 1; + self.hostcalls.push(Hostcall::NewA); + Ok(Resource::new_own(rep)) + } + + fn drop(&mut self, val: Resource) -> Result<()> { + self.hostcalls.push(Hostcall::DropAX(val.rep())); + Ok(()) + } + } + + impl foo::foo::a::Host for MyImports {} + + #[test] + fn run() -> Result<()> { + let engine = engine(); + + let component = Component::new( + &engine, + r#" +(component + ;; setup the `foo:foo/a` import + (import (interface "foo:foo/a") (instance $a + (export $x "x" (type (sub resource))) + (export "[constructor]x" (func (result (own $x)))) + )) + (alias export $a "x" (type $a-x)) + (core func $a-x-drop (canon resource.drop $a-x)) + (core func $a-x-ctor (canon lower (func $a "[constructor]x"))) + + ;; setup the root import of the `x` resource + (import "x" (type $x (sub resource))) + (core func $root-x-dtor (canon resource.drop $x)) + + ;; setup and declare the `x` resource for the `b` export. + (core module $indirect-dtor + (func (export "b-x-dtor") (param i32) + local.get 0 + i32.const 0 + call_indirect (param i32) + ) + (table (export "$imports") 1 1 funcref) + ) + (core instance $indirect-dtor (instantiate $indirect-dtor)) + (type $b-x (resource (rep i32) (dtor (func $indirect-dtor "b-x-dtor")))) + (core func $b-x-drop (canon resource.drop $b-x)) + (core func $b-x-rep (canon resource.rep $b-x)) + (core func $b-x-new (canon resource.new $b-x)) + + ;; main module implementation + (core module $main + (import "foo:foo/a" "[constructor]x" (func $a-x-ctor (result i32))) + (import "foo:foo/a" "[resource-drop]x" (func $a-x-dtor (param i32))) + (import "$root" "[resource-drop]x" (func $x-dtor (param i32))) + (import "[export]b" "[resource-drop]x" (func $b-x-dtor (param i32))) + (import "[export]b" "[resource-new]x" (func $b-x-new (param i32) (result i32))) + (import "[export]b" "[resource-rep]x" (func $b-x-rep (param i32) (result i32))) + (func (export "b#[constructor]x") (param i32) (result i32) + (call $a-x-dtor (local.get 0)) + (call $b-x-new (call $a-x-ctor)) + ) + (func (export "b#[method]x.foo") (param i32) (result i32) + local.get 0) + (func (export "b#[dtor]x") (param i32) + (call $a-x-dtor (local.get 0)) + ) + (func (export "f") (param i32 i32) (result i32) + (call $x-dtor (local.get 0)) + local.get 1 + ) + ) + (core instance $main (instantiate $main + (with "foo:foo/a" (instance + (export "[resource-drop]x" (func $a-x-drop)) + (export "[constructor]x" (func $a-x-ctor)) + )) + (with "$root" (instance + (export "[resource-drop]x" (func $root-x-dtor)) + )) + (with "[export]b" (instance + (export "[resource-drop]x" (func $b-x-drop)) + (export "[resource-rep]x" (func $b-x-rep)) + (export "[resource-new]x" (func $b-x-new)) + )) + )) + + ;; fill in `$indirect-dtor`'s table with the actual destructor definition + ;; now that it's available. + (core module $fixup + (import "" "b-x-dtor" (func $b-x-dtor (param i32))) + (import "" "$imports" (table 1 1 funcref)) + (elem (i32.const 0) func $b-x-dtor) + ) + (core instance (instantiate $fixup + (with "" (instance + (export "$imports" (table 0 "$imports")) + (export "b-x-dtor" (func $main "b#[dtor]x")) + )) + )) + + ;; Create the `b` export through a subcomponent instantiation. + (func $b-x-ctor (param "y" (own $a-x)) (result (own $b-x)) + (canon lift (core func $main "b#[constructor]x"))) + (func $b-x-foo (param "self" (borrow $b-x)) (result u32) + (canon lift (core func $main "b#[method]x.foo"))) + (component $b + (import "a-x" (type $y (sub resource))) + (import "b-x" (type $x' (sub resource))) + (import "ctor" (func $ctor (param "y" (own $y)) (result (own $x')))) + (import "foo" (func $foo (param "self" (borrow $x')) (result u32))) + (export $x "x" (type $x')) + (export "[constructor]x" + (func $ctor) + (func (param "y" (own $y)) (result (own $x)))) + (export "[method]x.foo" + (func $foo) + (func (param "self" (borrow $x)) (result u32))) + ) + (instance (export "b") (instantiate $b + (with "ctor" (func $b-x-ctor)) + (with "foo" (func $b-x-foo)) + (with "a-x" (type 0 "x")) + (with "b-x" (type $b-x)) + )) + + ;; Create the `f` export which is a bare function + (func (export "f") (param "x1" (own $x)) (param "x2" (own $x)) (result (own $x)) + (canon lift (core func $main "f"))) +) + "#, + )?; + + let mut linker = Linker::new(&engine); + Resources::add_to_linker(&mut linker, |f: &mut MyImports| f)?; + let mut store = Store::new(&engine, MyImports::default()); + let (i, _) = Resources::instantiate(&mut store, &component, &linker)?; + + // call the root export `f` twice + let ret = i.call_f(&mut store, Resource::new_own(1), Resource::new_own(2))?; + assert_eq!(ret.rep(), 2); + assert_eq!( + mem::take(&mut store.data_mut().hostcalls), + [Hostcall::DropRootX(1)] + ); + let ret = i.call_f(&mut store, Resource::new_own(3), Resource::new_own(4))?; + assert_eq!(ret.rep(), 4); + assert_eq!( + mem::take(&mut store.data_mut().hostcalls), + [Hostcall::DropRootX(3)] + ); + + // interact with the `b` export + let b = i.b(); + let b_x = b.x().call_constructor(&mut store, Resource::new_own(5))?; + assert_eq!( + mem::take(&mut store.data_mut().hostcalls), + [Hostcall::DropAX(5), Hostcall::NewA] + ); + b.x().call_foo(&mut store, b_x.clone())?; + assert_eq!(mem::take(&mut store.data_mut().hostcalls), []); + b_x.resource_drop(&mut store)?; + assert_eq!( + mem::take(&mut store.data_mut().hostcalls), + [Hostcall::DropAX(0)], + ); + Ok(()) + } +}