From 8fa3f37596b30813302a8ea25b8caa2c3f5d0151 Mon Sep 17 00:00:00 2001 From: Bjorn Beishline <75190918+BjornTheProgrammer@users.noreply.github.com> Date: Thu, 11 Jun 2026 20:04:37 -0700 Subject: [PATCH 1/2] feat(rust) add native link option --- crates/core/src/lib.rs | 1 + crates/{cpp => core}/src/symbol_name.rs | 2 +- crates/cpp/src/lib.rs | 6 +- crates/guest-rust/macro/src/lib.rs | 9 ++ crates/guest-rust/src/lib.rs | 18 +++ crates/guest-rust/src/rt/mod.rs | 2 +- crates/rust/src/bindgen.rs | 1 + crates/rust/src/interface.rs | 150 +++++++++++++-------- crates/rust/src/lib.rs | 137 ++++++++++++++++++- crates/rust/tests/codegen.rs | 166 ++++++++++++++++++++++++ 10 files changed, 427 insertions(+), 65 deletions(-) rename crates/{cpp => core}/src/symbol_name.rs (98%) diff --git a/crates/core/src/lib.rs b/crates/core/src/lib.rs index ee5a63b30..7e9f53cfa 100644 --- a/crates/core/src/lib.rs +++ b/crates/core/src/lib.rs @@ -14,6 +14,7 @@ mod path; pub use path::name_package_module; mod async_; pub use async_::AsyncFilterSet; +pub mod symbol_name; #[derive(Default, Copy, Clone, PartialEq, Eq, Debug)] pub enum Direction { diff --git a/crates/cpp/src/symbol_name.rs b/crates/core/src/symbol_name.rs similarity index 98% rename from crates/cpp/src/symbol_name.rs rename to crates/core/src/symbol_name.rs index 4b71d1005..1798eb749 100644 --- a/crates/cpp/src/symbol_name.rs +++ b/crates/core/src/symbol_name.rs @@ -1,4 +1,4 @@ -use wit_bindgen_core::abi; +use crate::abi; fn hexdigit(v: u32) -> char { if v < 10 { diff --git a/crates/cpp/src/lib.rs b/crates/cpp/src/lib.rs index 2d7b30099..e0277893e 100644 --- a/crates/cpp/src/lib.rs +++ b/crates/cpp/src/lib.rs @@ -9,12 +9,13 @@ use std::{ process::{Command, Stdio}, str::FromStr, }; -use symbol_name::{make_external_component, make_external_symbol}; use wit_bindgen_c::to_c_ident; use wit_bindgen_core::{ Files, InterfaceGenerator, Source, Types, WorldGenerator, abi::{self, AbiVariant, Bindgen, Bitcast, LiftLower, WasmSignature, WasmType}, - name_package_module, uwrite, uwriteln, + name_package_module, + symbol_name::{make_external_component, make_external_symbol}, + uwrite, uwriteln, wit_parser::{ Alignment, ArchitectureSize, Docs, Function, FunctionKind, Handle, Int, InterfaceId, Param, Resolve, SizeAlign, Stability, Type, TypeDef, TypeDefKind, TypeId, TypeOwner, WorldId, @@ -24,7 +25,6 @@ use wit_bindgen_core::{ use wit_parser::TypeIdVisitor; // mod wamr; -mod symbol_name; pub const RESOURCE_IMPORT_BASE_CLASS_NAME: &str = "ResourceImportBase"; pub const RESOURCE_EXPORT_BASE_CLASS_NAME: &str = "ResourceExportBase"; diff --git a/crates/guest-rust/macro/src/lib.rs b/crates/guest-rust/macro/src/lib.rs index 603222a77..be9077d4c 100644 --- a/crates/guest-rust/macro/src/lib.rs +++ b/crates/guest-rust/macro/src/lib.rs @@ -175,6 +175,9 @@ impl Parse for Config { Opt::MergeStructurallyEqualTypes(enable) => { opts.merge_structurally_equal_types = Some(Some(enable.value())) } + Opt::LinkNativeSymbols(enable) => { + opts.link_native_symbols = enable.value(); + } } } } else { @@ -332,6 +335,7 @@ mod kw { syn::custom_keyword!(debug); syn::custom_keyword!(enable_method_chaining); syn::custom_keyword!(merge_structurally_equal_types); + syn::custom_keyword!(link_native_symbols); } #[derive(Clone)] @@ -416,6 +420,7 @@ enum Opt { Debug(syn::LitBool), EnableMethodChaining(syn::LitBool), MergeStructurallyEqualTypes(syn::LitBool), + LinkNativeSymbols(syn::LitBool), } impl Parse for Opt { @@ -623,6 +628,10 @@ impl Parse for Opt { input.parse::()?; input.parse::()?; Ok(Opt::MergeStructurallyEqualTypes(input.parse()?)) + } else if l.peek(kw::link_native_symbols) { + input.parse::()?; + input.parse::()?; + Ok(Opt::LinkNativeSymbols(input.parse()?)) } else { Err(l.error()) } diff --git a/crates/guest-rust/src/lib.rs b/crates/guest-rust/src/lib.rs index 3efde07ad..d598432ec 100644 --- a/crates/guest-rust/src/lib.rs +++ b/crates/guest-rust/src/lib.rs @@ -891,6 +891,24 @@ extern crate std; /// // structurally equal, which is useful when import and export the same /// // interface. /// merge_structurally_equal_types: true, +/// +/// // Make the same generated bindings usable on a native (non-wasm) +/// // target as well as on wasm32. +/// // +/// // Imports normally compile to `unreachable!()` off wasm32. With this +/// // enabled each one instead calls through a function pointer that a host +/// // installs at load time via a generated +/// // `__wit_bindgen_register_*` symbol, and exports additionally get a +/// // native symbol whose name encodes the characters a linker cannot +/// // accept. Both targets still build from one source. +/// // +/// // The registration symbols are prefixed with a hex-encoded +/// // `/` so that two `generate!` invocations in one crate +/// // don't collide. Binding the *same* world twice in one linkage unit +/// // still does; use `type_section_suffix` to tell them apart. See +/// // `wit_bindgen_rust::Opts::link_native_symbols` for the full list of +/// // symbols a host can expect. +/// link_native_symbols: true, /// }); /// ``` /// diff --git a/crates/guest-rust/src/rt/mod.rs b/crates/guest-rust/src/rt/mod.rs index 349f8006a..889f5a5a5 100644 --- a/crates/guest-rust/src/rt/mod.rs +++ b/crates/guest-rust/src/rt/mod.rs @@ -153,7 +153,7 @@ pub fn maybe_link_cabi_realloc() { /// `cabi_realloc` module above. It's otherwise never explicitly called. /// /// For more information about this see `./ci/rebuild-libwit-bindgen-cabi.sh`. -#[cfg(any(target_env = "p1", target_env = ""))] +#[cfg(any(target_env = "p1", target_env = "", not(target_arch = "wasm32")))] pub unsafe fn cabi_realloc( old_ptr: *mut u8, old_len: usize, diff --git a/crates/rust/src/bindgen.rs b/crates/rust/src/bindgen.rs index 767beb6a9..57fbd8ab4 100644 --- a/crates/rust/src/bindgen.rs +++ b/crates/rust/src/bindgen.rs @@ -67,6 +67,7 @@ impl<'a, 'b> FunctionBindgen<'a, 'b> { &rust_name, params, results, + self.r#gen.r#gen.native_symbols(), )); rust_name } diff --git a/crates/rust/src/interface.rs b/crates/rust/src/interface.rs index fc809bad0..8c07cd3ba 100644 --- a/crates/rust/src/interface.rs +++ b/crates/rust/src/interface.rs @@ -12,7 +12,7 @@ use std::fmt::Write as _; use std::mem; use wit_bindgen_core::abi::{self, AbiVariant, LiftLower}; use wit_bindgen_core::{ - AnonymousTypeGenerator, Source, TypeInfo, dealias, uwrite, uwriteln, wit_parser::*, + AnonymousTypeGenerator, Source, TypeInfo, dealias, symbol_name, uwrite, uwriteln, wit_parser::*, }; pub struct InterfaceGenerator<'a> { @@ -212,6 +212,7 @@ impl<'i> InterfaceGenerator<'i> { "new", &[abi::WasmType::Pointer], &[abi::WasmType::I32], + self.r#gen.native_symbols(), ); let import_rep = crate::declare_import( &wasm_import_module, @@ -219,6 +220,7 @@ impl<'i> InterfaceGenerator<'i> { "rep", &[abi::WasmType::I32], &[abi::WasmType::Pointer], + self.r#gen.native_symbols(), ); uwriteln!( self.src, @@ -347,7 +349,6 @@ macro_rules! {macro_name} {{ }; self.generate_raw_cabi_export(func, &ty, "$($path_to_types)*", async_); } - let export_prefix = self.r#gen.opts.export_prefix.as_deref().unwrap_or(""); for name in resources_to_drop { let module = match self.identifier { Identifier::Interface(_, key) => self.resolve.name_world_key(key), @@ -356,23 +357,25 @@ macro_rules! {macro_name} {{ } }; let camel = name.to_upper_camel_case(); - uwriteln!( - self.src, - r#" - const _: () = {{ - #[doc(hidden)] - #[unsafe(export_name = "{export_prefix}{module}#[dtor]{name}")] - #[allow(non_snake_case)] - unsafe extern "C" fn dtor(rep: *mut u8) {{ - unsafe {{ - $($path_to_types)*::{camel}::dtor::< - <$ty as $($path_to_types)*::Guest>::{camel} - >(rep) + for (cfg, symbol) in self.core_export_symbols(&format!("{module}#[dtor]{name}")) { + uwriteln!( + self.src, + r#" + const _: () = {{ + #[doc(hidden)] + {cfg}#[unsafe(export_name = "{symbol}")] + #[allow(non_snake_case)] + unsafe extern "C" fn dtor(rep: *mut u8) {{ + unsafe {{ + $($path_to_types)*::{camel}::dtor::< + <$ty as $($path_to_types)*::Guest>::{camel} + >(rep) + }} }} - }} - }}; - "# - ); + }}; + "# + ); + } } uwriteln!(self.src, "}};);"); uwriteln!(self.src, "}}"); @@ -1019,6 +1022,7 @@ fn abi_layout(&mut self) -> ::core::alloc::Layout {{ "call", &sig.params, &sig.results, + self.r#gen.native_symbols(), ); let mut args = String::new(); for i in 0..params_lower.len() { @@ -1281,60 +1285,93 @@ unsafe fn call_import(&mut self, _params: Self::ParamsLower, _results: *mut u8) Identifier::World(_) => None, Identifier::StreamOrFuturePayload => unreachable!(), }; - let export_prefix = self.r#gen.opts.export_prefix.as_deref().unwrap_or(""); let export_name = func.legacy_core_export_name(wasm_module_export_name.as_deref()); let export_name = if async_ { format!("[async-lift]{export_name}") } else { export_name.to_string() }; - uwrite!( - self.src, - "\ - #[unsafe(export_name = \"{export_prefix}{export_name}\")] - unsafe extern \"C\" fn export_{name_snake}\ -", - ); - let params = self.print_export_sig(func, async_); - self.push_str(" {\n"); - uwriteln!( - self.src, - "unsafe {{ {path_to_self}::_export_{name_snake}_cabi::<{ty}>({}) }}", - params.join(", ") - ); - self.push_str("}\n"); - - let export_prefix = self.r#gen.opts.export_prefix.as_deref().unwrap_or(""); - if async_ { + for (cfg, symbol) in self.core_export_symbols(&export_name) { uwrite!( self.src, "\ - #[unsafe(export_name = \"{export_prefix}[callback]{export_name}\")] - unsafe extern \"C\" fn _callback_{name_snake}(event0: u32, event1: u32, event2: u32) -> u32 {{ - unsafe {{ - {path_to_self}::__callback_{name_snake}(event0, event1, event2) - }} - }} - " - ); - } else if abi::guest_export_needs_post_return(self.resolve, func) { - uwrite!( - self.src, - "\ - #[unsafe(export_name = \"{export_prefix}cabi_post_{export_name}\")] - unsafe extern \"C\" fn _post_return_{name_snake}\ -" + {cfg}#[unsafe(export_name = \"{symbol}\")] + unsafe extern \"C\" fn export_{name_snake}\ +", ); - let params = self.print_post_return_sig(func); - self.src.push_str("{\n"); + let params = self.print_export_sig(func, async_); + self.push_str(" {\n"); uwriteln!( self.src, - "unsafe {{ {path_to_self}::__post_return_{name_snake}::<{ty}>({}) }}", + "unsafe {{ {path_to_self}::_export_{name_snake}_cabi::<{ty}>({}) }}", params.join(", ") ); - self.src.push_str("}\n"); + self.push_str("}\n"); + } + + if async_ { + for (cfg, symbol) in self.core_export_symbols(&format!("[callback]{export_name}")) { + uwrite!( + self.src, + "\ + {cfg}#[unsafe(export_name = \"{symbol}\")] + unsafe extern \"C\" fn _callback_{name_snake}(event0: u32, event1: u32, event2: u32) -> u32 {{ + unsafe {{ + {path_to_self}::__callback_{name_snake}(event0, event1, event2) + }} + }} + " + ); + } + } else if abi::guest_export_needs_post_return(self.resolve, func) { + for (cfg, symbol) in self.core_export_symbols(&format!("cabi_post_{export_name}")) { + uwrite!( + self.src, + "\ + {cfg}#[unsafe(export_name = \"{symbol}\")] + unsafe extern \"C\" fn _post_return_{name_snake}\ +" + ); + let params = self.print_post_return_sig(func); + self.src.push_str("{\n"); + uwriteln!( + self.src, + "unsafe {{ {path_to_self}::__post_return_{name_snake}::<{ty}>({}) }}", + params.join(", ") + ); + self.src.push_str("}\n"); + } + } + } + + /// Returns each copy of a core export named `export_name` that needs to be + /// emitted, as `(cfg, symbol)`: the `cfg` attribute to gate the copy with + /// and the symbol to export it as. + /// + /// Normally there's just one copy: the canonical ABI name with no `cfg`. + /// With `link_native_symbols` enabled a second, hex-encoded copy is emitted + /// for native targets as well, because native linkers reject the `:`, `/`, + /// `#`, `[` and `]` characters that canonical names contain. Names that + /// survive encoding unchanged (`$root` exports, for instance) are emitted + /// once with no `cfg` rather than twice. + fn core_export_symbols(&self, export_name: &str) -> Vec<(&'static str, String)> { + let prefix = self.r#gen.opts.export_prefix.as_deref().unwrap_or(""); + let wasm = format!("{prefix}{export_name}"); + if self.r#gen.native_symbols().is_none() { + return vec![("", wasm)]; + } + let native = format!( + "{prefix}{}", + symbol_name::make_external_component(export_name) + ); + if native == wasm { + return vec![("", wasm)]; } + vec![ + ("#[cfg(target_arch = \"wasm32\")]\n", wasm), + ("#[cfg(not(target_arch = \"wasm32\"))]\n", native), + ] } fn print_export_sig(&mut self, func: &Function, async_: bool) -> Vec { @@ -2952,6 +2989,7 @@ impl<'a> {camel}Borrow<'a>{{ "drop", &[abi::WasmType::I32], &[], + self.r#gen.native_symbols(), ); uwriteln!( self.src, diff --git a/crates/rust/src/lib.rs b/crates/rust/src/lib.rs index dfe8c2623..280bc3bbd 100644 --- a/crates/rust/src/lib.rs +++ b/crates/rust/src/lib.rs @@ -10,8 +10,8 @@ use std::path::{Path, PathBuf}; use std::str::FromStr; use wit_bindgen_core::abi::{Bitcast, WasmType}; use wit_bindgen_core::{ - AsyncFilterSet, Files, InterfaceGenerator as _, Source, Types, WorldGenerator, dealias, - name_package_module, uwrite, uwriteln, wit_parser::*, + AsyncFilterSet, Files, InterfaceGenerator as _, Source, Types, WorldGenerator, abi, dealias, + name_package_module, symbol_name, uwrite, uwriteln, wit_parser::*, }; mod bindgen; @@ -47,6 +47,12 @@ pub struct RustWasm { used_member_attr_selectors: HashSet, world: Option, + /// Prefix applied to all native linkage symbols (`Some` iff + /// `opts.link_native_symbols` is set). This namespaces the symbols by + /// world so that two `generate!` invocations in the same crate don't + /// collide, see `RustWasm::native_symbols`. + native_symbols: Option, + rt_module: IndexSet, export_macros: Vec<(String, String)>, @@ -348,6 +354,32 @@ pub struct Opts { /// If true, methods normally returning `()` instead return `&Self`. This applies to both imported and exported methods. #[cfg_attr(feature = "clap", arg(long))] pub enable_method_chaining: bool, + + /// If true, make the generated bindings usable on native (non-`wasm32`) + /// targets in addition to `wasm32`, rather than stubbing every import out + /// with `unreachable!()`. + /// + /// Canonical ABI symbol names contain characters native linkers reject + /// (`:`, `/`, `#`, ...), so off `wasm32` all symbols are hex-encoded with + /// the same scheme the C++ generator uses (see + /// `wit_bindgen_core::symbol_name`): + /// + /// * Each **import** calls through a function pointer that the host + /// installs at load time via a generated + /// `__wit_bindgen_register_` hook taking the import's + /// core signature. Imports aren't resolved by the linker, so a host + /// only registers what it implements; calling an unregistered import + /// aborts with a message naming both symbols. + /// * Each **export** (including post-return, async callbacks and resource + /// destructors) is additionally exported under its hex-encoded core + /// export name. + /// + /// The `` prefix is a hex-encoded + /// `/`, so distinct worlds in one + /// crate don't collide. Binding the *same* world twice still does; set + /// `type_section_suffix` to disambiguate. + #[cfg_attr(feature = "clap", arg(long))] + pub link_native_symbols: bool, } impl Opts { @@ -479,6 +511,10 @@ impl RustWasm { .unwrap_or("wit_bindgen::rt") } + fn native_symbols(&self) -> Option<&str> { + self.native_symbols.as_deref() + } + fn map_type_path(&self) -> String { self.opts .map_type @@ -549,6 +585,30 @@ impl RustWasm { Ok(remapped) } + fn finish_native_cabi_realloc(&mut self) { + let Some(prefix) = self.native_symbols().map(str::to_string) else { + return; + }; + let rt = self.runtime_path().to_string(); + let name = format!("__wit_bindgen_cabi_realloc_{prefix}"); + uwriteln!( + self.src, + r#" +#[cfg(not(target_arch = "wasm32"))] +#[unsafe(no_mangle)] +#[allow(non_snake_case)] +pub unsafe extern "C" fn {name}( + old_ptr: *mut u8, + old_len: usize, + align: usize, + new_len: usize, +) -> *mut u8 {{ + unsafe {{ {rt}::cabi_realloc(old_ptr, old_len, align, new_len) }} +}} +"# + ); + } + fn finish_runtime_module(&mut self) { if !self.rt_module.is_empty() { // As above, disable rustfmt, as we use prettyplease. @@ -1263,6 +1323,17 @@ impl WorldGenerator for RustWasm { }); self.world = Some(world); + self.native_symbols = self.opts.link_native_symbols.then(|| { + let w = &resolve.worlds[world]; + let pkg = w + .package + .map(|p| resolve.packages[p].name.to_string()) + .unwrap_or_default(); + let suffix = self.opts.type_section_suffix.as_deref().unwrap_or(""); + let name = format!("{pkg}/{}{suffix}", w.name); + format!("{}_", symbol_name::make_external_component(&name)) + }); + let world = &resolve.worlds[world]; // Specify that all imports local to the world's package should be // generated @@ -1491,6 +1562,8 @@ impl WorldGenerator for RustWasm { let exports = mem::take(&mut self.export_modules); self.emit_modules(exports); + self.finish_native_cabi_realloc(); + self.finish_runtime_module(); self.finish_export_macro(resolve, world); @@ -1864,6 +1937,7 @@ fn declare_import( rust_name: &str, params: &[WasmType], results: &[WasmType], + native_prefix: Option<&str>, ) -> String { let mut sig = "(".to_owned(); for param in params.iter() { @@ -1877,6 +1951,62 @@ fn declare_import( sig.push_str(" -> "); sig.push_str(wasm_type(*result)); } + + let non_wasm = if let Some(prefix) = native_prefix { + let symbol = symbol_name::make_external_symbol( + wasm_import_module, + wasm_import_name, + abi::AbiVariant::GuestImport, + ); + let ptr_static = format!("__WIT_BINDGEN_IMPORT_{prefix}{symbol}"); + let register_name = format!("__wit_bindgen_register_{prefix}{symbol}"); + let named_params: Vec = params + .iter() + .enumerate() + .map(|(i, ty)| format!("arg{i}: {}", wasm_type(*ty))) + .collect(); + let ret_sig = results + .first() + .map(|r| format!(" -> {}", wasm_type(*r))) + .unwrap_or_default(); + let call_args = (0..params.len()) + .map(|i| format!("arg{i}")) + .collect::>() + .join(", "); + let named_params_str = named_params.join(", "); + + format!( + r#"#[cfg(not(target_arch = "wasm32"))] + #[allow(non_upper_case_globals)] + static {ptr_static}: ::core::sync::atomic::AtomicPtr<()> = + ::core::sync::atomic::AtomicPtr::new(::core::ptr::null_mut()); + + #[cfg(not(target_arch = "wasm32"))] + #[unsafe(no_mangle)] + #[allow(non_snake_case)] + pub unsafe extern "C" fn {register_name}(func: unsafe extern "C" fn{sig}) {{ + {ptr_static}.store(func as *mut (), ::core::sync::atomic::Ordering::Release); + }} + + #[cfg(not(target_arch = "wasm32"))] + unsafe extern "C" fn {rust_name}({named_params_str}){ret_sig} {{ + let ptr = {ptr_static}.load(::core::sync::atomic::Ordering::Acquire); + assert!( + !ptr.is_null(), + "import `{wasm_import_module}#{wasm_import_name}` was called before the host \ + registered an implementation for it via `{register_name}`" + ); + let f: unsafe extern "C" fn{sig} = unsafe {{ ::core::mem::transmute(ptr) }}; + unsafe {{ f({call_args}) }} + }}"#, + ) + } else { + format!( + r#"#[cfg(not(target_arch = "wasm32"))] + unsafe extern "C" fn {rust_name}{sig} {{ unreachable!() }}"# + ) + }; + format!( " #[cfg(target_arch = \"wasm32\")] @@ -1886,8 +2016,7 @@ fn declare_import( fn {rust_name}{sig}; }} - #[cfg(not(target_arch = \"wasm32\"))] - unsafe extern \"C\" fn {rust_name}{sig} {{ unreachable!() }} + {non_wasm} " ) } diff --git a/crates/rust/tests/codegen.rs b/crates/rust/tests/codegen.rs index e8046ceaf..1b19d189b 100644 --- a/crates/rust/tests/codegen.rs +++ b/crates/rust/tests/codegen.rs @@ -403,3 +403,169 @@ mod versioned_selectors { assert!(Alpha { x: 1 } < Alpha { x: 2 }); } } + +#[allow(unused, reason = "testing codegen, not functionality")] +mod link_native_symbols { + wit_bindgen::generate!({ + inline: r#" + package test:native; + + interface operations { + resource thing { + constructor(x: u32); + get: func() -> u32; + } + add: func(a: u32, b: u32) -> u32; + describe: func(value: u32) -> string; + } + + world test { + import operations; + export operations; + } + "#, + generate_all, + link_native_symbols: true, + }); + + // Covers the resource destructor and post-return exports, both of which + // need native symbol names of their own. + struct Component; + + impl exports::test::native::operations::Guest for Component { + type Thing = MyThing; + + fn add(a: u32, b: u32) -> u32 { + a + b + } + + fn describe(value: u32) -> String { + value.to_string() + } + } + + struct MyThing(u32); + + impl exports::test::native::operations::GuestThing for MyThing { + fn new(x: u32) -> Self { + MyThing(x) + } + + fn get(&self) -> u32 { + self.0 + } + } + + export!(Component); +} + +#[allow(unused, reason = "testing codegen, not functionality")] +mod link_native_symbols_root { + wit_bindgen::generate!({ + inline: r#" + package test:native-root; + + world test { + import an-import: func(a: u32) -> u32; + export an-export: func(a: u32) -> u32; + } + "#, + generate_all, + link_native_symbols: true, + }); + + struct Component; + + impl Guest for Component { + fn an_export(a: u32) -> u32 { + a + } + } + + export!(Component); +} + +#[allow(unused, reason = "testing codegen, not functionality")] +mod link_native_symbols_async { + wit_bindgen::generate!({ + inline: r#" + package test:native-async; + + interface operations { + describe: func(value: u32) -> string; + } + + world test { + import operations; + export operations; + } + "#, + generate_all, + link_native_symbols: true, + async: true, + }); + + struct Component; + + impl exports::test::native_async::operations::Guest for Component { + async fn describe(value: u32) -> String { + value.to_string() + } + } + + export!(Component); +} + +#[allow(unused, reason = "testing codegen, not functionality")] +mod link_native_symbols_shared_one { + wit_bindgen::generate!({ + inline: r#" + package test:native-shared; + interface operations { add: func(a: u32, b: u32) -> u32; } + world one { import operations; } + "#, + generate_all, + link_native_symbols: true, + }); +} + +#[allow(unused, reason = "testing codegen, not functionality")] +mod link_native_symbols_shared_two { + wit_bindgen::generate!({ + inline: r#" + package test:native-shared; + interface operations { add: func(a: u32, b: u32) -> u32; } + world two { import operations; } + "#, + generate_all, + link_native_symbols: true, + }); +} + +#[allow(unused, reason = "testing codegen, not functionality")] +mod link_native_symbols_same_world_one { + wit_bindgen::generate!({ + inline: r#" + package test:native-same; + interface operations { add: func(a: u32, b: u32) -> u32; } + world same { import operations; } + "#, + generate_all, + link_native_symbols: true, + type_section_suffix: "-one", + }); +} + +#[allow(unused, reason = "testing codegen, not functionality")] +mod link_native_symbols_same_world_two { + wit_bindgen::generate!({ + inline: r#" + package test:native-same; + interface operations { add: func(a: u32, b: u32) -> u32; } + world same { import operations; } + "#, + generate_all, + link_native_symbols: true, + type_section_suffix: "-two", + }); +} From f2ddae485d270865cf27822f10be71a812a08999 Mon Sep 17 00:00:00 2001 From: Bjorn Beishline <75190918+BjornTheProgrammer@users.noreply.github.com> Date: Fri, 21 Aug 2026 22:28:50 -0700 Subject: [PATCH 2/2] feat(rust) make link_native_symbols default --- crates/guest-rust/macro/src/lib.rs | 9 -- crates/guest-rust/src/lib.rs | 51 ++++++---- crates/rust/src/interface.rs | 151 +++++++++++++---------------- crates/rust/src/lib.rs | 125 +++++++++--------------- crates/rust/tests/codegen.rs | 68 +------------ 5 files changed, 147 insertions(+), 257 deletions(-) diff --git a/crates/guest-rust/macro/src/lib.rs b/crates/guest-rust/macro/src/lib.rs index be9077d4c..603222a77 100644 --- a/crates/guest-rust/macro/src/lib.rs +++ b/crates/guest-rust/macro/src/lib.rs @@ -175,9 +175,6 @@ impl Parse for Config { Opt::MergeStructurallyEqualTypes(enable) => { opts.merge_structurally_equal_types = Some(Some(enable.value())) } - Opt::LinkNativeSymbols(enable) => { - opts.link_native_symbols = enable.value(); - } } } } else { @@ -335,7 +332,6 @@ mod kw { syn::custom_keyword!(debug); syn::custom_keyword!(enable_method_chaining); syn::custom_keyword!(merge_structurally_equal_types); - syn::custom_keyword!(link_native_symbols); } #[derive(Clone)] @@ -420,7 +416,6 @@ enum Opt { Debug(syn::LitBool), EnableMethodChaining(syn::LitBool), MergeStructurallyEqualTypes(syn::LitBool), - LinkNativeSymbols(syn::LitBool), } impl Parse for Opt { @@ -628,10 +623,6 @@ impl Parse for Opt { input.parse::()?; input.parse::()?; Ok(Opt::MergeStructurallyEqualTypes(input.parse()?)) - } else if l.peek(kw::link_native_symbols) { - input.parse::()?; - input.parse::()?; - Ok(Opt::LinkNativeSymbols(input.parse()?)) } else { Err(l.error()) } diff --git a/crates/guest-rust/src/lib.rs b/crates/guest-rust/src/lib.rs index d598432ec..8d4ec6a2d 100644 --- a/crates/guest-rust/src/lib.rs +++ b/crates/guest-rust/src/lib.rs @@ -891,27 +891,42 @@ extern crate std; /// // structurally equal, which is useful when import and export the same /// // interface. /// merge_structurally_equal_types: true, -/// -/// // Make the same generated bindings usable on a native (non-wasm) -/// // target as well as on wasm32. -/// // -/// // Imports normally compile to `unreachable!()` off wasm32. With this -/// // enabled each one instead calls through a function pointer that a host -/// // installs at load time via a generated -/// // `__wit_bindgen_register_*` symbol, and exports additionally get a -/// // native symbol whose name encodes the characters a linker cannot -/// // accept. Both targets still build from one source. -/// // -/// // The registration symbols are prefixed with a hex-encoded -/// // `/` so that two `generate!` invocations in one crate -/// // don't collide. Binding the *same* world twice in one linkage unit -/// // still does; use `type_section_suffix` to tell them apart. See -/// // `wit_bindgen_rust::Opts::link_native_symbols` for the full list of -/// // symbols a host can expect. -/// link_native_symbols: true, /// }); /// ``` /// +/// ## Native (non-WebAssembly) targets +/// +/// Generated bindings also compile for native targets, which is useful for +/// testing component code without a wasm runtime or for building it as a +/// `cdylib` plugin. Native linkers don't accept the `:`, `/`, `#`, `[` and +/// `]` characters that canonical ABI symbol names use, so on native targets +/// symbols are hex-encoded with the scheme in +/// `wit_bindgen_core::symbol_name` (the same one the C++ generator uses). +/// +/// Imports are not resolved by the native linker. Each import calls through +/// a function pointer that starts out null, and a host provides an +/// implementation at load time by calling the generated +/// `__wit_bindgen_register_` function with a function pointer +/// of the import's core signature (`` here is +/// `make_external_symbol(module, name, GuestImport)`). This means everything +/// links whether or not a host is present: a host only needs to register the +/// imports it actually implements, and calling an import that was never +/// registered aborts with a message naming the import and its registration +/// function. +/// +/// Exports, including post-return functions, async callbacks, and resource +/// destructors, are exported under their hex-encoded core export names. A +/// `__wit_bindgen_cabi_realloc_` function is also exported so hosts +/// can allocate guest-owned memory when lowering arguments, as the canonical +/// ABI requires. +/// +/// The `` prefix above is a hex-encoded +/// `/`, which keeps two `generate!` +/// invocations in one binary from defining the same symbols. Note that +/// binding the same world twice in one native binary will fail to link with +/// duplicate symbols unless `type_section_suffix` is used to tell the two +/// apart. +/// /// [WIT package]: https://component-model.bytecodealliance.org/design/packages.html #[cfg(feature = "macros")] pub use wit_bindgen_rust_macro::generate; diff --git a/crates/rust/src/interface.rs b/crates/rust/src/interface.rs index 8c07cd3ba..b82444b17 100644 --- a/crates/rust/src/interface.rs +++ b/crates/rust/src/interface.rs @@ -357,25 +357,23 @@ macro_rules! {macro_name} {{ } }; let camel = name.to_upper_camel_case(); - for (cfg, symbol) in self.core_export_symbols(&format!("{module}#[dtor]{name}")) { - uwriteln!( - self.src, - r#" - const _: () = {{ - #[doc(hidden)] - {cfg}#[unsafe(export_name = "{symbol}")] - #[allow(non_snake_case)] - unsafe extern "C" fn dtor(rep: *mut u8) {{ - unsafe {{ - $($path_to_types)*::{camel}::dtor::< - <$ty as $($path_to_types)*::Guest>::{camel} - >(rep) - }} + let attrs = self.core_export_attrs(&format!("{module}#[dtor]{name}")); + uwriteln!( + self.src, + r#" + const _: () = {{ + #[doc(hidden)] + {attrs}#[allow(non_snake_case)] + unsafe extern "C" fn dtor(rep: *mut u8) {{ + unsafe {{ + $($path_to_types)*::{camel}::dtor::< + <$ty as $($path_to_types)*::Guest>::{camel} + >(rep) }} - }}; - "# - ); - } + }} + }}; + "# + ); } uwriteln!(self.src, "}};);"); uwriteln!(self.src, "}}"); @@ -1292,86 +1290,69 @@ unsafe fn call_import(&mut self, _params: Self::ParamsLower, _results: *mut u8) export_name.to_string() }; - for (cfg, symbol) in self.core_export_symbols(&export_name) { + let attrs = self.core_export_attrs(&export_name); + uwrite!( + self.src, + "\ + {attrs}unsafe extern \"C\" fn export_{name_snake}\ +", + ); + let params = self.print_export_sig(func, async_); + self.push_str(" {\n"); + uwriteln!( + self.src, + "unsafe {{ {path_to_self}::_export_{name_snake}_cabi::<{ty}>({}) }}", + params.join(", ") + ); + self.push_str("}\n"); + + if async_ { + let attrs = self.core_export_attrs(&format!("[callback]{export_name}")); uwrite!( self.src, "\ - {cfg}#[unsafe(export_name = \"{symbol}\")] - unsafe extern \"C\" fn export_{name_snake}\ -", + {attrs}unsafe extern \"C\" fn _callback_{name_snake}(event0: u32, event1: u32, event2: u32) -> u32 {{ + unsafe {{ + {path_to_self}::__callback_{name_snake}(event0, event1, event2) + }} + }} + " ); - let params = self.print_export_sig(func, async_); - self.push_str(" {\n"); + } else if abi::guest_export_needs_post_return(self.resolve, func) { + let attrs = self.core_export_attrs(&format!("cabi_post_{export_name}")); + uwrite!( + self.src, + "\ + {attrs}unsafe extern \"C\" fn _post_return_{name_snake}\ +" + ); + let params = self.print_post_return_sig(func); + self.src.push_str("{\n"); uwriteln!( self.src, - "unsafe {{ {path_to_self}::_export_{name_snake}_cabi::<{ty}>({}) }}", + "unsafe {{ {path_to_self}::__post_return_{name_snake}::<{ty}>({}) }}", params.join(", ") ); - self.push_str("}\n"); - } - - if async_ { - for (cfg, symbol) in self.core_export_symbols(&format!("[callback]{export_name}")) { - uwrite!( - self.src, - "\ - {cfg}#[unsafe(export_name = \"{symbol}\")] - unsafe extern \"C\" fn _callback_{name_snake}(event0: u32, event1: u32, event2: u32) -> u32 {{ - unsafe {{ - {path_to_self}::__callback_{name_snake}(event0, event1, event2) - }} - }} - " - ); - } - } else if abi::guest_export_needs_post_return(self.resolve, func) { - for (cfg, symbol) in self.core_export_symbols(&format!("cabi_post_{export_name}")) { - uwrite!( - self.src, - "\ - {cfg}#[unsafe(export_name = \"{symbol}\")] - unsafe extern \"C\" fn _post_return_{name_snake}\ -" - ); - let params = self.print_post_return_sig(func); - self.src.push_str("{\n"); - uwriteln!( - self.src, - "unsafe {{ {path_to_self}::__post_return_{name_snake}::<{ty}>({}) }}", - params.join(", ") - ); - self.src.push_str("}\n"); - } + self.src.push_str("}\n"); } } - /// Returns each copy of a core export named `export_name` that needs to be - /// emitted, as `(cfg, symbol)`: the `cfg` attribute to gate the copy with - /// and the symbol to export it as. + /// Returns the `export_name` attributes for a core export named + /// `export_name`. /// - /// Normally there's just one copy: the canonical ABI name with no `cfg`. - /// With `link_native_symbols` enabled a second, hex-encoded copy is emitted - /// for native targets as well, because native linkers reject the `:`, `/`, - /// `#`, `[` and `]` characters that canonical names contain. Names that - /// survive encoding unchanged (`$root` exports, for instance) are emitted - /// once with no `cfg` rather than twice. - fn core_export_symbols(&self, export_name: &str) -> Vec<(&'static str, String)> { + /// Has to exist due to the fact that native names cannot contain + /// special characters that wasm32 can like '/'. + /// + /// `cfg_attr` conditions are mutually exclusive, so exactly one attribute + /// applies on any target (for names that survive encoding unchanged, such + /// as `$root` exports, both carry the same string). + fn core_export_attrs(&self, export_name: &str) -> String { let prefix = self.r#gen.opts.export_prefix.as_deref().unwrap_or(""); - let wasm = format!("{prefix}{export_name}"); - if self.r#gen.native_symbols().is_none() { - return vec![("", wasm)]; - } - let native = format!( - "{prefix}{}", - symbol_name::make_external_component(export_name) - ); - if native == wasm { - return vec![("", wasm)]; - } - vec![ - ("#[cfg(target_arch = \"wasm32\")]\n", wasm), - ("#[cfg(not(target_arch = \"wasm32\"))]\n", native), - ] + let native = symbol_name::make_external_component(export_name); + format!( + "#[cfg_attr(target_arch = \"wasm32\", unsafe(export_name = \"{prefix}{export_name}\"))]\n\ + #[cfg_attr(not(target_arch = \"wasm32\"), unsafe(export_name = \"{prefix}{native}\"))]\n" + ) } fn print_export_sig(&mut self, func: &Function, async_: bool) -> Vec { diff --git a/crates/rust/src/lib.rs b/crates/rust/src/lib.rs index 280bc3bbd..ec24a7331 100644 --- a/crates/rust/src/lib.rs +++ b/crates/rust/src/lib.rs @@ -47,10 +47,10 @@ pub struct RustWasm { used_member_attr_selectors: HashSet, world: Option, - /// Prefix applied to all native linkage symbols (`Some` iff - /// `opts.link_native_symbols` is set). This namespaces the symbols by - /// world so that two `generate!` invocations in the same crate don't - /// collide, see `RustWasm::native_symbols`. + /// Prefix applied to all native linkage symbols, set during `preprocess`. + /// This namespaces the symbols by world so that two `generate!` + /// invocations in the same crate don't collide, see + /// `RustWasm::native_symbols`. native_symbols: Option, rt_module: IndexSet, @@ -354,32 +354,6 @@ pub struct Opts { /// If true, methods normally returning `()` instead return `&Self`. This applies to both imported and exported methods. #[cfg_attr(feature = "clap", arg(long))] pub enable_method_chaining: bool, - - /// If true, make the generated bindings usable on native (non-`wasm32`) - /// targets in addition to `wasm32`, rather than stubbing every import out - /// with `unreachable!()`. - /// - /// Canonical ABI symbol names contain characters native linkers reject - /// (`:`, `/`, `#`, ...), so off `wasm32` all symbols are hex-encoded with - /// the same scheme the C++ generator uses (see - /// `wit_bindgen_core::symbol_name`): - /// - /// * Each **import** calls through a function pointer that the host - /// installs at load time via a generated - /// `__wit_bindgen_register_` hook taking the import's - /// core signature. Imports aren't resolved by the linker, so a host - /// only registers what it implements; calling an unregistered import - /// aborts with a message naming both symbols. - /// * Each **export** (including post-return, async callbacks and resource - /// destructors) is additionally exported under its hex-encoded core - /// export name. - /// - /// The `` prefix is a hex-encoded - /// `/`, so distinct worlds in one - /// crate don't collide. Binding the *same* world twice still does; set - /// `type_section_suffix` to disambiguate. - #[cfg_attr(feature = "clap", arg(long))] - pub link_native_symbols: bool, } impl Opts { @@ -511,8 +485,10 @@ impl RustWasm { .unwrap_or("wit_bindgen::rt") } - fn native_symbols(&self) -> Option<&str> { - self.native_symbols.as_deref() + fn native_symbols(&self) -> &str { + self.native_symbols + .as_deref() + .expect("native symbol prefix is set during preprocess") } fn map_type_path(&self) -> String { @@ -586,9 +562,7 @@ impl RustWasm { } fn finish_native_cabi_realloc(&mut self) { - let Some(prefix) = self.native_symbols().map(str::to_string) else { - return; - }; + let prefix = self.native_symbols().to_string(); let rt = self.runtime_path().to_string(); let name = format!("__wit_bindgen_cabi_realloc_{prefix}"); uwriteln!( @@ -1323,7 +1297,7 @@ impl WorldGenerator for RustWasm { }); self.world = Some(world); - self.native_symbols = self.opts.link_native_symbols.then(|| { + self.native_symbols = Some({ let w = &resolve.worlds[world]; let pkg = w .package @@ -1937,7 +1911,7 @@ fn declare_import( rust_name: &str, params: &[WasmType], results: &[WasmType], - native_prefix: Option<&str>, + native_prefix: &str, ) -> String { let mut sig = "(".to_owned(); for param in params.iter() { @@ -1952,31 +1926,38 @@ fn declare_import( sig.push_str(wasm_type(*result)); } - let non_wasm = if let Some(prefix) = native_prefix { - let symbol = symbol_name::make_external_symbol( - wasm_import_module, - wasm_import_name, - abi::AbiVariant::GuestImport, - ); - let ptr_static = format!("__WIT_BINDGEN_IMPORT_{prefix}{symbol}"); - let register_name = format!("__wit_bindgen_register_{prefix}{symbol}"); - let named_params: Vec = params - .iter() - .enumerate() - .map(|(i, ty)| format!("arg{i}: {}", wasm_type(*ty))) - .collect(); - let ret_sig = results - .first() - .map(|r| format!(" -> {}", wasm_type(*r))) - .unwrap_or_default(); - let call_args = (0..params.len()) - .map(|i| format!("arg{i}")) - .collect::>() - .join(", "); - let named_params_str = named_params.join(", "); - - format!( - r#"#[cfg(not(target_arch = "wasm32"))] + let symbol = symbol_name::make_external_symbol( + wasm_import_module, + wasm_import_name, + abi::AbiVariant::GuestImport, + ); + let ptr_static = format!("__WIT_BINDGEN_IMPORT_{native_prefix}{symbol}"); + let register_name = format!("__wit_bindgen_register_{native_prefix}{symbol}"); + let named_params: Vec = params + .iter() + .enumerate() + .map(|(i, ty)| format!("arg{i}: {}", wasm_type(*ty))) + .collect(); + let ret_sig = results + .first() + .map(|r| format!(" -> {}", wasm_type(*r))) + .unwrap_or_default(); + let call_args = (0..params.len()) + .map(|i| format!("arg{i}")) + .collect::>() + .join(", "); + let named_params_str = named_params.join(", "); + + format!( + r#" + #[cfg(target_arch = "wasm32")] + #[link(wasm_import_module = "{wasm_import_module}")] + unsafe extern "C" {{ + #[link_name = "{wasm_import_name}"] + fn {rust_name}{sig}; + }} + + #[cfg(not(target_arch = "wasm32"))] #[allow(non_upper_case_globals)] static {ptr_static}: ::core::sync::atomic::AtomicPtr<()> = ::core::sync::atomic::AtomicPtr::new(::core::ptr::null_mut()); @@ -1998,26 +1979,8 @@ fn declare_import( ); let f: unsafe extern "C" fn{sig} = unsafe {{ ::core::mem::transmute(ptr) }}; unsafe {{ f({call_args}) }} - }}"#, - ) - } else { - format!( - r#"#[cfg(not(target_arch = "wasm32"))] - unsafe extern "C" fn {rust_name}{sig} {{ unreachable!() }}"# - ) - }; - - format!( - " - #[cfg(target_arch = \"wasm32\")] - #[link(wasm_import_module = \"{wasm_import_module}\")] - unsafe extern \"C\" {{ - #[link_name = \"{wasm_import_name}\"] - fn {rust_name}{sig}; }} - - {non_wasm} - " + "#, ) } diff --git a/crates/rust/tests/codegen.rs b/crates/rust/tests/codegen.rs index 1b19d189b..79ca429fb 100644 --- a/crates/rust/tests/codegen.rs +++ b/crates/rust/tests/codegen.rs @@ -405,7 +405,7 @@ mod versioned_selectors { } #[allow(unused, reason = "testing codegen, not functionality")] -mod link_native_symbols { +mod native_symbols { wit_bindgen::generate!({ inline: r#" package test:native; @@ -425,11 +425,8 @@ mod link_native_symbols { } "#, generate_all, - link_native_symbols: true, }); - // Covers the resource destructor and post-return exports, both of which - // need native symbol names of their own. struct Component; impl exports::test::native::operations::Guest for Component { @@ -460,33 +457,7 @@ mod link_native_symbols { } #[allow(unused, reason = "testing codegen, not functionality")] -mod link_native_symbols_root { - wit_bindgen::generate!({ - inline: r#" - package test:native-root; - - world test { - import an-import: func(a: u32) -> u32; - export an-export: func(a: u32) -> u32; - } - "#, - generate_all, - link_native_symbols: true, - }); - - struct Component; - - impl Guest for Component { - fn an_export(a: u32) -> u32 { - a - } - } - - export!(Component); -} - -#[allow(unused, reason = "testing codegen, not functionality")] -mod link_native_symbols_async { +mod native_symbols_async { wit_bindgen::generate!({ inline: r#" package test:native-async; @@ -501,7 +472,6 @@ mod link_native_symbols_async { } "#, generate_all, - link_native_symbols: true, async: true, }); @@ -517,7 +487,7 @@ mod link_native_symbols_async { } #[allow(unused, reason = "testing codegen, not functionality")] -mod link_native_symbols_shared_one { +mod native_symbols_shared_one { wit_bindgen::generate!({ inline: r#" package test:native-shared; @@ -525,12 +495,11 @@ mod link_native_symbols_shared_one { world one { import operations; } "#, generate_all, - link_native_symbols: true, }); } #[allow(unused, reason = "testing codegen, not functionality")] -mod link_native_symbols_shared_two { +mod native_symbols_shared_two { wit_bindgen::generate!({ inline: r#" package test:native-shared; @@ -538,34 +507,5 @@ mod link_native_symbols_shared_two { world two { import operations; } "#, generate_all, - link_native_symbols: true, - }); -} - -#[allow(unused, reason = "testing codegen, not functionality")] -mod link_native_symbols_same_world_one { - wit_bindgen::generate!({ - inline: r#" - package test:native-same; - interface operations { add: func(a: u32, b: u32) -> u32; } - world same { import operations; } - "#, - generate_all, - link_native_symbols: true, - type_section_suffix: "-one", - }); -} - -#[allow(unused, reason = "testing codegen, not functionality")] -mod link_native_symbols_same_world_two { - wit_bindgen::generate!({ - inline: r#" - package test:native-same; - interface operations { add: func(a: u32, b: u32) -> u32; } - world same { import operations; } - "#, - generate_all, - link_native_symbols: true, - type_section_suffix: "-two", }); }