From ced97de1e87fec6789ad5a2a80ddf691fac7b1d9 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/4] 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 | 136 ++++++++++++++++++- crates/rust/tests/codegen.rs | 166 ++++++++++++++++++++++++ 10 files changed, 427 insertions(+), 64 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 b03c173dd..ed0e16d81 100644 --- a/crates/core/src/lib.rs +++ b/crates/core/src/lib.rs @@ -16,6 +16,7 @@ mod async_; pub use async_::AsyncFilterSet; mod chainable_method; pub use chainable_method::{ChainableMethodFilterSet, ChainingMode}; +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 7c7cc214b..35c31b656 100644 --- a/crates/guest-rust/macro/src/lib.rs +++ b/crates/guest-rust/macro/src/lib.rs @@ -183,6 +183,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 { @@ -340,6 +343,7 @@ mod kw { syn::custom_keyword!(debug); syn::custom_keyword!(chainable_methods); syn::custom_keyword!(merge_structurally_equal_types); + syn::custom_keyword!(link_native_symbols); } #[derive(Clone)] @@ -424,6 +428,7 @@ enum Opt { Debug(syn::LitBool), ChainableMethods(ChainableMethodFilterSet, Span), MergeStructurallyEqualTypes(syn::LitBool), + LinkNativeSymbols(syn::LitBool), } impl Parse for Opt { @@ -638,6 +643,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 b9dbe2946..c099c3d9b 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 70aeb67a5..e7a4877aa 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 628142d19..b665ea9a8 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, ChainingMode, Source, TypeInfo, dealias, uwrite, uwriteln, + AnonymousTypeGenerator, ChainingMode, Source, TypeInfo, dealias, symbol_name, uwrite, uwriteln, wit_parser::*, }; @@ -218,6 +218,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, @@ -225,6 +226,7 @@ impl<'i> InterfaceGenerator<'i> { "rep", &[abi::WasmType::I32], &[abi::WasmType::Pointer], + self.r#gen.native_symbols(), ); uwriteln!( self.src, @@ -353,7 +355,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), @@ -362,23 +363,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, "}}"); @@ -1036,6 +1039,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() { @@ -1298,60 +1302,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 { @@ -2987,6 +3024,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 33bf3806a..eb2781c4c 100644 --- a/crates/rust/src/lib.rs +++ b/crates/rust/src/lib.rs @@ -11,7 +11,8 @@ use std::str::FromStr; use wit_bindgen_core::abi::{Bitcast, WasmType}; use wit_bindgen_core::{ AsyncFilterSet, ChainableMethodFilterSet, ChainingMode, Files, InterfaceGenerator as _, Source, - Types, WorldGenerator, dealias, name_package_module, uwrite, uwriteln, wit_parser::*, + Types, WorldGenerator, abi, dealias, name_package_module, symbol_name, uwrite, uwriteln, + wit_parser::*, }; mod bindgen; @@ -47,6 +48,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 +355,32 @@ pub struct Opts { #[cfg_attr(feature = "clap", clap(flatten))] #[cfg_attr(feature = "serde", serde(flatten))] pub chainable_methods: ChainableMethodFilterSet, + + /// 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 +512,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 +586,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. @@ -1273,6 +1334,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 @@ -1501,6 +1573,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); @@ -1881,6 +1955,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() { @@ -1894,6 +1969,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\")] @@ -1903,8 +2034,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 5cc96cf42..efc50d4e9 100644 --- a/crates/rust/tests/codegen.rs +++ b/crates/rust/tests/codegen.rs @@ -422,3 +422,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 782f400da3c9b69df224fa24c5b7c98071b72941 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/4] 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 35c31b656..7c7cc214b 100644 --- a/crates/guest-rust/macro/src/lib.rs +++ b/crates/guest-rust/macro/src/lib.rs @@ -183,9 +183,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 { @@ -343,7 +340,6 @@ mod kw { syn::custom_keyword!(debug); syn::custom_keyword!(chainable_methods); syn::custom_keyword!(merge_structurally_equal_types); - syn::custom_keyword!(link_native_symbols); } #[derive(Clone)] @@ -428,7 +424,6 @@ enum Opt { Debug(syn::LitBool), ChainableMethods(ChainableMethodFilterSet, Span), MergeStructurallyEqualTypes(syn::LitBool), - LinkNativeSymbols(syn::LitBool), } impl Parse for Opt { @@ -643,10 +638,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 b665ea9a8..697e67fed 100644 --- a/crates/rust/src/interface.rs +++ b/crates/rust/src/interface.rs @@ -363,25 +363,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, "}}"); @@ -1309,86 +1307,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 eb2781c4c..f1e494d46 100644 --- a/crates/rust/src/lib.rs +++ b/crates/rust/src/lib.rs @@ -48,10 +48,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, @@ -355,32 +355,6 @@ pub struct Opts { #[cfg_attr(feature = "clap", clap(flatten))] #[cfg_attr(feature = "serde", serde(flatten))] pub chainable_methods: ChainableMethodFilterSet, - - /// 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 { @@ -512,8 +486,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 { @@ -587,9 +563,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!( @@ -1334,7 +1308,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 @@ -1955,7 +1929,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() { @@ -1970,31 +1944,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()); @@ -2016,26 +1997,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 efc50d4e9..462125f9f 100644 --- a/crates/rust/tests/codegen.rs +++ b/crates/rust/tests/codegen.rs @@ -424,7 +424,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; @@ -444,11 +444,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 { @@ -479,33 +476,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; @@ -520,7 +491,6 @@ mod link_native_symbols_async { } "#, generate_all, - link_native_symbols: true, async: true, }); @@ -536,7 +506,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; @@ -544,12 +514,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; @@ -557,34 +526,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", }); } From 3f87282d7b85b5fe8f9a5f0ae9c26facbb1c2fb3 Mon Sep 17 00:00:00 2001 From: Bjorn Beishline <75190918+BjornTheProgrammer@users.noreply.github.com> Date: Sat, 22 Aug 2026 00:11:33 -0700 Subject: [PATCH 3/4] a --- crates/guest-rust/src/lib.rs | 33 +++++------- crates/guest-rust/src/rt/mod.rs | 87 ++++++++++++++++++++++++++++++ crates/rust/src/bindgen.rs | 2 +- crates/rust/src/interface.rs | 8 +-- crates/rust/src/lib.rs | 96 ++++++++------------------------- crates/rust/tests/codegen.rs | 28 ++-------- 6 files changed, 132 insertions(+), 122 deletions(-) diff --git a/crates/guest-rust/src/lib.rs b/crates/guest-rust/src/lib.rs index 8d4ec6a2d..01d4296e5 100644 --- a/crates/guest-rust/src/lib.rs +++ b/crates/guest-rust/src/lib.rs @@ -904,28 +904,23 @@ extern crate std; /// `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. +/// a function pointer that starts out null and is looked up on first use +/// from a host-installed resolver: after loading the library a host calls +/// the exported `__wit_bindgen_set_import_resolver` function (see +/// `wit_bindgen::rt::ImportResolver`) with a callback that maps an import's +/// core module and function names to a function pointer with the import's +/// core signature. This means everything links whether or not a host is +/// present, the bindings themselves define no global symbols (so any number +/// of `generate!` invocations can coexist in one binary), and a host only +/// needs to implement the imports it cares about — calling an import the +/// resolver doesn't provide aborts with a message naming it. /// /// 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_bindgen_cabi_realloc` function is also exported so hosts can +/// allocate guest-owned memory when lowering arguments, as the canonical ABI +/// requires. These two symbols and `__wit_bindgen_set_import_resolver` are +/// defined once in the `wit-bindgen` runtime crate rather than per world. /// /// [WIT package]: https://component-model.bytecodealliance.org/design/packages.html #[cfg(feature = "macros")] diff --git a/crates/guest-rust/src/rt/mod.rs b/crates/guest-rust/src/rt/mod.rs index c099c3d9b..c6de6e893 100644 --- a/crates/guest-rust/src/rt/mod.rs +++ b/crates/guest-rust/src/rt/mod.rs @@ -192,6 +192,93 @@ pub unsafe fn cabi_realloc( return ptr; } +/// Native (non-wasm) import resolution. +/// +/// On native targets imports aren't resolved by the linker. Each generated +/// import shim instead asks a host-installed resolver for its implementation +/// the first time it's called, identifying the import by its core module and +/// function name as plain strings. The host installs the resolver once per +/// loaded library through `__wit_bindgen_set_import_resolver`, so the +/// bindings themselves define no global symbols at all — any number of +/// `generate!` invocations (even of the same world) can coexist in one +/// binary. +#[cfg(not(target_arch = "wasm32"))] +mod native_imports { + use core::sync::atomic::{AtomicPtr, Ordering}; + + /// A host-provided callback returning the implementation of the import + /// named by `module`/`name` (the canonical ABI core import names, e.g. + /// `my:pkg/iface` and `[method]res.frob`), or null if the host doesn't + /// implement it. The returned pointer must be a function with the + /// import's core signature. `ctx` is the value passed alongside the + /// resolver, returned to the host on every call. + pub type ImportResolver = unsafe extern "C" fn( + ctx: *mut (), + module: *const u8, + module_len: usize, + name: *const u8, + name_len: usize, + ) -> *mut (); + + static RESOLVER: AtomicPtr<()> = AtomicPtr::new(core::ptr::null_mut()); + static RESOLVER_CTX: AtomicPtr<()> = AtomicPtr::new(core::ptr::null_mut()); + + /// Installs the import resolver for this linkage unit. Hosts call this + /// (typically via `dlsym`) after loading the library and before calling + /// any export. + #[unsafe(no_mangle)] + pub unsafe extern "C" fn __wit_bindgen_set_import_resolver( + resolver: ImportResolver, + ctx: *mut (), + ) { + RESOLVER_CTX.store(ctx, Ordering::Relaxed); + // The release store of the resolver publishes the context above. + RESOLVER.store(resolver as *mut (), Ordering::Release); + } + + /// Called by generated import shims on their first invocation. + pub fn resolve_import(module: &str, name: &str) -> *mut () { + let resolver = RESOLVER.load(Ordering::Acquire); + assert!( + !resolver.is_null(), + "import `{module}#{name}` was called before the host installed an \ + import resolver via `__wit_bindgen_set_import_resolver`" + ); + let ctx = RESOLVER_CTX.load(Ordering::Relaxed); + let resolver: ImportResolver = unsafe { core::mem::transmute(resolver) }; + let ptr = unsafe { + resolver( + ctx, + module.as_ptr(), + module.len(), + name.as_ptr(), + name.len(), + ) + }; + assert!( + !ptr.is_null(), + "the host's import resolver provided no implementation for \ + import `{module}#{name}`" + ); + ptr + } + + /// The guest allocator, exported so hosts can allocate guest-owned + /// memory when lowering data, as the canonical ABI requires. + #[unsafe(no_mangle)] + pub unsafe extern "C" fn __wit_bindgen_cabi_realloc( + old_ptr: *mut u8, + old_len: usize, + align: usize, + new_len: usize, + ) -> *mut u8 { + unsafe { crate::rt::cabi_realloc(old_ptr, old_len, align, new_len) } + } +} + +#[cfg(not(target_arch = "wasm32"))] +pub use native_imports::{ImportResolver, resolve_import}; + /// Provide a hook for generated export functions to run static constructors at /// most once. /// diff --git a/crates/rust/src/bindgen.rs b/crates/rust/src/bindgen.rs index e7a4877aa..0c6e0b72e 100644 --- a/crates/rust/src/bindgen.rs +++ b/crates/rust/src/bindgen.rs @@ -67,7 +67,7 @@ impl<'a, 'b> FunctionBindgen<'a, 'b> { &rust_name, params, results, - self.r#gen.r#gen.native_symbols(), + &self.r#gen.r#gen.runtime_path().to_string(), )); rust_name } diff --git a/crates/rust/src/interface.rs b/crates/rust/src/interface.rs index 697e67fed..32bf7f33e 100644 --- a/crates/rust/src/interface.rs +++ b/crates/rust/src/interface.rs @@ -218,7 +218,7 @@ impl<'i> InterfaceGenerator<'i> { "new", &[abi::WasmType::Pointer], &[abi::WasmType::I32], - self.r#gen.native_symbols(), + &self.r#gen.runtime_path().to_string(), ); let import_rep = crate::declare_import( &wasm_import_module, @@ -226,7 +226,7 @@ impl<'i> InterfaceGenerator<'i> { "rep", &[abi::WasmType::I32], &[abi::WasmType::Pointer], - self.r#gen.native_symbols(), + &self.r#gen.runtime_path().to_string(), ); uwriteln!( self.src, @@ -1037,7 +1037,7 @@ fn abi_layout(&mut self) -> ::core::alloc::Layout {{ "call", &sig.params, &sig.results, - self.r#gen.native_symbols(), + &self.r#gen.runtime_path().to_string(), ); let mut args = String::new(); for i in 0..params_lower.len() { @@ -3005,7 +3005,7 @@ impl<'a> {camel}Borrow<'a>{{ "drop", &[abi::WasmType::I32], &[], - self.r#gen.native_symbols(), + &self.r#gen.runtime_path().to_string(), ); uwriteln!( self.src, diff --git a/crates/rust/src/lib.rs b/crates/rust/src/lib.rs index f1e494d46..da1ccfd69 100644 --- a/crates/rust/src/lib.rs +++ b/crates/rust/src/lib.rs @@ -11,8 +11,7 @@ use std::str::FromStr; use wit_bindgen_core::abi::{Bitcast, WasmType}; use wit_bindgen_core::{ AsyncFilterSet, ChainableMethodFilterSet, ChainingMode, Files, InterfaceGenerator as _, Source, - Types, WorldGenerator, abi, dealias, name_package_module, symbol_name, uwrite, uwriteln, - wit_parser::*, + Types, WorldGenerator, dealias, name_package_module, uwrite, uwriteln, wit_parser::*, }; mod bindgen; @@ -48,12 +47,6 @@ pub struct RustWasm { used_member_attr_selectors: HashSet, world: Option, - /// 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, export_macros: Vec<(String, String)>, @@ -486,12 +479,6 @@ impl RustWasm { .unwrap_or("wit_bindgen::rt") } - fn native_symbols(&self) -> &str { - self.native_symbols - .as_deref() - .expect("native symbol prefix is set during preprocess") - } - fn map_type_path(&self) -> String { self.opts .map_type @@ -562,28 +549,6 @@ impl RustWasm { Ok(remapped) } - fn finish_native_cabi_realloc(&mut self) { - let prefix = self.native_symbols().to_string(); - 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. @@ -1308,17 +1273,6 @@ impl WorldGenerator for RustWasm { }); self.world = Some(world); - self.native_symbols = Some({ - 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 @@ -1547,7 +1501,6 @@ 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); @@ -1923,13 +1876,26 @@ fn wasm_type(ty: WasmType) -> &'static str { } } +/// Declares the core import `wasm_import_module`/`wasm_import_name` as a +/// function named `rust_name`, usable on both `wasm32` and native targets. +/// +/// On `wasm32` this is an ordinary linker-resolved wasm import. On native +/// targets the import instead calls through a function pointer looked up on +/// first use from the host's import resolver (see `rt::resolve_import` and +/// the exported `__wit_bindgen_set_import_resolver`), identified by its core +/// module and function names as plain strings. Everything links whether or +/// not a host is present — the shim defines no global symbols, so any number +/// of `generate!` invocations can coexist in one binary — and calling an +/// import with no resolver (or one the host doesn't implement) aborts with a +/// message naming the import, just as the old `unreachable!()` stubs +/// aborted. fn declare_import( wasm_import_module: &str, wasm_import_name: &str, rust_name: &str, params: &[WasmType], results: &[WasmType], - native_prefix: &str, + rt: &str, ) -> String { let mut sig = "(".to_owned(); for param in params.iter() { @@ -1944,13 +1910,6 @@ fn declare_import( sig.push_str(wasm_type(*result)); } - 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() @@ -1975,26 +1934,15 @@ fn declare_import( 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()); - - #[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}`" - ); + static CACHE: ::core::sync::atomic::AtomicPtr<()> = + ::core::sync::atomic::AtomicPtr::new(::core::ptr::null_mut()); + let mut ptr = CACHE.load(::core::sync::atomic::Ordering::Acquire); + if ptr.is_null() {{ + ptr = {rt}::resolve_import("{wasm_import_module}", "{wasm_import_name}"); + CACHE.store(ptr, ::core::sync::atomic::Ordering::Release); + }} let f: unsafe extern "C" fn{sig} = unsafe {{ ::core::mem::transmute(ptr) }}; unsafe {{ f({call_args}) }} }} diff --git a/crates/rust/tests/codegen.rs b/crates/rust/tests/codegen.rs index 462125f9f..977fd642f 100644 --- a/crates/rust/tests/codegen.rs +++ b/crates/rust/tests/codegen.rs @@ -423,6 +423,10 @@ mod versioned_selectors { } } +// These two modules deliberately call `export!`: the native export symbols +// are generated inside the `__export_*_cabi!` macro, so without expanding it +// none of that code is ever compiled. (The native import shims compile in +// every module above as a side effect of being default behavior.) #[allow(unused, reason = "testing codegen, not functionality")] mod native_symbols { wit_bindgen::generate!({ @@ -504,27 +508,3 @@ mod native_symbols_async { export!(Component); } - -#[allow(unused, reason = "testing codegen, not functionality")] -mod 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, - }); -} - -#[allow(unused, reason = "testing codegen, not functionality")] -mod 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, - }); -} From ae12065c265626b500137c8fbc12c3b3ee2acafa Mon Sep 17 00:00:00 2001 From: Bjorn Beishline <75190918+BjornTheProgrammer@users.noreply.github.com> Date: Wed, 26 Aug 2026 12:58:19 -0700 Subject: [PATCH 4/4] feat(rust): identify native imports by module#name via a host resolver Replaces the per-import __wit_bindgen_register_* hooks with a single __wit_bindgen_set_import_resolver entry point in the runtime crate, adds a per-world __wit_bindgen_world_* marker symbol so hosts can verify the world before calling anything, and un-prefixes __wit_bindgen_cabi_realloc. Co-Authored-By: Claude Fable 5 --- crates/guest-rust/src/lib.rs | 58 ++++++++++++++++++++----- crates/guest-rust/src/rt/mod.rs | 72 ++++++++++++++++++------------- crates/rust/src/bindgen.rs | 2 +- crates/rust/src/interface.rs | 8 ++-- crates/rust/src/lib.rs | 51 +++++++++++++++++++--- crates/rust/tests/codegen.rs | 75 ++++++++++++++++++++++++++++++++- 6 files changed, 214 insertions(+), 52 deletions(-) diff --git a/crates/guest-rust/src/lib.rs b/crates/guest-rust/src/lib.rs index 01d4296e5..9a0773440 100644 --- a/crates/guest-rust/src/lib.rs +++ b/crates/guest-rust/src/lib.rs @@ -906,21 +906,59 @@ extern crate std; /// Imports are not resolved by the native linker. Each import calls through /// a function pointer that starts out null and is looked up on first use /// from a host-installed resolver: after loading the library a host calls -/// the exported `__wit_bindgen_set_import_resolver` function (see -/// `wit_bindgen::rt::ImportResolver`) with a callback that maps an import's -/// core module and function names to a function pointer with the import's -/// core signature. This means everything links whether or not a host is -/// present, the bindings themselves define no global symbols (so any number -/// of `generate!` invocations can coexist in one binary), and a host only -/// needs to implement the imports it cares about — calling an import the -/// resolver doesn't provide aborts with a message naming it. +/// the exported `__wit_bindgen_set_import_resolver` function with a callback +/// that maps an import's core module and function names to a function +/// pointer with the import's core signature. Its signature is +/// +/// ```c +/// void __wit_bindgen_set_import_resolver( +/// void *(*resolver)(void *ctx, const char *import), +/// void *ctx); +/// ``` +/// +/// where `import` is the import's core module and function name joined by +/// `#` as one NUL-terminated string, e.g. `my:pkg/iface@1.0.0#[method]res.frob` +/// (see `wit_bindgen::rt::ImportResolver`). Passing a null `resolver` +/// uninstalls the current one. +/// +/// This means everything links whether or not a host is present, the import +/// shims define no global symbols at all, and a host only needs to implement +/// the imports it cares about — calling an import the resolver doesn't +/// provide aborts with a message naming it. +/// +/// The resolver should be installed once, before any export is called: each +/// import caches the pointer it was given, so a later +/// `__wit_bindgen_set_import_resolver` call won't be seen by imports that +/// have already been resolved. The resolver is also shared by every +/// `generate!` invocation linked into the library and is keyed only by core +/// module and function name, so two worlds importing the same core name +/// necessarily get the same implementation. /// /// 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. These two symbols and `__wit_bindgen_set_import_resolver` are -/// defined once in the `wit-bindgen` runtime crate rather than per world. +/// requires. That symbol and `__wit_bindgen_set_import_resolver` are defined +/// once in the `wit-bindgen` runtime crate rather than per world. +/// +/// Each world additionally exports a marker function +/// `const char *__wit_bindgen_world_(void)`, where `` is the +/// hex-encoded fully qualified world name including its package version and +/// `type_section_suffix`, e.g. `my:pkg@1.0.0/my-world`. Since `dlsym` can't +/// tell a host whether the library it opened implements the world it expects +/// — and the canonical ABI lowering of a world changes with the world, so +/// guessing wrong corrupts memory instead of failing cleanly — a host should +/// look this symbol up before installing a resolver or calling an export, and +/// treat a missing symbol as the wrong plugin. Calling it returns the world +/// name as a NUL-terminated string, for the resulting error message. +/// +/// This marker is keyed the same way as the `component-type` custom section a +/// wasm build emits, so the rules are the ones a wasm build already imposes. +/// Binding the same world twice needs a `type_section_suffix` to tell the +/// markers apart (plus `export_prefix` for the core export names). Two +/// *different* worlds sharing one fully qualified name is not a supported +/// configuration at all: here the linker rejects the duplicate marker, and on +/// wasm `wit-component` refuses to merge the two packages. /// /// [WIT package]: https://component-model.bytecodealliance.org/design/packages.html #[cfg(feature = "macros")] diff --git a/crates/guest-rust/src/rt/mod.rs b/crates/guest-rust/src/rt/mod.rs index c6de6e893..c6360d38c 100644 --- a/crates/guest-rust/src/rt/mod.rs +++ b/crates/guest-rust/src/rt/mod.rs @@ -198,27 +198,36 @@ pub unsafe fn cabi_realloc( /// import shim instead asks a host-installed resolver for its implementation /// the first time it's called, identifying the import by its core module and /// function name as plain strings. The host installs the resolver once per -/// loaded library through `__wit_bindgen_set_import_resolver`, so the -/// bindings themselves define no global symbols at all — any number of -/// `generate!` invocations (even of the same world) can coexist in one -/// binary. +/// loaded library through `__wit_bindgen_set_import_resolver`, so the import +/// shims define no global symbols at all and any number of `generate!` +/// invocations can share one resolver. (Export symbols and world markers are +/// a separate matter: those are global, so binding the same world twice in +/// one binary needs `export_prefix` and `type_section_suffix` respectively to +/// avoid duplicate symbols.) +/// +/// Because each shim caches the pointer the resolver handed it, installing a +/// resolver a second time has no effect on imports that have already been +/// called. Hosts are expected to install one before calling any export. #[cfg(not(target_arch = "wasm32"))] mod native_imports { + use core::ffi::{CStr, c_char}; use core::sync::atomic::{AtomicPtr, Ordering}; /// A host-provided callback returning the implementation of the import - /// named by `module`/`name` (the canonical ABI core import names, e.g. - /// `my:pkg/iface` and `[method]res.frob`), or null if the host doesn't - /// implement it. The returned pointer must be a function with the - /// import's core signature. `ctx` is the value passed alongside the - /// resolver, returned to the host on every call. - pub type ImportResolver = unsafe extern "C" fn( - ctx: *mut (), - module: *const u8, - module_len: usize, - name: *const u8, - name_len: usize, - ) -> *mut (); + /// named by `import`, or null if the host doesn't implement it. The + /// returned pointer must be a function with the import's core signature. + /// `ctx` is the value passed alongside the resolver, returned to the host + /// on every call. + /// + /// `import` is the import's canonical ABI core module and function name + /// joined by `#`, as a single NUL-terminated string — for example + /// `my:pkg/iface@1.0.0#[method]res.frob`, or `$root#some-func` for a + /// function imported at the top level of a world. (Note the package + /// version trails the interface name here, unlike in a world name.) It + /// points into the guest's static data and stays valid for as long as the + /// library is loaded, so a host may use it as a lookup key without + /// copying it. + pub type ImportResolver = unsafe extern "C" fn(ctx: *mut (), import: *const c_char) -> *mut (); static RESOLVER: AtomicPtr<()> = AtomicPtr::new(core::ptr::null_mut()); static RESOLVER_CTX: AtomicPtr<()> = AtomicPtr::new(core::ptr::null_mut()); @@ -226,39 +235,42 @@ mod native_imports { /// Installs the import resolver for this linkage unit. Hosts call this /// (typically via `dlsym`) after loading the library and before calling /// any export. + /// + /// Passing `None` for `resolver` uninstalls the current one. That only + /// affects imports which haven't been resolved yet; imports already + /// called keep the pointer they cached. #[unsafe(no_mangle)] pub unsafe extern "C" fn __wit_bindgen_set_import_resolver( - resolver: ImportResolver, + resolver: Option, ctx: *mut (), ) { + let resolver = match resolver { + Some(resolver) => resolver as *mut (), + None => core::ptr::null_mut(), + }; RESOLVER_CTX.store(ctx, Ordering::Relaxed); // The release store of the resolver publishes the context above. - RESOLVER.store(resolver as *mut (), Ordering::Release); + RESOLVER.store(resolver, Ordering::Release); } /// Called by generated import shims on their first invocation. - pub fn resolve_import(module: &str, name: &str) -> *mut () { + /// + /// `import` is the `module#name` string described on [`ImportResolver`]. + pub fn resolve_import(import: &CStr) -> *mut () { + let display = import.to_str().unwrap_or(""); let resolver = RESOLVER.load(Ordering::Acquire); assert!( !resolver.is_null(), - "import `{module}#{name}` was called before the host installed an \ + "import `{display}` was called before the host installed an \ import resolver via `__wit_bindgen_set_import_resolver`" ); let ctx = RESOLVER_CTX.load(Ordering::Relaxed); let resolver: ImportResolver = unsafe { core::mem::transmute(resolver) }; - let ptr = unsafe { - resolver( - ctx, - module.as_ptr(), - module.len(), - name.as_ptr(), - name.len(), - ) - }; + let ptr = unsafe { resolver(ctx, import.as_ptr()) }; assert!( !ptr.is_null(), "the host's import resolver provided no implementation for \ - import `{module}#{name}`" + import `{display}`" ); ptr } diff --git a/crates/rust/src/bindgen.rs b/crates/rust/src/bindgen.rs index 0c6e0b72e..efd10db19 100644 --- a/crates/rust/src/bindgen.rs +++ b/crates/rust/src/bindgen.rs @@ -67,7 +67,7 @@ impl<'a, 'b> FunctionBindgen<'a, 'b> { &rust_name, params, results, - &self.r#gen.r#gen.runtime_path().to_string(), + self.r#gen.r#gen.runtime_path(), )); rust_name } diff --git a/crates/rust/src/interface.rs b/crates/rust/src/interface.rs index 32bf7f33e..d52c733ed 100644 --- a/crates/rust/src/interface.rs +++ b/crates/rust/src/interface.rs @@ -218,7 +218,7 @@ impl<'i> InterfaceGenerator<'i> { "new", &[abi::WasmType::Pointer], &[abi::WasmType::I32], - &self.r#gen.runtime_path().to_string(), + self.r#gen.runtime_path(), ); let import_rep = crate::declare_import( &wasm_import_module, @@ -226,7 +226,7 @@ impl<'i> InterfaceGenerator<'i> { "rep", &[abi::WasmType::I32], &[abi::WasmType::Pointer], - &self.r#gen.runtime_path().to_string(), + self.r#gen.runtime_path(), ); uwriteln!( self.src, @@ -1037,7 +1037,7 @@ fn abi_layout(&mut self) -> ::core::alloc::Layout {{ "call", &sig.params, &sig.results, - &self.r#gen.runtime_path().to_string(), + self.r#gen.runtime_path(), ); let mut args = String::new(); for i in 0..params_lower.len() { @@ -3005,7 +3005,7 @@ impl<'a> {camel}Borrow<'a>{{ "drop", &[abi::WasmType::I32], &[], - &self.r#gen.runtime_path().to_string(), + self.r#gen.runtime_path(), ); uwriteln!( self.src, diff --git a/crates/rust/src/lib.rs b/crates/rust/src/lib.rs index da1ccfd69..0e9dd5bba 100644 --- a/crates/rust/src/lib.rs +++ b/crates/rust/src/lib.rs @@ -11,7 +11,8 @@ use std::str::FromStr; use wit_bindgen_core::abi::{Bitcast, WasmType}; use wit_bindgen_core::{ AsyncFilterSet, ChainableMethodFilterSet, ChainingMode, Files, InterfaceGenerator as _, Source, - Types, WorldGenerator, dealias, name_package_module, uwrite, uwriteln, wit_parser::*, + Types, WorldGenerator, dealias, name_package_module, symbol_name, uwrite, uwriteln, + wit_parser::*, }; mod bindgen; @@ -549,6 +550,46 @@ impl RustWasm { Ok(remapped) } + /// Emits this world's native marker symbol. + /// + /// Native bindings are loaded by `dlsym`, which gives a host no way to + /// tell whether the library it opened implements the WIT world it expects + /// or something else — and the canonical ABI lowering of a world changes + /// with the world, so guessing wrong corrupts memory rather than failing + /// cleanly. So each world exports a marker function named for the world, + /// which a host looks up before calling anything else, treating a missing + /// symbol as the wrong plugin. Calling it returns the world name as a + /// NUL-terminated string, for the error message. + /// + /// The symbol is keyed on the world's name and `type_section_suffix`, + /// exactly like the `component-type` custom section is on wasm, so the + /// rules match what a wasm build already imposes: binding the same world + /// twice needs a `type_section_suffix` to tell the two apart, and two + /// different worlds sharing a fully qualified name is not a supported + /// configuration in the first place — on wasm `wit-component` refuses to + /// merge them, and here the linker refuses to define the symbol twice. + fn finish_native_world_marker(&mut self, resolve: &Resolve, world: WorldId) { + let world = &resolve.worlds[world]; + let pkg = world + .package + .map(|p| resolve.packages[p].name.to_string()) + .unwrap_or_default(); + let name = format!("{pkg}/{}", world.name); + let suffix = self.opts.type_section_suffix.as_deref().unwrap_or(""); + let symbol = symbol_name::make_external_component(&format!("{name}{suffix}")); + uwriteln!( + self.src, + r#" +#[cfg(not(target_arch = "wasm32"))] +#[unsafe(no_mangle)] +#[allow(non_snake_case)] +pub extern "C" fn __wit_bindgen_world_{symbol}() -> *const ::core::ffi::c_char {{ + c"{name}".as_ptr() +}} +"# + ); + } + fn finish_runtime_module(&mut self) { if !self.rt_module.is_empty() { // As above, disable rustfmt, as we use prettyplease. @@ -1501,7 +1542,7 @@ impl WorldGenerator for RustWasm { let exports = mem::take(&mut self.export_modules); self.emit_modules(exports); - + self.finish_native_world_marker(resolve, world); self.finish_runtime_module(); self.finish_export_macro(resolve, world); @@ -1884,8 +1925,8 @@ fn wasm_type(ty: WasmType) -> &'static str { /// first use from the host's import resolver (see `rt::resolve_import` and /// the exported `__wit_bindgen_set_import_resolver`), identified by its core /// module and function names as plain strings. Everything links whether or -/// not a host is present — the shim defines no global symbols, so any number -/// of `generate!` invocations can coexist in one binary — and calling an +/// not a host is present — the shim defines no global symbols, so import +/// shims never collide between `generate!` invocations — and calling an /// import with no resolver (or one the host doesn't implement) aborts with a /// message naming the import, just as the old `unreachable!()` stubs /// aborted. @@ -1940,7 +1981,7 @@ fn declare_import( ::core::sync::atomic::AtomicPtr::new(::core::ptr::null_mut()); let mut ptr = CACHE.load(::core::sync::atomic::Ordering::Acquire); if ptr.is_null() {{ - ptr = {rt}::resolve_import("{wasm_import_module}", "{wasm_import_name}"); + ptr = {rt}::resolve_import(c"{wasm_import_module}#{wasm_import_name}"); CACHE.store(ptr, ::core::sync::atomic::Ordering::Release); }} let f: unsafe extern "C" fn{sig} = unsafe {{ ::core::mem::transmute(ptr) }}; diff --git a/crates/rust/tests/codegen.rs b/crates/rust/tests/codegen.rs index 977fd642f..28898f30e 100644 --- a/crates/rust/tests/codegen.rs +++ b/crates/rust/tests/codegen.rs @@ -24,8 +24,13 @@ mod multiple_paths { #[allow(unused, reason = "testing codegen, not functionality")] mod inline_and_path { wit_bindgen::generate!({ + // NB: this package is deliberately not `test:paths` like + // `multiple_paths` above. Two structurally different worlds under one + // fully qualified name is not a supported configuration: on wasm + // `wit-component` refuses to merge the two `component-type` sections, + // and natively the two world markers collide at link time. inline: r#" - package test:paths; + package test:inline-and-path-root; world test { import test:inline-and-path/bar; @@ -251,7 +256,11 @@ mod borrowing_method_chaining { } "#, generate_all, - chainable_methods: ["&all"] + chainable_methods: ["&all"], + // `method_chaining` above binds this same world; natively the world + // marker symbols would collide without a suffix, just like the + // `component-type` sections would in a wasm component build. + type_section_suffix: "-borrowing", }); } @@ -508,3 +517,65 @@ mod native_symbols_async { export!(Component); } + +// Import shims are not namespaced by world (they define no symbols at all), +// so two worlds importing the same interface link fine side by side. +#[allow(unused, reason = "testing codegen, not functionality")] +mod 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, + }); +} + +#[allow(unused, reason = "testing codegen, not functionality")] +mod 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, + }); +} + +// The other two kinds of native symbol are global, so binding the *same* world +// twice needs both knobs: `type_section_suffix` for the world marker (the same +// option a wasm build needs to keep the two `component-type` sections apart) +// and `export_prefix` for the core export names. Drop the suffix and this +// fails with `symbol `__wit_bindgen_world_...` is already defined`; drop the +// prefix and it fails with `symbol `run` is already defined`. +macro_rules! native_symbols_same_world { + ($module:ident, $tag:literal) => { + #[allow(unused, reason = "testing codegen, not functionality")] + mod $module { + wit_bindgen::generate!({ + inline: r#" + package test:native-same-world; + world w { export run: func() -> u32; } + "#, + generate_all, + type_section_suffix: $tag, + export_prefix: $tag, + }); + + struct Component; + + impl Guest for Component { + fn run() -> u32 { + 0 + } + } + + export!(Component); + } + }; +} + +native_symbols_same_world!(native_symbols_same_world_a, "a_"); +native_symbols_same_world!(native_symbols_same_world_b, "b_");