diff --git a/crates/guest-rust/macro/src/lib.rs b/crates/guest-rust/macro/src/lib.rs index ae4635708..5246dab81 100644 --- a/crates/guest-rust/macro/src/lib.rs +++ b/crates/guest-rust/macro/src/lib.rs @@ -2,6 +2,7 @@ use proc_macro2::{Span, TokenStream}; use quote::ToTokens; use std::collections::HashMap; use std::path::{Path, PathBuf}; +use std::str::FromStr; use std::sync::atomic::{AtomicUsize, Ordering::Relaxed}; use syn::parse::{Error, Parse, ParseStream, Result}; use syn::punctuated::Punctuated; @@ -9,7 +10,7 @@ use syn::spanned::Spanned; use syn::{braced, token, LitStr, Token}; use wit_bindgen_core::wit_parser::{PackageId, Resolve, UnresolvedPackageGroup, WorldId}; use wit_bindgen_core::AsyncFilterSet; -use wit_bindgen_rust::{Opts, Ownership, WithOption}; +use wit_bindgen_rust::{Opts, Ownership, StubsMode, WithOption}; #[proc_macro] pub fn generate(input: proc_macro::TokenStream) -> proc_macro::TokenStream { @@ -106,9 +107,7 @@ impl Parse for Config { Opt::Skip(list) => opts.skip.extend(list.iter().map(|i| i.value())), Opt::RuntimePath(path) => opts.runtime_path = Some(path.value()), Opt::BitflagsPath(path) => opts.bitflags_path = Some(path.value()), - Opt::Stubs => { - opts.stubs = true; - } + Opt::Stubs(mode) => opts.stubs = mode, Opt::ExportPrefix(prefix) => opts.export_prefix = Some(prefix.value()), Opt::AdditionalDerives(paths) => { opts.additional_derive_attributes = paths @@ -380,7 +379,7 @@ enum Opt { Ownership(Ownership), RuntimePath(syn::LitStr), BitflagsPath(syn::LitStr), - Stubs, + Stubs(StubsMode), ExportPrefix(syn::LitStr), // Parse as paths so we can take the concrete types/macro names rather than raw strings AdditionalDerives(Vec), @@ -486,7 +485,22 @@ impl Parse for Opt { Ok(Opt::BitflagsPath(input.parse()?)) } else if l.peek(kw::stubs) { input.parse::()?; - Ok(Opt::Stubs) + input.parse::()?; + let stubs_mode = input.parse::()?; + Ok(Opt::Stubs(match stubs_mode.to_string().as_str() { + "Omit" => StubsMode::Omit, + "Embedded" => StubsMode::Embedded, + "Separate" => StubsMode::Separate, + name => { + return Err(Error::new( + stubs_mode.span(), + format!( + "unrecognized stubs mode: `{name}`; \ + expected `Omit`, `Embedded`, or `Separate`" + ), + )); + } + })) } else if l.peek(kw::export_prefix) { input.parse::()?; input.parse::()?; diff --git a/crates/rust/src/lib.rs b/crates/rust/src/lib.rs index ee03aa7a1..8b4209355 100644 --- a/crates/rust/src/lib.rs +++ b/crates/rust/src/lib.rs @@ -5,8 +5,10 @@ use heck::*; use indexmap::{IndexMap, IndexSet}; use std::collections::{BTreeMap, HashMap, HashSet}; use std::fmt::{self, Write as _}; +use std::hint::unreachable_unchecked; use std::mem; use std::str::FromStr; +use std::sync::LazyLock; use wit_bindgen_core::abi::{Bitcast, WasmType}; use wit_bindgen_core::{ dealias, name_package_module, uwrite, uwriteln, wit_parser::*, AsyncFilterSet, Files, @@ -30,6 +32,8 @@ struct RustWasm { types: Types, src_preamble: Source, src: Source, + /// Used when stubs == StubsMode::Separate + stubs_src: Source, opts: Opts, import_modules: Vec<(String, Vec)>, export_modules: Vec<(String, Vec)>, @@ -166,10 +170,18 @@ pub struct Opts { #[cfg_attr(feature = "clap", arg(long, value_name = "NAME"))] pub skip: Vec, - /// If true, generate stub implementations for any exported functions, + /// Whether to generate stub implementations for any exported functions, /// interfaces, and/or resources. - #[cfg_attr(feature = "clap", arg(long))] - pub stubs: bool, + /// + /// Valid values are: + /// + /// - `omit`: Stubs will not be generated. + /// + /// - `embedded`: Stubs will be generated in the bindings file. + /// + /// - `separate`: Stubs will be generated in a separate _impl file. + #[cfg_attr(feature = "clap", arg(long, default_value_t = StubsMode::Omit))] + pub stubs: StubsMode, /// Optionally prefix any export names with the specified value. /// @@ -421,7 +433,11 @@ impl RustWasm { uwriteln!(self.src, "#[rustfmt::skip]"); } - self.src.push_str("mod _rt {\n"); + if matches!(self.opts.stubs, StubsMode::Separate) { + self.src.push_str("pub(crate) mod _rt {\n"); + } else { + self.src.push_str("mod _rt {\n"); + } self.src.push_str("#![allow(dead_code, clippy::all)]\n"); let mut emitted = IndexSet::new(); while !self.rt_module.is_empty() { @@ -839,9 +855,18 @@ macro_rules! __export_{world_name}_impl {{ {use_vis} use __export_{world_name}_impl as {export_macro_name};" ); - if self.opts.stubs { - uwriteln!(self.src, "export!(Stub);"); - } + match self.opts.stubs { + StubsMode::Embedded => uwriteln!(self.src, "export!(Stub);"), + StubsMode::Separate => { + let name = &resolve.worlds[world_id].name; + let module_name = to_rust_module_raw(name.to_snake_case().as_str()); + uwriteln!( + self.stubs_src, + "export!(Stub with_types_in crate::{module_name});" + ) + } + StubsMode::Omit => {} + }; } /// Generates a `#[link_section]` custom section to get smuggled through @@ -981,8 +1006,26 @@ impl WorldGenerator for RustWasm { if !self.opts.skip.is_empty() { uwriteln!(self.src_preamble, "// * skip: {:?}", self.opts.skip); } - if self.opts.stubs { - uwriteln!(self.src_preamble, "// * stubs"); + if !matches!(self.opts.stubs, StubsMode::Omit) { + uwriteln!(self.src_preamble, "// * stubs: {:?}", self.opts.stubs); + if matches!(self.opts.stubs, StubsMode::Separate) { + uwriteln!(self.stubs_src, "#[allow(warnings)]\n"); + let name = &resolve.worlds[world].name; + let module_name = name.to_snake_case(); + let module_name_ident = to_rust_module_raw(module_name.as_str()); + if module_name_ident != to_rust_ident_raw(module_name.as_str()) { + // In no_std, `core` is automatically in scope at the crate root, + // so a `mod core;` here would conflict with the built-in `core` crate + uwriteln!(self.stubs_src, "#[path = \"{module_name}.rs\"]\n"); + } + uwriteln!( + self.stubs_src, + r#"mod {module_name_ident}; +#[allow(warnings)] +use crate::{module_name_ident}::*;"# + ); + self.stubs_src.push_str("\n"); + } } if let Some(export_prefix) = &self.opts.export_prefix { uwriteln!( @@ -1195,7 +1238,7 @@ impl WorldGenerator for RustWasm { self.export_macros .push((macro_name, self.interface_names[&id].path.clone())); - if self.opts.stubs { + if !matches!(self.opts.stubs, StubsMode::Omit) { let world_id = self.world.unwrap(); let mut r#gen = self.interface( Identifier::World(world_id), @@ -1205,7 +1248,12 @@ impl WorldGenerator for RustWasm { ); r#gen.generate_stub(Some((id, name)), resolve.interfaces[id].functions.values()); let stub = r#gen.finish(); - self.src.push_str(&stub); + let stubs = match self.opts.stubs { + StubsMode::Omit => unsafe { unreachable_unchecked() }, + StubsMode::Embedded => &mut self.src, + StubsMode::Separate => &mut self.stubs_src, + }; + stubs.push_str(&stub); } Ok(()) } @@ -1223,12 +1271,17 @@ impl WorldGenerator for RustWasm { self.src.push_str(&src); self.export_macros.push((macro_name, String::new())); - if self.opts.stubs { + if !matches!(self.opts.stubs, StubsMode::Omit) { let mut r#gen = self.interface(Identifier::World(world), "[export]$root", resolve, false); r#gen.generate_stub(None, funcs.iter().map(|f| f.1)); let stub = r#gen.finish(); - self.src.push_str(&stub); + let stubs = match self.opts.stubs { + StubsMode::Omit => unsafe { unreachable_unchecked() }, + StubsMode::Embedded => &mut self.src, + StubsMode::Separate => &mut self.stubs_src, + }; + stubs.push_str(&stub); } Ok(()) } @@ -1342,8 +1395,13 @@ impl WorldGenerator for RustWasm { }, ); - if self.opts.stubs { - self.src.push_str("\n#[derive(Debug)]\npub struct Stub;\n"); + if !matches!(self.opts.stubs, StubsMode::Omit) { + let stubs = match self.opts.stubs { + StubsMode::Omit => unsafe { unreachable_unchecked() }, + StubsMode::Embedded => &mut self.src, + StubsMode::Separate => &mut self.stubs_src, + }; + stubs.push_str("\n#[derive(Debug)]\npub struct Stub;\n"); } let mut src = mem::take(&mut self.src); @@ -1360,6 +1418,15 @@ impl WorldGenerator for RustWasm { let module_name = name.to_snake_case(); files.push(&format!("{module_name}.rs"), src.as_bytes()); + if matches!(self.opts.stubs, StubsMode::Separate) { + let mut src = mem::take(&mut self.stubs_src); + if self.opts.format { + let syntax_tree = syn::parse_file(src.as_str()).unwrap(); + *src.as_mut_string() = prettyplease::unparse(&syntax_tree); + } + files.push(&format!("{module_name}_impl.rs"), src.as_bytes()); + } + let remapped_keys = self .with .iter() @@ -1483,6 +1550,50 @@ impl fmt::Display for Ownership { } } +#[derive(Default, Debug, Clone, Copy)] +#[cfg_attr( + feature = "serde", + derive(serde::Deserialize), + serde(rename_all = "kebab-case") +)] +pub enum StubsMode { + /// Stubs will not be generated. + #[default] + Omit, + + /// Stubs will be generated in the bindings file. + Embedded, + + /// Stubs will be generated in a separate file. + Separate, +} + +impl FromStr for StubsMode { + type Err = String; + + fn from_str(s: &str) -> Result { + match s { + "omit" => Ok(Self::Omit), + "embedded" => Ok(Self::Embedded), + "separate" => Ok(Self::Separate), + _ => Err(format!( + "unrecognized stubsMode: `{s}`; \ + expected 'omit', `embedded`, or `separate`" + )), + } + } +} + +impl fmt::Display for StubsMode { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + f.write_str(match self { + StubsMode::Omit => "omit", + StubsMode::Embedded => "embedded", + StubsMode::Separate => "separate", + }) + } +} + /// Options for with "with" remappings. #[derive(Debug, Clone)] #[cfg_attr( @@ -1533,61 +1644,44 @@ impl FnSig { } } +static RUST_KEYWORDS: LazyLock> = LazyLock::new(|| { + // Source: https://doc.rust-lang.org/reference/keywords.html + HashSet::from([ + "as", "break", "const", "continue", "crate", "else", "enum", "extern", "false", "fn", + "for", "if", "impl", "in", "let", "loop", "match", "mod", "move", "mut", "pub", "ref", + "return", "self", "Self", "static", "struct", "super", "trait", "true", "type", "unsafe", + "use", "where", "while", "async", "await", "dyn", "abstract", "become", "box", "do", + "final", "macro", "override", "priv", "typeof", "unsized", "virtual", "yield", "try", + "gen", + ]) +}); + pub fn to_rust_ident(name: &str) -> String { - match name { + if RUST_KEYWORDS.contains(name) { // Escape Rust keywords. - // Source: https://doc.rust-lang.org/reference/keywords.html - "as" => "as_".into(), - "break" => "break_".into(), - "const" => "const_".into(), - "continue" => "continue_".into(), - "crate" => "crate_".into(), - "else" => "else_".into(), - "enum" => "enum_".into(), - "extern" => "extern_".into(), - "false" => "false_".into(), - "fn" => "fn_".into(), - "for" => "for_".into(), - "if" => "if_".into(), - "impl" => "impl_".into(), - "in" => "in_".into(), - "let" => "let_".into(), - "loop" => "loop_".into(), - "match" => "match_".into(), - "mod" => "mod_".into(), - "move" => "move_".into(), - "mut" => "mut_".into(), - "pub" => "pub_".into(), - "ref" => "ref_".into(), - "return" => "return_".into(), - "self" => "self_".into(), - "static" => "static_".into(), - "struct" => "struct_".into(), - "super" => "super_".into(), - "trait" => "trait_".into(), - "true" => "true_".into(), - "type" => "type_".into(), - "unsafe" => "unsafe_".into(), - "use" => "use_".into(), - "where" => "where_".into(), - "while" => "while_".into(), - "async" => "async_".into(), - "await" => "await_".into(), - "dyn" => "dyn_".into(), - "abstract" => "abstract_".into(), - "become" => "become_".into(), - "box" => "box_".into(), - "do" => "do_".into(), - "final" => "final_".into(), - "macro" => "macro_".into(), - "override" => "override_".into(), - "priv" => "priv_".into(), - "typeof" => "typeof_".into(), - "unsized" => "unsized_".into(), - "virtual" => "virtual_".into(), - "yield" => "yield_".into(), - "try" => "try_".into(), - s => s.to_snake_case(), + format!("{}_", name) + } else { + name.to_snake_case() + } +} + +pub fn to_rust_ident_raw(name: &str) -> String { + if RUST_KEYWORDS.contains(name) { + // Turn Rust keywords into raw identifiers. + format!("r#{}", name) + } else { + name.to_snake_case() + } +} + +static RUST_CRATE_NAMES: LazyLock> = + LazyLock::new(|| HashSet::from(["core", "alloc", "std"])); + +pub fn to_rust_module_raw(name: &str) -> String { + if RUST_CRATE_NAMES.contains(name) { + format!("bindings_{}", name) + } else { + to_rust_ident_raw(name) } } diff --git a/crates/test/src/c.rs b/crates/test/src/c.rs index 8ffb09cf5..aef1143ac 100644 --- a/crates/test/src/c.rs +++ b/crates/test/src/c.rs @@ -68,6 +68,7 @@ impl LanguageMethods for C { fn codegen_test_variants(&self) -> &[(&str, &[&str])] { &[ + ("base", &[]), ("no-sig-flattening", &["--no-sig-flattening"]), ("autodrop", &["--autodrop-borrows=yes"]), ("async", &["--async=all"]), diff --git a/crates/test/src/cpp.rs b/crates/test/src/cpp.rs index 0ccbd6af1..069f17374 100644 --- a/crates/test/src/cpp.rs +++ b/crates/test/src/cpp.rs @@ -46,36 +46,36 @@ impl LanguageMethods for Cpp { _args: &[String], ) -> bool { match name { - "async-trait-function.wit" - | "error-context.wit" - | "futures.wit" - | "import_export_func.wit" - | "import-func.wit" - | "issue573.wit" - | "issue929-no-export.wit" - | "keywords.wit" - | "lift-lower-foreign.wit" - | "lists.wit" - | "multiversion" - | "resource-alias.wit" - | "resource-borrow-in-record.wit" - | "resources.wit" - | "resources-in-aggregates.wit" - | "resources-with-futures.wit" - | "resources-with-streams.wit" - | "ret-areas.wit" - | "return-resource-from-export.wit" - | "same-names1.wit" - | "same-names5.wit" - | "simple-http.wit" - | "variants.wit" - | "variants-unioning-types.wit" - | "wasi-cli" - | "wasi-filesystem" - | "wasi-http" - | "wasi-io" - | "worlds-with-types.wit" - | "streams.wit" => true, + "async-trait-function.wit-base" + | "error-context.wit-base" + | "futures.wit-base" + | "import_export_func.wit-base" + | "import-func.wit-base" + | "issue573.wit-base" + | "issue929-no-export.wit-base" + | "keywords.wit-base" + | "lift-lower-foreign.wit-base" + | "lists.wit-base" + | "multiversion-base" + | "resource-alias.wit-base" + | "resource-borrow-in-record.wit-base" + | "resources.wit-base" + | "resources-in-aggregates.wit-base" + | "resources-with-futures.wit-base" + | "resources-with-streams.wit-base" + | "ret-areas.wit-base" + | "return-resource-from-export.wit-base" + | "same-names1.wit-base" + | "same-names5.wit-base" + | "simple-http.wit-base" + | "variants.wit-base" + | "variants-unioning-types.wit-base" + | "wasi-cli-base" + | "wasi-filesystem-base" + | "wasi-http-base" + | "wasi-io-base" + | "worlds-with-types.wit-base" + | "streams.wit-base" => true, _ => false, } } diff --git a/crates/test/src/csharp.rs b/crates/test/src/csharp.rs index 0bfeebbbc..d78d437d5 100644 --- a/crates/test/src/csharp.rs +++ b/crates/test/src/csharp.rs @@ -30,8 +30,8 @@ impl LanguageMethods for Csharp { &["--runtime=native-aot"] } - fn default_bindgen_args_for_codegen(&self) -> &[&str] { - &["--generate-stub"] + fn codegen_test_variants(&self) -> &[(&str, &[&str])] { + &[("base", &["--generate-stub"])] } fn should_fail_verify( diff --git a/crates/test/src/lib.rs b/crates/test/src/lib.rs index 468eaa309..5c6e0fc20 100644 --- a/crates/test/src/lib.rs +++ b/crates/test/src/lib.rs @@ -214,6 +214,7 @@ struct Verify<'a> { bindings_dir: &'a Path, artifacts_dir: &'a Path, args: &'a [String], + args_kind: &'a str, world: &'a str, } @@ -503,21 +504,8 @@ impl Runner<'_> { continue; } - let mut args = Vec::new(); - for arg in language.obj().default_bindgen_args_for_codegen() { - args.push(arg.to_string()); - } - - codegen_tests.push(( - language.clone(), - test, - name.to_string(), - args.clone(), - config.clone(), - )); - for (args_kind, new_args) in language.obj().codegen_test_variants() { - let mut args = args.clone(); + let mut args = Vec::new(); for arg in new_args.iter() { args.push(arg.to_string()); } @@ -615,6 +603,7 @@ impl Runner<'_> { bindings_dir: &bindings_dir, wit_test: test, args: &bindgen.args, + args_kind, }, ) .context("failed to verify generated bindings")?; @@ -1107,16 +1096,15 @@ trait LanguageMethods { /// `//@` for Rust or `;;@` for WebAssembly Text. fn comment_prefix_for_test_config(&self) -> Option<&str>; - /// Returns the extra permutations, if any, of arguments to use with codegen - /// tests. + /// Returns all test permutations of arguments to use with codegen tests. /// /// This is used to run all codegen tests with a variety of bindings /// generator options. The first element in the tuple is a descriptive - /// string that should be unique (used in file names) and the second elemtn + /// string that should be unique (used in file names) and the second element /// is the list of arguments for that variant to pass to the bindings /// generator. fn codegen_test_variants(&self) -> &[(&str, &[&str])] { - &[] + &[("base", &[])] } /// Performs any one-time preparation necessary for this language, such as @@ -1174,12 +1162,6 @@ trait LanguageMethods { &[] } - /// Same as `default_bindgen_args` but specifically applied during codegen - /// tests, such as generating stub impls by default. - fn default_bindgen_args_for_codegen(&self) -> &[&str] { - &[] - } - /// Returns the name of this bindings generator when passed to /// `wit-bindgen`. /// diff --git a/crates/test/src/rust.rs b/crates/test/src/rust.rs index 9f68f7593..b53ee44a3 100644 --- a/crates/test/src/rust.rs +++ b/crates/test/src/rust.rs @@ -67,7 +67,9 @@ impl LanguageMethods for Rust { // Currently there's a bug with this borrowing mode which means that // this variant does not pass. - if name == "wasi-http-borrowed-duplicate" { + if name == "wasi-http-borrowed-duplicate" + || name == "wasi-http-borrowed-duplicate-separate-stubs" + { return true; } @@ -76,13 +78,35 @@ impl LanguageMethods for Rust { fn codegen_test_variants(&self) -> &[(&str, &[&str])] { &[ - ("borrowed", &["--ownership=borrowing"]), + // embedded stubs + ("base", &["--stubs=embedded"]), + ("borrowed", &["--stubs=embedded", "--ownership=borrowing"]), ( "borrowed-duplicate", - &["--ownership=borrowing-duplicate-if-necessary"], + &[ + "--stubs=embedded", + "--ownership=borrowing-duplicate-if-necessary", + ], + ), + ("async", &["--stubs=embedded", "--async=all"]), + ("no-std", &["--stubs=embedded", "--std-feature"]), + // separate stubs + ( + "base-separate-stubs", + &["--stubs=separate", "--ownership=borrowing"], + ), + ( + "borrowed-duplicate-separate-stubs", + &[ + "--stubs=separate", + "--ownership=borrowing-duplicate-if-necessary", + ], + ), + ("async-separate-stubs", &["--stubs=separate", "--async=all"]), + ( + "no-std-separate-stubs", + &["--stubs=separate", "--std-feature"], ), - ("async", &["--async=all"]), - ("no-std", &["--std-feature"]), ] } @@ -90,10 +114,6 @@ impl LanguageMethods for Rust { &["--generate-all", "--format"] } - fn default_bindgen_args_for_codegen(&self) -> &[&str] { - &["--stubs"] - } - fn prepare(&self, runner: &mut Runner<'_>) -> Result<()> { let cwd = env::current_dir()?; let opts = &runner.opts.rust; @@ -228,9 +248,15 @@ path = 'lib.rs' } fn verify(&self, runner: &Runner<'_>, verify: &Verify<'_>) -> Result<()> { - let bindings = verify - .bindings_dir - .join(format!("{}.rs", verify.world.to_snake_case())); + let bindings = verify.bindings_dir.join(format!( + "{}{}.rs", + verify.world.to_snake_case(), + if verify.args_kind.ends_with("-separate-stubs") { + "_impl" + } else { + "" + }, + )); let test_edition = |edition: Edition| -> Result<()> { let mut cmd = runner.rustc(edition); cmd.arg(&bindings)