diff --git a/Cargo.lock b/Cargo.lock index ca3a62be8..3e229c4eb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1862,6 +1862,7 @@ dependencies = [ "syn 2.0.15", "wit-bindgen-core", "wit-bindgen-rust", + "wit-bindgen-rust-lib", "wit-component", ] diff --git a/crates/core/src/lib.rs b/crates/core/src/lib.rs index 8f0155129..5cef3e60c 100644 --- a/crates/core/src/lib.rs +++ b/crates/core/src/lib.rs @@ -80,11 +80,13 @@ impl Types { live.add_type(resolve, ty); } for id in live.iter() { - let info = self.type_info.get_mut(&id).unwrap(); - if import { - info.borrowed = true; - } else { - info.owned = true; + if resolve.types[id].name.is_some() { + let info = self.type_info.get_mut(&id).unwrap(); + if import { + info.borrowed = true; + } else { + info.owned = true; + } } } let mut live = LiveTypes::default(); @@ -93,7 +95,9 @@ impl Types { live.add_type(resolve, ty); } for id in live.iter() { - self.type_info.get_mut(&id).unwrap().owned = true; + if resolve.types[id].name.is_some() { + self.type_info.get_mut(&id).unwrap().owned = true; + } } for ty in func.results.iter_types() { diff --git a/crates/rust-lib/src/lib.rs b/crates/rust-lib/src/lib.rs index 8de072c27..17626fbdc 100644 --- a/crates/rust-lib/src/lib.rs +++ b/crates/rust-lib/src/lib.rs @@ -2,6 +2,7 @@ use heck::*; use std::collections::HashMap; use std::fmt::{self, Write}; use std::iter::zip; +use std::str::FromStr; use wit_bindgen_core::wit_parser::abi::{Bitcast, LiftLower, WasmType}; use wit_bindgen_core::{wit_parser::*, TypeInfo, Types}; @@ -12,19 +13,51 @@ pub enum TypeMode { LeafBorrowed(&'static str), } +#[derive(Default, Debug, Clone, Copy)] +pub enum Ownership { + /// Generated types will be composed entirely of owning fields, regardless + /// of whether they are used as parameters to imports or not. + #[default] + Owning, + + /// Generated types used as parameters to imports will be "deeply + /// borrowing", i.e. contain references rather than owned values when + /// applicable. + Borrowing { + /// Whether or not to generate "duplicate" type definitions for a single + /// WIT type if necessary, for example if it's used as both an import + /// and an export, or if it's used both as a parameter to an import and + /// a return value from an import. + duplicate_if_necessary: bool, + }, +} + +impl FromStr for Ownership { + type Err = String; + + fn from_str(s: &str) -> Result { + match s { + "owning" => Ok(Self::Owning), + "borrowing" => Ok(Self::Borrowing { + duplicate_if_necessary: false, + }), + "borrowing-duplicate-if-necessary" => Ok(Self::Borrowing { + duplicate_if_necessary: true, + }), + _ => Err(format!( + "unrecognized ownership: `{s}`; \ + expected `owning`, `borrowing`, or `borrowing-duplicate-if-necessary`" + )), + } + } +} + pub trait RustGenerator<'a> { fn resolve(&self) -> &'a Resolve; fn path_to_interface(&self, interface: InterfaceId) -> Option; - /// This, if enabled, will possibly cause types to get duplicate copies to - /// get generated of each other. For example a record containing a string - /// used both in the import and export context would get one variant - /// generated for both. - /// - /// If this is disabled then the import context would require the same type - /// used for the export context, which has an owned string that might not - /// otherwise be necessary. - fn duplicate_if_necessary(&self) -> bool; + /// Whether to generate owning or borrowing type definitions. + fn ownership(&self) -> Ownership; /// Return true iff the generator should qualify uses of `std` features with /// `#[cfg(feature = "std")]` in its output. @@ -46,7 +79,13 @@ pub trait RustGenerator<'a> { fn push_str(&mut self, s: &str); fn info(&self, ty: TypeId) -> TypeInfo; fn types_mut(&mut self) -> &mut Types; - fn print_borrowed_slice(&mut self, mutbl: bool, ty: &Type, lifetime: &'static str); + fn print_borrowed_slice( + &mut self, + mutbl: bool, + ty: &Type, + lifetime: &'static str, + mode: TypeMode, + ); fn print_borrowed_str(&mut self, lifetime: &'static str); fn rustdoc(&mut self, docs: &Docs) { @@ -346,30 +385,35 @@ pub trait RustGenerator<'a> { } fn print_list(&mut self, ty: &Type, mode: TypeMode) { + let next_mode = if matches!(self.ownership(), Ownership::Owning) { + TypeMode::Owned + } else { + mode + }; match mode { TypeMode::AllBorrowed(lt) => { - self.print_borrowed_slice(false, ty, lt); + self.print_borrowed_slice(false, ty, lt, next_mode); } TypeMode::LeafBorrowed(lt) => { if self.resolve().all_bits_valid(ty) { - self.print_borrowed_slice(false, ty, lt); + self.print_borrowed_slice(false, ty, lt, next_mode); } else { self.push_str(self.vec_name()); self.push_str("::<"); - self.print_ty(ty, mode); + self.print_ty(ty, next_mode); self.push_str(">"); } } TypeMode::Owned => { self.push_str(self.vec_name()); self.push_str("::<"); - self.print_ty(ty, mode); + self.print_ty(ty, next_mode); self.push_str(">"); } } } - fn print_rust_slice(&mut self, mutbl: bool, ty: &Type, lifetime: &'static str) { + fn print_rust_slice(&mut self, mutbl: bool, ty: &Type, lifetime: &'static str, mode: TypeMode) { self.push_str("&"); if lifetime != "'_" { self.push_str(lifetime); @@ -379,7 +423,7 @@ pub trait RustGenerator<'a> { self.push_str(" mut "); } self.push_str("["); - self.print_ty(ty, TypeMode::AllBorrowed(lifetime)); + self.print_ty(ty, mode); self.push_str("]"); } @@ -409,12 +453,13 @@ pub trait RustGenerator<'a> { return Vec::new(); } let mut result = Vec::new(); - let first_mode = if info.owned || !info.borrowed { - TypeMode::Owned - } else { - assert!(!self.uses_two_names(&info)); - TypeMode::AllBorrowed("'a") - }; + let first_mode = + if info.owned || !info.borrowed || matches!(self.ownership(), Ownership::Owning) { + TypeMode::Owned + } else { + assert!(!self.uses_two_names(&info)); + TypeMode::AllBorrowed("'a") + }; result.push((self.result_name(ty), first_mode)); if self.uses_two_names(&info) { result.push((self.param_name(ty), TypeMode::AllBorrowed("'a"))); @@ -1009,10 +1054,21 @@ pub trait RustGenerator<'a> { } fn uses_two_names(&self, info: &TypeInfo) -> bool { - info.has_list && info.borrowed && info.owned && self.duplicate_if_necessary() + info.has_list + && info.borrowed + && info.owned + && matches!( + self.ownership(), + Ownership::Borrowing { + duplicate_if_necessary: true + } + ) } fn lifetime_for(&self, info: &TypeInfo, mode: TypeMode) -> Option<&'static str> { + if matches!(self.ownership(), Ownership::Owning) { + return None; + } let lt = match mode { TypeMode::AllBorrowed(s) | TypeMode::LeafBorrowed(s) => s, _ => return None, diff --git a/crates/rust-macro/Cargo.toml b/crates/rust-macro/Cargo.toml index a15eb7e3d..76e631583 100644 --- a/crates/rust-macro/Cargo.toml +++ b/crates/rust-macro/Cargo.toml @@ -20,5 +20,6 @@ proc-macro2 = "1.0" syn = "2.0" wit-bindgen-core = { workspace = true } wit-bindgen-rust = { workspace = true } +wit-bindgen-rust-lib = { workspace = true } wit-component = { workspace = true } anyhow = { workspace = true } diff --git a/crates/rust-macro/src/lib.rs b/crates/rust-macro/src/lib.rs index ebc2e8977..f39d36fb3 100644 --- a/crates/rust-macro/src/lib.rs +++ b/crates/rust-macro/src/lib.rs @@ -2,9 +2,10 @@ use proc_macro2::{Span, TokenStream}; use std::path::{Path, PathBuf}; use syn::parse::{Error, Parse, ParseStream, Result}; use syn::punctuated::Punctuated; -use syn::{token, Token}; +use syn::{braced, token, Token}; use wit_bindgen_core::wit_parser::{PackageId, Resolve, UnresolvedPackage, WorldId}; use wit_bindgen_rust::Opts; +use wit_bindgen_rust_lib::Ownership; #[proc_macro] pub fn generate(input: proc_macro::TokenStream) -> proc_macro::TokenStream { @@ -60,7 +61,7 @@ impl Parse for Config { Opt::UseStdFeature => opts.std_feature = true, Opt::RawStrings => opts.raw_strings = true, Opt::MacroExport => opts.macro_export = true, - Opt::DuplicateIfNecessary => opts.duplicate_if_necessary = true, + Opt::Ownership(ownership) => opts.ownership = ownership, Opt::MacroCallPrefix(prefix) => opts.macro_call_prefix = Some(prefix.value()), Opt::ExportMacroName(name) => opts.export_macro_name = Some(name.value()), Opt::Skip(list) => opts.skip.extend(list.iter().map(|i| i.value())), @@ -146,7 +147,7 @@ mod kw { syn::custom_keyword!(world); syn::custom_keyword!(path); syn::custom_keyword!(inline); - syn::custom_keyword!(duplicate_if_necessary); + syn::custom_keyword!(ownership); } enum Opt { @@ -159,7 +160,7 @@ enum Opt { MacroCallPrefix(syn::LitStr), ExportMacroName(syn::LitStr), Skip(Vec), - DuplicateIfNecessary, + Ownership(Ownership), } impl Parse for Opt { @@ -186,9 +187,44 @@ impl Parse for Opt { } else if l.peek(kw::macro_export) { input.parse::()?; Ok(Opt::MacroExport) - } else if l.peek(kw::duplicate_if_necessary) { - input.parse::()?; - Ok(Opt::DuplicateIfNecessary) + } else if l.peek(kw::ownership) { + input.parse::()?; + input.parse::()?; + let ownership = input.parse::()?; + Ok(Opt::Ownership(match ownership.to_string().as_str() { + "Owning" => Ownership::Owning, + "Borrowing" => Ownership::Borrowing { + duplicate_if_necessary: { + let contents; + braced!(contents in input); + let field = contents.parse::()?; + match field.to_string().as_str() { + "duplicate_if_necessary" => { + contents.parse::()?; + contents.parse::()?.value + } + name => { + return Err(Error::new( + field.span(), + format!( + "unrecognized `Ownership::Borrowing` field: `{name}`; \ + expected `duplicate_if_necessary`" + ), + )); + } + } + }, + }, + name => { + return Err(Error::new( + ownership.span(), + format!( + "unrecognized ownership: `{name}`; \ + expected `Owning` or `Borrowing`" + ), + )); + } + })) } else if l.peek(kw::macro_call_prefix) { input.parse::()?; input.parse::()?; diff --git a/crates/rust/src/lib.rs b/crates/rust/src/lib.rs index ae446893e..607435e5d 100644 --- a/crates/rust/src/lib.rs +++ b/crates/rust/src/lib.rs @@ -10,8 +10,8 @@ use wit_bindgen_core::{ WorldGenerator, }; use wit_bindgen_rust_lib::{ - int_repr, to_rust_ident, wasm_type, FnSig, RustFlagsRepr, RustFunctionGenerator, RustGenerator, - TypeMode, + int_repr, to_rust_ident, wasm_type, FnSig, Ownership, RustFlagsRepr, RustFunctionGenerator, + RustGenerator, TypeMode, }; #[derive(Default)] @@ -69,11 +69,18 @@ pub struct Opts { #[cfg_attr(feature = "clap", arg(long))] pub skip: Vec, - /// Whether or not to generate "duplicate" type definitions for a single - /// WIT type if necessary, for example if it's used as both an import and an - /// export. + /// Whether to generate owning or borrowing type definitions. + /// + /// Valid values include: + /// - `owning`: Generated types will be composed entirely of owning fields, + /// regardless of whether they are used as parameters to imports or not. + /// - `borrowing`: Generated types used as parameters to imports will be + /// "deeply borrowing", i.e. contain references rather than owned values + /// when applicable. + /// - `borrowing-duplicate-if-necessary`: As above, but generating distinct + /// types for borrowing and owning, if necessary. #[cfg_attr(feature = "clap", arg(long))] - pub duplicate_if_necessary: bool, + pub ownership: Ownership, } impl Opts { @@ -724,8 +731,8 @@ impl<'a> RustGenerator<'a> for InterfaceGenerator<'a> { self.resolve } - fn duplicate_if_necessary(&self) -> bool { - self.gen.opts.duplicate_if_necessary + fn ownership(&self) -> Ownership { + self.gen.opts.ownership } fn path_to_interface(&self, interface: InterfaceId) -> Option { @@ -779,8 +786,14 @@ impl<'a> RustGenerator<'a> for InterfaceGenerator<'a> { &mut self.gen.types } - fn print_borrowed_slice(&mut self, mutbl: bool, ty: &Type, lifetime: &'static str) { - self.print_rust_slice(mutbl, ty, lifetime); + fn print_borrowed_slice( + &mut self, + mutbl: bool, + ty: &Type, + lifetime: &'static str, + mode: TypeMode, + ) { + self.print_rust_slice(mutbl, ty, lifetime, mode); } fn print_borrowed_str(&mut self, lifetime: &'static str) { diff --git a/crates/rust/tests/codegen.rs b/crates/rust/tests/codegen.rs index c33e570bb..d253aa3c5 100644 --- a/crates/rust/tests/codegen.rs +++ b/crates/rust/tests/codegen.rs @@ -18,7 +18,9 @@ mod codegen_tests { mod duplicate { wit_bindgen::generate!({ path: $test, - duplicate_if_necessary, + ownership: Borrowing { + duplicate_if_necessary: true + } }); #[test] diff --git a/crates/test-rust-wasm/Cargo.toml b/crates/test-rust-wasm/Cargo.toml index c8e149160..0d2241b53 100644 --- a/crates/test-rust-wasm/Cargo.toml +++ b/crates/test-rust-wasm/Cargo.toml @@ -47,3 +47,15 @@ test = false [[bin]] name = "results" test = false + +[[bin]] +name = "owning" +test = false + +[[bin]] +name = "borrowing" +test = false + +[[bin]] +name = "borrowing-duplicate-if-necessary" +test = false diff --git a/crates/test-rust-wasm/src/bin/borrowing-duplicate-if-necessary.rs b/crates/test-rust-wasm/src/bin/borrowing-duplicate-if-necessary.rs new file mode 100644 index 000000000..dbbd1c2d9 --- /dev/null +++ b/crates/test-rust-wasm/src/bin/borrowing-duplicate-if-necessary.rs @@ -0,0 +1,3 @@ +include!("../../../../tests/runtime/ownership/borrowing-duplicate-if-necessary.rs"); + +fn main() {} diff --git a/crates/test-rust-wasm/src/bin/borrowing.rs b/crates/test-rust-wasm/src/bin/borrowing.rs new file mode 100644 index 000000000..db4e36ce0 --- /dev/null +++ b/crates/test-rust-wasm/src/bin/borrowing.rs @@ -0,0 +1,3 @@ +include!("../../../../tests/runtime/ownership/borrowing.rs"); + +fn main() {} diff --git a/crates/test-rust-wasm/src/bin/owning.rs b/crates/test-rust-wasm/src/bin/owning.rs new file mode 100644 index 000000000..4f6ed846d --- /dev/null +++ b/crates/test-rust-wasm/src/bin/owning.rs @@ -0,0 +1,3 @@ +include!("../../../../tests/runtime/ownership/owning.rs"); + +fn main() {} diff --git a/tests/runtime/lists/wasm.rs b/tests/runtime/lists/wasm.rs index bf693ecc2..ff6cbd295 100644 --- a/tests/runtime/lists/wasm.rs +++ b/tests/runtime/lists/wasm.rs @@ -1,4 +1,9 @@ -wit_bindgen::generate!(in "../../tests/runtime/lists"); +wit_bindgen::generate!({ + path: "../../tests/runtime/lists", + ownership: Borrowing { + duplicate_if_necessary: false + } +}); struct Component; diff --git a/tests/runtime/main.rs b/tests/runtime/main.rs index 8e0950929..12c02b161 100644 --- a/tests/runtime/main.rs +++ b/tests/runtime/main.rs @@ -2,7 +2,7 @@ use anyhow::Result; use std::borrow::Cow; use std::fs; use std::io::Write; -use std::path::{Path, PathBuf}; +use std::path::PathBuf; use std::process::Command; use wasm_encoder::{Encode, Section}; use wasmtime::component::{Component, Instance, Linker}; @@ -14,6 +14,7 @@ mod flavorful; mod lists; mod many_arguments; mod numbers; +mod ownership; mod records; mod smoke; mod strings; @@ -43,6 +44,19 @@ fn run_test( instantiate: fn(&mut Store>, &Component, &Linker>) -> Result<(U, Instance)>, test: fn(U, &mut Store>) -> Result<()>, ) -> Result<()> +where + T: Default, +{ + run_test_from_dir(name, name, add_to_linker, instantiate, test) +} + +fn run_test_from_dir( + dir_name: &str, + name: &str, + add_to_linker: fn(&mut Linker>) -> Result<()>, + instantiate: fn(&mut Store>, &Component, &Linker>) -> Result<(U, Instance)>, + test: fn(U, &mut Store>) -> Result<()>, +) -> Result<()> where T: Default, { @@ -54,7 +68,7 @@ where config.wasm_component_model(true); let engine = Engine::new(&config)?; - for wasm in tests(name)? { + for wasm in tests(name, dir_name)? { let component = Component::from_file(&engine, &wasm)?; let mut linker = Linker::new(&engine); @@ -70,11 +84,11 @@ where Ok(()) } -fn tests(name: &str) -> Result> { +fn tests(name: &str, dir_name: &str) -> Result> { let mut result = Vec::new(); let mut dir = PathBuf::from("./tests/runtime"); - dir.push(name); + dir.push(dir_name); let mut resolve = Resolve::new(); let (pkg, _files) = resolve.push_dir(&dir).unwrap(); @@ -289,8 +303,6 @@ fn tests(name: &str) -> Result> { #[cfg(feature = "teavm-java")] if !java.is_empty() { - use heck::*; - const DEPTH_FROM_TARGET_DIR: u32 = 2; let base_dir = { @@ -327,7 +339,6 @@ fn tests(name: &str) -> Result> { dst_files.push(dst); } - let upper = world_name.to_upper_camel_case(); for java_impl in java { let dst = java_dir.join( java_impl diff --git a/tests/runtime/ownership.rs b/tests/runtime/ownership.rs new file mode 100644 index 000000000..39f75fd8f --- /dev/null +++ b/tests/runtime/ownership.rs @@ -0,0 +1,57 @@ +use anyhow::Result; +use wasmtime::Store; + +wasmtime::component::bindgen!(in "tests/runtime/ownership"); + +#[derive(Default)] +pub struct MyImports { + called_foo: bool, + called_bar: bool, + called_baz: bool, +} + +impl lists::Host for MyImports { + fn foo(&mut self, list: Vec>) -> Result>> { + self.called_foo = true; + Ok(list) + } +} + +impl thing_in::Host for MyImports { + fn bar(&mut self, _value: thing_in::Thing) -> Result<()> { + self.called_bar = true; + Ok(()) + } +} + +impl thing_in_and_out::Host for MyImports { + fn baz(&mut self, value: thing_in_and_out::Thing) -> Result { + self.called_baz = true; + Ok(value) + } +} + +#[test] +fn run() -> Result<()> { + for name in ["owning", "borrowing", "borrowing-duplicate-if-necessary"] { + crate::run_test_from_dir( + "ownership", + name, + |linker| Ownership::add_to_linker(linker, |x| &mut x.0), + |store, component, linker| Ownership::instantiate(store, component, linker), + run_test, + )?; + } + + Ok(()) +} + +fn run_test(exports: Ownership, store: &mut Store>) -> Result<()> { + exports.call_foo(&mut *store)?; + + assert!(store.data().0.called_foo); + assert!(store.data().0.called_bar); + assert!(store.data().0.called_baz); + + Ok(()) +} diff --git a/tests/runtime/ownership/borrowing-duplicate-if-necessary.rs b/tests/runtime/ownership/borrowing-duplicate-if-necessary.rs new file mode 100644 index 000000000..7417cae28 --- /dev/null +++ b/tests/runtime/ownership/borrowing-duplicate-if-necessary.rs @@ -0,0 +1,43 @@ +wit_bindgen::generate!({ + path: "../../tests/runtime/ownership", + ownership: Borrowing { + duplicate_if_necessary: true + } +}); + +impl PartialEq for thing_in_and_out::ThingResult { + fn eq(&self, other: &Self) -> bool { + self.name == other.name && self.value == other.value + } +} + +struct Exports; + +export_ownership!(Exports); + +impl Ownership for Exports { + fn foo() { + let value = &[&["foo", "bar"] as &[_]] as &[_]; + assert_eq!( + vec![vec!["foo".to_owned(), "bar".to_owned()]], + lists::foo(value) + ); + + thing_in::bar(thing_in::Thing { + name: "thing 1", + value: &["some value", "another value"], + }); + + let value = thing_in_and_out::ThingParam { + name: "thing 1", + value: &["some value", "another value"], + }; + assert_eq!( + thing_in_and_out::ThingResult { + name: "thing 1".to_owned(), + value: vec!["some value".to_owned(), "another value".to_owned()], + }, + thing_in_and_out::baz(value) + ); + } +} diff --git a/tests/runtime/ownership/borrowing.rs b/tests/runtime/ownership/borrowing.rs new file mode 100644 index 000000000..c5fb96f21 --- /dev/null +++ b/tests/runtime/ownership/borrowing.rs @@ -0,0 +1,37 @@ +wit_bindgen::generate!({ + path: "../../tests/runtime/ownership", + ownership: Borrowing { + duplicate_if_necessary: false + } +}); + +impl PartialEq for thing_in_and_out::Thing { + fn eq(&self, other: &Self) -> bool { + self.name == other.name && self.value == other.value + } +} + +struct Exports; + +export_ownership!(Exports); + +impl Ownership for Exports { + fn foo() { + let value = &[&["foo", "bar"] as &[_]] as &[_]; + assert_eq!( + vec![vec!["foo".to_owned(), "bar".to_owned()]], + lists::foo(value) + ); + + thing_in::bar(thing_in::Thing { + name: "thing 1", + value: &["some value", "another value"], + }); + + let value = thing_in_and_out::Thing { + name: "thing 1".to_owned(), + value: vec!["some value".to_owned(), "another value".to_owned()], + }; + assert_eq!(value, thing_in_and_out::baz(&value)); + } +} diff --git a/tests/runtime/ownership/owning.rs b/tests/runtime/ownership/owning.rs new file mode 100644 index 000000000..4adf6d680 --- /dev/null +++ b/tests/runtime/ownership/owning.rs @@ -0,0 +1,32 @@ +wit_bindgen::generate!({ + path: "../../tests/runtime/ownership", + ownership: Owning +}); + +impl PartialEq for thing_in_and_out::Thing { + fn eq(&self, other: &Self) -> bool { + self.name == other.name && self.value == other.value + } +} + +struct Exports; + +export_ownership!(Exports); + +impl Ownership for Exports { + fn foo() { + let value = vec![vec!["foo".to_owned(), "bar".to_owned()]]; + assert_eq!(value, lists::foo(&value)); + + thing_in::bar(&thing_in::Thing { + name: "thing 1".to_owned(), + value: vec!["some value".to_owned(), "another value".to_owned()], + }); + + let value = thing_in_and_out::Thing { + name: "thing 1".to_owned(), + value: vec!["some value".to_owned(), "another value".to_owned()], + }; + assert_eq!(value, thing_in_and_out::baz(&value)); + } +} diff --git a/tests/runtime/ownership/world.wit b/tests/runtime/ownership/world.wit new file mode 100644 index 000000000..978278455 --- /dev/null +++ b/tests/runtime/ownership/world.wit @@ -0,0 +1,27 @@ +package test:ownership + +world ownership { + import lists: interface { + foo: func(a: list>) -> list> + } + + import thing-in: interface { + record thing { + name: string, + value: list + } + + bar: func(a: thing) + } + + import thing-in-and-out: interface { + record thing { + name: string, + value: list + } + + baz: func(a: thing) -> thing + } + + export foo: func() +}