From 076182983ad1784521353ffd7567d8850bfc3014 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/6] 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 c79a22a55..b29793e56 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, "}}"); @@ -1072,6 +1075,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() { @@ -1334,60 +1338,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 { @@ -3023,6 +3060,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 bd443078770617c4a5b57b811c0d84eb56f5b561 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/6] 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 b29793e56..874edad86 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, "}}"); @@ -1345,86 +1343,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 a19207a0643208621e0fb08375319d67d7afacff Mon Sep 17 00:00:00 2001 From: Bjorn Beishline <75190918+BjornTheProgrammer@users.noreply.github.com> Date: Thu, 3 Sep 2026 02:35:49 -0700 Subject: [PATCH 3/6] feat(rust): resolve native imports through a host-installed resolver --- crates/guest-rust/src/lib.rs | 52 +++++++----- crates/guest-rust/src/rt/mod.rs | 6 ++ crates/guest-rust/src/rt/native_imports.rs | 84 +++++++++++++++++++ crates/rust/src/bindgen.rs | 2 +- crates/rust/src/interface.rs | 8 +- crates/rust/src/lib.rs | 93 +++++++--------------- crates/rust/tests/codegen.rs | 45 ++++++++++- 7 files changed, 199 insertions(+), 91 deletions(-) create mode 100644 crates/guest-rust/src/rt/native_imports.rs diff --git a/crates/guest-rust/src/lib.rs b/crates/guest-rust/src/lib.rs index 8d4ec6a2d..eb6880c0b 100644 --- a/crates/guest-rust/src/lib.rs +++ b/crates/guest-rust/src/lib.rs @@ -903,29 +903,39 @@ extern crate std; /// 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. +/// Imports are not resolved by the native linker. Each import instead calls +/// through a function pointer looked up on first use from a host-installed +/// resolver. After loading the library a host calls the exported +/// +/// ```c +/// void __wit_bindgen_set_import_resolver( +/// void *(*resolver)(void *ctx, const char *module, const char *name), +/// void *ctx); +/// ``` +/// +/// with a callback mapping an import's core module and function name (e.g. +/// `my:pkg/iface@1.0.0` and `[method]res.frob`) to a function pointer with +/// the import's core signature, or null if the host doesn't implement it +/// (see `wit_bindgen::rt::ImportResolver`). Everything links whether or not +/// a host is present, and calling an import with no implementation aborts +/// with a message naming it. Install the resolver once, before calling any +/// export: each import caches the pointer it was given. /// /// 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. +/// destructors, are exported under their hex-encoded core export names. The +/// runtime crate also exports `__wit_bindgen_cabi_realloc` so hosts can +/// allocate guest-owned memory when lowering arguments. +/// +/// Each world additionally exports a marker +/// `const char *__wit_bindgen_world_(void)`, where `` is the +/// hex-encoded `/` (e.g. +/// `my:pkg@1.0.0/my-world`), which returns that name. `dlsym` can't otherwise +/// tell a host whether a library implements the world it expects, and calling +/// into the wrong world corrupts memory rather than failing, so hosts should +/// look this symbol up before anything else. The marker is keyed like the +/// `component-type` custom section on wasm: binding the same world twice in +/// one binary needs a `type_section_suffix` (and `export_prefix` for the +/// export names). /// /// [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..95e420469 100644 --- a/crates/guest-rust/src/rt/mod.rs +++ b/crates/guest-rust/src/rt/mod.rs @@ -192,6 +192,12 @@ pub unsafe fn cabi_realloc( return ptr; } +#[cfg(not(target_arch = "wasm32"))] +mod native_imports; + +#[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/guest-rust/src/rt/native_imports.rs b/crates/guest-rust/src/rt/native_imports.rs new file mode 100644 index 000000000..2908432e4 --- /dev/null +++ b/crates/guest-rust/src/rt/native_imports.rs @@ -0,0 +1,84 @@ +//! 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`. +//! +//! 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. + +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` and `name`, 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. +/// +/// `module` and `name` are the import's canonical ABI core module and +/// function name as two NUL-terminated strings. For example +/// `my:pkg/iface@1.0.0` and `[method]res.frob`, with module `$root` for a +/// function imported at the top level of a world. +pub type ImportResolver = + unsafe extern "C" fn(ctx: *mut (), module: *const c_char, name: *const c_char) -> *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 +/// 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: Option, + ctx: *mut (), +) { + let resolver = match resolver { + Some(resolver) => resolver as *mut (), + None => core::ptr::null_mut(), + }; + RESOLVER_CTX.store(ctx, Ordering::Relaxed); + RESOLVER.store(resolver, Ordering::Release); +} + +/// Called by generated import shims on their first invocation. +/// +/// `module` and `name` are the strings described on [`ImportResolver`]. +pub fn resolve_import(module: &CStr, name: &CStr) -> *mut () { + let module_display = module.to_string_lossy(); + let name_display = name.to_string_lossy(); + let resolver = RESOLVER.load(Ordering::Acquire); + assert!( + !resolver.is_null(), + "import `{module_display}#{name_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(), name.as_ptr()) }; + assert!( + !ptr.is_null(), + "the host's import resolver provided no implementation for \ + import `{module_display}#{name_display}`" + ); + 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) } +} diff --git a/crates/rust/src/bindgen.rs b/crates/rust/src/bindgen.rs index e7a4877aa..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.native_symbols(), + self.r#gen.r#gen.runtime_path(), )); rust_name } diff --git a/crates/rust/src/interface.rs b/crates/rust/src/interface.rs index 874edad86..86e00c570 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(), ); 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(), ); uwriteln!( self.src, @@ -1073,7 +1073,7 @@ fn abi_layout(&mut self) -> ::core::alloc::Layout {{ "call", &sig.params, &sig.results, - self.r#gen.native_symbols(), + self.r#gen.runtime_path(), ); let mut args = String::new(); for i in 0..params_lower.len() { @@ -3041,7 +3041,7 @@ impl<'a> {camel}Borrow<'a>{{ "drop", &[abi::WasmType::I32], &[], - self.r#gen.native_symbols(), + self.r#gen.runtime_path(), ); uwriteln!( self.src, diff --git a/crates/rust/src/lib.rs b/crates/rust/src/lib.rs index f1e494d46..2597679d8 100644 --- a/crates/rust/src/lib.rs +++ b/crates/rust/src/lib.rs @@ -11,7 +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, + Types, WorldGenerator, dealias, name_package_module, symbol_name, uwrite, uwriteln, wit_parser::*, }; @@ -48,12 +48,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 +480,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,23 +550,28 @@ 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}"); + /// Emits the world's native marker symbol, `__wit_bindgen_world_`, + /// which hosts look up before calling anything else to check that the + /// library they opened implements the world they expect. It's keyed on + /// the world name and `type_section_suffix`, like the `component-type` + /// section on wasm. + 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 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) }} +pub extern "C" fn __wit_bindgen_world_{symbol}() -> *const ::core::ffi::c_char {{ + c"{name}".as_ptr() }} "# ); @@ -1308,17 +1301,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,8 +1529,7 @@ impl WorldGenerator for RustWasm { let exports = mem::take(&mut self.export_modules); self.emit_modules(exports); - self.finish_native_cabi_realloc(); - + self.finish_native_world_marker(resolve, world); self.finish_runtime_module(); self.finish_export_macro(resolve, world); @@ -1923,13 +1904,17 @@ fn wasm_type(ty: WasmType) -> &'static str { } } +/// Declares the core import `wasm_import_module`/`wasm_import_name` as a +/// function named `rust_name`. On `wasm32` this is a plain linker-resolved +/// import; natively it's a shim that asks the host's resolver for the +/// implementation on first call (see `rt::resolve_import`) and caches it. 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 +1929,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 +1953,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(c"{wasm_import_module}", c"{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..bc45b7542 100644 --- a/crates/rust/tests/codegen.rs +++ b/crates/rust/tests/codegen.rs @@ -24,8 +24,10 @@ mod multiple_paths { #[allow(unused, reason = "testing codegen, not functionality")] mod inline_and_path { wit_bindgen::generate!({ + // Deliberately not `test:paths` like `multiple_paths` above: two + // different worlds under one name collide on the native world marker. inline: r#" - package test:paths; + package test:inline-and-path-root; world test { import test:inline-and-path/bar; @@ -251,7 +253,9 @@ mod borrowing_method_chaining { } "#, generate_all, - chainable_methods: ["&all"] + chainable_methods: ["&all"], + // `owning_method_chaining` above binds the same world; keep the markers apart. + type_section_suffix: "-borrowing", }); } @@ -423,6 +427,8 @@ mod versioned_selectors { } } +// These call `export!` so the native export symbols, which live inside the +// `__export_*_cabi!` macro, actually get compiled. #[allow(unused, reason = "testing codegen, not functionality")] mod native_symbols { wit_bindgen::generate!({ @@ -505,6 +511,8 @@ mod native_symbols_async { export!(Component); } +// Import shims define no symbols, so two worlds importing the same +// interface link side by side. #[allow(unused, reason = "testing codegen, not functionality")] mod native_symbols_shared_one { wit_bindgen::generate!({ @@ -528,3 +536,36 @@ mod native_symbols_shared_two { generate_all, }); } + +// Binding the *same* world twice needs `type_section_suffix` for the world +// marker and `export_prefix` for the export names; drop either and it fails +// to link with a duplicate symbol. +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_"); From 008b4325c74193b8a0290eff62cebc12e3a02150 Mon Sep 17 00:00:00 2001 From: Bjorn Beishline <75190918+BjornTheProgrammer@users.noreply.github.com> Date: Thu, 3 Sep 2026 04:09:28 -0700 Subject: [PATCH 4/6] feat(rust) make async work with native --- .gitignore | 4 + Cargo.lock | 11 + crates/guest-rust/src/rt/async_support.rs | 40 +- .../guest-rust/src/rt/async_support/cabi.rs | 51 +- .../src/rt/async_support/error_context.rs | 6 +- .../src/rt/async_support/waitable_set.rs | 4 +- crates/guest-rust/src/rt/native_imports.rs | 9 + crates/rust/Cargo.toml | 2 + crates/rust/src/interface.rs | 81 ++- crates/rust/src/lib.rs | 75 ++- crates/rust/tests/native-e2e/Cargo.toml | 15 + crates/rust/tests/native-e2e/src/lib.rs | 63 +++ crates/rust/tests/native_e2e.rs | 488 ++++++++++++++++++ 13 files changed, 781 insertions(+), 68 deletions(-) create mode 100644 crates/rust/tests/native-e2e/Cargo.toml create mode 100644 crates/rust/tests/native-e2e/src/lib.rs create mode 100644 crates/rust/tests/native_e2e.rs diff --git a/.gitignore b/.gitignore index 96b9f616a..2e3a7237f 100644 --- a/.gitignore +++ b/.gitignore @@ -11,3 +11,7 @@ crates/guest-rust/src/cabi_realloc.o wit_component /wit-bindgen.sln + +# Built by crates/rust/tests/native_e2e.rs +crates/rust/tests/native-e2e/Cargo.lock +crates/rust/tests/native-e2e/target diff --git a/Cargo.lock b/Cargo.lock index 707c0ac1e..d1d8cd0ff 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -604,6 +604,16 @@ version = "0.2.186" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" +[[package]] +name = "libloading" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" +dependencies = [ + "cfg-if", + "windows-link", +] + [[package]] name = "libtest-mimic" version = "0.8.2" @@ -1506,6 +1516,7 @@ dependencies = [ "futures", "heck", "indexmap", + "libloading", "prettyplease", "serde", "serde_json", diff --git a/crates/guest-rust/src/rt/async_support.rs b/crates/guest-rust/src/rt/async_support.rs index a8b11bb25..0e2519c81 100644 --- a/crates/guest-rust/src/rt/async_support.rs +++ b/crates/guest-rust/src/rt/async_support.rs @@ -27,33 +27,49 @@ macro_rules! rtdebug { /// Helper macro to deduplicate foreign definitions of wasm functions. /// -/// This automatically imports when on wasm targets and then defines a dummy -/// panicking shim for native targets to support native compilation but fail at -/// runtime. +/// On wasm targets this declares the canonical ABI built-ins as ordinary +/// linker-resolved imports. On native targets each one instead becomes a shim +/// that asks the host's import resolver for its implementation on first call +/// (see `rt::native_imports`), identified by the same module and name, so the +/// generated code is the same on both targets and only who satisfies the +/// import differs. macro_rules! extern_wasm { ( - $(#[$extern_attr:meta])* + #[link(wasm_import_module = $module:literal)] unsafe extern "C" { $( - $(#[$func_attr:meta])* - $vis:vis fn $func_name:ident ( $($args:tt)* ) $(-> $ret:ty)?; + #[link_name = $name:literal] + $vis:vis fn $func_name:ident ( $($arg:ident : $ty:ty),* $(,)? ) $(-> $ret:ty)?; )* } ) => { $( #[cfg(not(target_family = "wasm"))] - #[allow(unused, reason = "dummy shim for non-wasm compilation, never invoked")] - $vis unsafe fn $func_name($($args)*) $(-> $ret)? { - unreachable!(); + #[allow(dead_code, reason = "mirrors the wasm import set even if unused natively")] + $vis unsafe fn $func_name($($arg: $ty),*) $(-> $ret)? { + static CACHE: ::core::sync::atomic::AtomicPtr<()> = + ::core::sync::atomic::AtomicPtr::new(::core::ptr::null_mut()); + // Named so as not to shadow any parameter. + let mut __impl = CACHE.load(::core::sync::atomic::Ordering::Acquire); + if __impl.is_null() { + __impl = crate::rt::resolve_import( + crate::rt::native_imports::cstr(concat!($module, "\0")), + crate::rt::native_imports::cstr(concat!($name, "\0")), + ); + CACHE.store(__impl, ::core::sync::atomic::Ordering::Release); + } + let __func: unsafe extern "C" fn($($ty),*) $(-> $ret)? = + unsafe { ::core::mem::transmute(__impl) }; + unsafe { __func($($arg),*) } } )* #[cfg(target_family = "wasm")] - $(#[$extern_attr])* + #[link(wasm_import_module = $module)] unsafe extern "C" { $( - $(#[$func_attr])* - $vis fn $func_name($($args)*) $(-> $ret)?; + #[link_name = $name] + $vis fn $func_name($($arg: $ty),*) $(-> $ret)?; )* } }; diff --git a/crates/guest-rust/src/rt/async_support/cabi.rs b/crates/guest-rust/src/rt/async_support/cabi.rs index b03f5da92..686659cf4 100644 --- a/crates/guest-rust/src/rt/async_support/cabi.rs +++ b/crates/guest-rust/src/rt/async_support/cabi.rs @@ -69,21 +69,42 @@ use core::ffi::c_void; -extern_wasm! { - unsafe extern "C" { - /// Sets the global task pointer to `ptr` provided. Returns the previous - /// value. - /// - /// This function acts as a dual getter and a setter. To get the - /// current task pointer a dummy `ptr` can be provided (e.g. NULL) and then - /// it's passed back when you're done working with it. When setting the - /// current task pointer it's recommended to call this and then call it - /// again with the previous value when the tasks's work is done. - /// - /// For executors they need to ensure that the `ptr` passed in lives for - /// the entire lifetime of the component model task. - pub fn wasip3_task_set(ptr: *mut wasip3_task) -> *mut wasip3_task; - } +#[cfg(target_family = "wasm")] +unsafe extern "C" { + /// Sets the global task pointer to `ptr` provided. Returns the previous + /// value. + /// + /// This function acts as a dual getter and a setter. To get the + /// current task pointer a dummy `ptr` can be provided (e.g. NULL) and then + /// it's passed back when you're done working with it. When setting the + /// current task pointer it's recommended to call this and then call it + /// again with the previous value when the tasks's work is done. + /// + /// For executors they need to ensure that the `ptr` passed in lives for + /// the entire lifetime of the component model task. + pub fn wasip3_task_set(ptr: *mut wasip3_task) -> *mut wasip3_task; +} + +/// Native counterpart of the C-defined `wasip3_task_set` above. Uses +/// thread local so there is no possible panic on multi thread when polling +/// two async exports at the same time. +#[cfg(all(not(target_family = "wasm"), feature = "std"))] +pub unsafe fn wasip3_task_set(ptr: *mut wasip3_task) -> *mut wasip3_task { + use core::cell::Cell; + std::thread_local!( + static CURRENT: Cell<*mut wasip3_task> = const { Cell::new(core::ptr::null_mut()) } + ); + CURRENT.with(|current| current.replace(ptr)) +} + +/// Without `std` there are no thread-locals on stable Rust, so this falls +/// back to one global slot, which requires the host to not poll two async +/// exports at the same time. +#[cfg(all(not(target_family = "wasm"), not(feature = "std")))] +pub unsafe fn wasip3_task_set(ptr: *mut wasip3_task) -> *mut wasip3_task { + use core::sync::atomic::{AtomicPtr, Ordering}; + static CURRENT: AtomicPtr = AtomicPtr::new(core::ptr::null_mut()); + CURRENT.swap(ptr, Ordering::AcqRel) } /// The first version of `wasip3_task` which implies the existence of the diff --git a/crates/guest-rust/src/rt/async_support/error_context.rs b/crates/guest-rust/src/rt/async_support/error_context.rs index 041955ac2..13037e765 100644 --- a/crates/guest-rust/src/rt/async_support/error_context.rs +++ b/crates/guest-rust/src/rt/async_support/error_context.rs @@ -74,10 +74,10 @@ extern_wasm! { #[link(wasm_import_module = "$root")] unsafe extern "C" { #[link_name = "[error-context-new-utf8]"] - fn new(_: *const u8, _: usize) -> u32; + fn new(ptr: *const u8, len: usize) -> u32; #[link_name = "[error-context-drop]"] - fn drop(_: u32); + fn drop(handle: u32); #[link_name = "[error-context-debug-message-utf8]"] - fn debug_message(_: u32, _: &mut RetPtr); + fn debug_message(handle: u32, ret: &mut RetPtr); } } diff --git a/crates/guest-rust/src/rt/async_support/waitable_set.rs b/crates/guest-rust/src/rt/async_support/waitable_set.rs index 27fbbc02f..bec1df76b 100644 --- a/crates/guest-rust/src/rt/async_support/waitable_set.rs +++ b/crates/guest-rust/src/rt/async_support/waitable_set.rs @@ -75,8 +75,8 @@ extern_wasm! { #[link_name = "[waitable-join]"] fn join(waitable: u32, set: u32); #[link_name = "[waitable-set-wait]"] - fn wait(_: u32, _: *mut [u32; 2]) -> u32; + fn wait(set: u32, event: *mut [u32; 2]) -> u32; #[link_name = "[waitable-set-poll]"] - fn poll(_: u32, _: *mut [u32; 2]) -> u32; + fn poll(set: u32, event: *mut [u32; 2]) -> u32; } } diff --git a/crates/guest-rust/src/rt/native_imports.rs b/crates/guest-rust/src/rt/native_imports.rs index 2908432e4..a65478df5 100644 --- a/crates/guest-rust/src/rt/native_imports.rs +++ b/crates/guest-rust/src/rt/native_imports.rs @@ -48,6 +48,15 @@ pub unsafe extern "C" fn __wit_bindgen_set_import_resolver( RESOLVER.store(resolver, Ordering::Release); } +/// Builds the `&CStr` for an import name from a `concat!(name, "\0")` +/// literal, for the runtime's own intrinsic shims. +pub(crate) const fn cstr(with_nul: &'static str) -> &'static CStr { + match CStr::from_bytes_with_nul(with_nul.as_bytes()) { + Ok(s) => s, + Err(_) => panic!("import name contains an interior NUL"), + } +} + /// Called by generated import shims on their first invocation. /// /// `module` and `name` are the strings described on [`ImportResolver`]. diff --git a/crates/rust/Cargo.toml b/crates/rust/Cargo.toml index 6b6e1773a..02657c498 100644 --- a/crates/rust/Cargo.toml +++ b/crates/rust/Cargo.toml @@ -41,6 +41,8 @@ test-helpers = { path = '../test-helpers' } # For use with the custom attributes test serde_json = { workspace = true } bytes = "1" +# For the native end-to-end test +libloading = "0.8" [features] serde = ['dep:serde', 'wit-bindgen-core/serde'] diff --git a/crates/rust/src/interface.rs b/crates/rust/src/interface.rs index 86e00c570..c5d21285e 100644 --- a/crates/rust/src/interface.rs +++ b/crates/rust/src/interface.rs @@ -725,26 +725,79 @@ macro_rules! {macro_name} {{ } } + // Natively the intrinsics are resolved through the host's import + // resolver, exactly like every other import. + let rt = self.r#gen.runtime_path(); + let handle = || ("handle".to_string(), "u32"); + let mut extra: Vec<(String, &str)> = Vec::new(); + if let PayloadFor::Stream = payload_for { + extra.push(("amt".to_string(), "usize")); + } + let mut native_intrinsics = String::new(); + for (rust_name, core_name, params, result) in [ + ( + "new", + format!("[{import_prefix}-new-{index}]{func_name}"), + vec![], + Some("u64"), + ), + ( + "cancel_write", + format!("[{import_prefix}-cancel-write-{index}]{func_name}"), + vec![handle()], + Some("u32"), + ), + ( + "cancel_read", + format!("[{import_prefix}-cancel-read-{index}]{func_name}"), + vec![handle()], + Some("u32"), + ), + ( + "drop_writable", + format!("[{import_prefix}-drop-writable-{index}]{func_name}"), + vec![handle()], + None, + ), + ( + "drop_readable", + format!("[{import_prefix}-drop-readable-{index}]{func_name}"), + vec![handle()], + None, + ), + ( + "start_read", + format!("[async-lower][{import_prefix}-read-{index}]{func_name}"), + [ + vec![handle(), ("ptr".to_string(), "*mut u8")], + extra.clone(), + ] + .concat(), + Some("u32"), + ), + ( + "start_write", + format!("[async-lower][{import_prefix}-write-{index}]{func_name}"), + [ + vec![handle(), ("ptr".to_string(), "*const u8")], + extra.clone(), + ] + .concat(), + Some("u32"), + ), + ] { + native_intrinsics.push_str(&crate::native_import_shim( + &module, &core_name, rust_name, ¶ms, result, rt, + )); + } + let code = format!( r#" #[doc(hidden)] #[allow(unused_unsafe)] pub mod vtable{ordinal} {{ - #[cfg(not(target_arch = "wasm32"))] - unsafe extern "C" fn cancel_write(_: u32) -> u32 {{ unreachable!() }} - #[cfg(not(target_arch = "wasm32"))] - unsafe extern "C" fn cancel_read(_: u32) -> u32 {{ unreachable!() }} - #[cfg(not(target_arch = "wasm32"))] - unsafe extern "C" fn drop_writable(_: u32) {{ unreachable!() }} - #[cfg(not(target_arch = "wasm32"))] - unsafe extern "C" fn drop_readable(_: u32) {{ unreachable!() }} - #[cfg(not(target_arch = "wasm32"))] - unsafe extern "C" fn new() -> u64 {{ unreachable!() }} - #[cfg(not(target_arch = "wasm32"))] - unsafe extern "C" fn start_read(_: u32, _: *mut u8{start_extra}) -> u32 {{ unreachable!() }} - #[cfg(not(target_arch = "wasm32"))] - unsafe extern "C" fn start_write(_: u32, _: *const u8{start_extra}) -> u32 {{ unreachable!() }} + {native_intrinsics} // Work around a behavior of LLD where in a shared library when an address // is taken of an imported function that only shows up as a `GOT.func` diff --git a/crates/rust/src/lib.rs b/crates/rust/src/lib.rs index 2597679d8..9ef1643da 100644 --- a/crates/rust/src/lib.rs +++ b/crates/rust/src/lib.rs @@ -1904,10 +1904,6 @@ fn wasm_type(ty: WasmType) -> &'static str { } } -/// Declares the core import `wasm_import_module`/`wasm_import_name` as a -/// function named `rust_name`. On `wasm32` this is a plain linker-resolved -/// import; natively it's a shim that asks the host's resolver for the -/// implementation on first call (see `rt::resolve_import`) and caches it. fn declare_import( wasm_import_module: &str, wasm_import_name: &str, @@ -1929,20 +1925,19 @@ fn declare_import( sig.push_str(wasm_type(*result)); } - let named_params: Vec = params + let named_params: Vec<(String, &str)> = params .iter() .enumerate() - .map(|(i, ty)| format!("arg{i}: {}", wasm_type(*ty))) + .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(", "); + let native = native_import_shim( + wasm_import_module, + wasm_import_name, + rust_name, + &named_params, + results.first().map(|r| wasm_type(*r)), + rt, + ); format!( r#" @@ -1952,18 +1947,54 @@ fn declare_import( #[link_name = "{wasm_import_name}"] fn {rust_name}{sig}; }} + {native} + "#, + ) +} + +/// Emits the native (non-`wasm32`) definition of the core import +/// `module`/`name`: an `unsafe extern "C" fn` named `rust_name` that asks the +/// host's import resolver for the implementation on first call (see +/// `rt::resolve_import`) and caches it. `params` are `(name, type)` pairs. +fn native_import_shim( + module: &str, + name: &str, + rust_name: &str, + params: &[(String, &str)], + result: Option<&str>, + rt: &str, +) -> String { + let named_params = params + .iter() + .map(|(arg, ty)| format!("{arg}: {ty}")) + .collect::>() + .join(", "); + let param_types = params + .iter() + .map(|(_, ty)| *ty) + .collect::>() + .join(", "); + let call_args = params + .iter() + .map(|(arg, _)| arg.as_str()) + .collect::>() + .join(", "); + let ret_sig = result.map(|r| format!(" -> {r}")).unwrap_or_default(); + format!( + r#" #[cfg(not(target_arch = "wasm32"))] - unsafe extern "C" fn {rust_name}({named_params_str}){ret_sig} {{ + unsafe extern "C" fn {rust_name}({named_params}){ret_sig} {{ 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(c"{wasm_import_module}", c"{wasm_import_name}"); - CACHE.store(ptr, ::core::sync::atomic::Ordering::Release); + // Named so as not to shadow any parameter. + let mut __impl = CACHE.load(::core::sync::atomic::Ordering::Acquire); + if __impl.is_null() {{ + __impl = {rt}::resolve_import(c"{module}", c"{name}"); + CACHE.store(__impl, ::core::sync::atomic::Ordering::Release); }} - let f: unsafe extern "C" fn{sig} = unsafe {{ ::core::mem::transmute(ptr) }}; - unsafe {{ f({call_args}) }} + let __func: unsafe extern "C" fn({param_types}){ret_sig} = unsafe {{ ::core::mem::transmute(__impl) }}; + unsafe {{ __func({call_args}) }} }} "#, ) diff --git a/crates/rust/tests/native-e2e/Cargo.toml b/crates/rust/tests/native-e2e/Cargo.toml new file mode 100644 index 000000000..841c211c1 --- /dev/null +++ b/crates/rust/tests/native-e2e/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "native-e2e-plugin" +version = "0.0.0" +edition = "2024" +publish = false + +[lib] +crate-type = ["cdylib"] + +[dependencies] +wit-bindgen = { path = "../../../guest-rust" } + +# Deliberately not a member of the wit-bindgen workspace: `tests/native_e2e.rs` +# builds this crate with a nested `cargo build` and `dlopen`s the result. +[workspace] diff --git a/crates/rust/tests/native-e2e/src/lib.rs b/crates/rust/tests/native-e2e/src/lib.rs new file mode 100644 index 000000000..50b56d87e --- /dev/null +++ b/crates/rust/tests/native-e2e/src/lib.rs @@ -0,0 +1,63 @@ +//! Guest side of `tests/native_e2e.rs`: a plugin built as a native `cdylib`. +//! The test provides its imports through the import resolver. + +wit_bindgen::generate!({ + inline: r#" + package test:native-e2e; + + interface host { + log: func(msg: string); + add: func(a: u32, b: u32) -> u32; + + resource counter { + constructor(start: u32); + bump: func() -> u32; + } + + enum kind { small, large } + fetch: async func(id: s64) -> result, string>; + } + + world plugin { + use host.{kind}; + import host; + + export greet: func(name: string, times: u32) -> string; + export sum: func(xs: list) -> u32; + export produce: async func(n: s64) -> result, kind, s64>, string>; + } + "#, +}); + +use test::native_e2e::host::{self, Counter}; +use wit_bindgen::rt::async_support::StreamReader; + +struct Plugin; + +impl Guest for Plugin { + fn greet(name: String, times: u32) -> String { + host::log(&format!("greet({name}, {times})")); + let counter = Counter::new(10); + let mut total = 0; + for _ in 0..times { + total = host::add(total, counter.bump()); + } + format!("hello {name}: {total}") + } + + fn sum(xs: Vec) -> u32 { + xs.iter().sum() + } + + /// Awaits an async import, then returns a stream it has written to. + async fn produce(n: i64) -> Result<(StreamReader, Kind, i64), String> { + let (kind, count) = host::fetch(n).await?; + let (mut tx, rx) = wit_stream::new::(); + let unwritten = tx.write_all((1..=count as u8).collect()).await; + assert!(unwritten.is_empty()); + drop(tx); + Ok((rx, kind, count * 2)) + } +} + +export!(Plugin); diff --git a/crates/rust/tests/native_e2e.rs b/crates/rust/tests/native_e2e.rs new file mode 100644 index 000000000..41c8394dd --- /dev/null +++ b/crates/rust/tests/native_e2e.rs @@ -0,0 +1,488 @@ +//! End-to-end test for native (non-wasm) linking. Builds the plugin in +//! `tests/native-e2e` as a `cdylib`, `dlopen`s it, checks the world marker, +//! installs an import resolver, and calls its exports through the core ABI. +//! The sync and async parts of the world have separate tests that share the +//! loaded library. + +#![cfg(not(target_arch = "wasm32"))] + +use libloading::{Library, Symbol}; +use std::env::consts::{DLL_PREFIX, DLL_SUFFIX}; +use std::ffi::{CStr, c_char}; +use std::mem::size_of; +use std::path::PathBuf; +use std::process::Command; +use std::ptr; +use std::sync::OnceLock; +use std::sync::atomic::{AtomicPtr, Ordering}; +use wit_bindgen_core::symbol_name::make_external_component; + +const WORLD: &str = "test:native-e2e/plugin"; +const HOST: &str = "test:native-e2e/host"; + +/// Arbitrary non-null context passed to `__wit_bindgen_set_import_resolver`. +/// The guest never dereferences it; it only passes it back to the resolver, +/// which checks that it did. A real host would pass a pointer to its state. +const CTX: *mut () = 0x1234 as *mut (); + +type Realloc = unsafe extern "C" fn(*mut u8, usize, usize, usize) -> *mut u8; +type ImportResolver = unsafe extern "C" fn(*mut (), *const c_char, *const c_char) -> *mut (); +type SetImportResolver = unsafe extern "C" fn(Option, *mut ()); + +/// The plugin's allocator, saved at load time so imports can allocate guest +/// memory. +static REALLOC: AtomicPtr<()> = AtomicPtr::new(ptr::null_mut()); + +/// Copies `bytes` into guest memory. The guest takes ownership of the +/// allocation. +unsafe fn lower_bytes(bytes: &[u8], align: usize) -> *mut u8 { + let realloc: Realloc = unsafe { std::mem::transmute(REALLOC.load(Ordering::Acquire)) }; + let dst = unsafe { realloc(ptr::null_mut(), 0, align, bytes.len()) }; + assert!(!dst.is_null()); + unsafe { ptr::copy_nonoverlapping(bytes.as_ptr(), dst, bytes.len()) }; + dst +} + +// Core signatures: `u32` is `i32`, `string` is `(*mut u8, usize)`, and a +// resource handle is an `i32` index into a host-side table. +mod sync_host { + use super::HOST; + use std::sync::Mutex; + + pub static LOG: Mutex> = Mutex::new(Vec::new()); + pub static COUNTERS: Mutex = Mutex::new(Counters(Vec::new())); + + unsafe extern "C" fn log(ptr: *mut u8, len: usize) { + let msg = unsafe { std::slice::from_raw_parts(ptr, len) }; + LOG.lock() + .unwrap() + .push(String::from_utf8(msg.to_vec()).unwrap()); + } + + unsafe extern "C" fn add(a: i32, b: i32) -> i32 { + (a as u32).wrapping_add(b as u32) as i32 + } + + /// Table of `counter` resources. Dropped counters stay as `None` so a + /// double drop is caught. + /// + /// Handles are 1-based; the guest's `Resource` reserves `0` and `u32::MAX`. + pub struct Counters(Vec>); + + impl Counters { + fn create(&mut self, start: u32) -> i32 { + self.0.push(Some(start)); + self.0.len() as i32 + } + + fn bump(&mut self, handle: i32) -> u32 { + let value = self + .slot(handle) + .as_mut() + .expect("bump on a dropped counter"); + *value += 1; + *value + } + + fn release(&mut self, handle: i32) { + assert!(self.slot(handle).take().is_some(), "counter dropped twice"); + } + + fn slot(&mut self, handle: i32) -> &mut Option { + &mut self.0[handle as usize - 1] + } + + /// Number of counters created. + pub fn created(&self) -> usize { + self.0.len() + } + + /// Number of counters not yet dropped. + pub fn live(&self) -> usize { + self.0.iter().filter(|c| c.is_some()).count() + } + + // Core-ABI entry points for `[constructor]counter`, + // `[method]counter.bump`, and `[resource-drop]counter`. + unsafe extern "C" fn abi_new(start: i32) -> i32 { + COUNTERS.lock().unwrap().create(start as u32) + } + + unsafe extern "C" fn abi_bump(handle: i32) -> i32 { + COUNTERS.lock().unwrap().bump(handle) as i32 + } + + unsafe extern "C" fn abi_drop(handle: i32) { + COUNTERS.lock().unwrap().release(handle) + } + } + + pub fn resolve(module: &str, name: &str) -> Option<*mut ()> { + Some(match (module, name) { + (HOST, "log") => log as *mut (), + (HOST, "add") => add as *mut (), + (HOST, "[constructor]counter") => Counters::abi_new as *mut (), + (HOST, "[method]counter.bump") => Counters::abi_bump as *mut (), + (HOST, "[resource-drop]counter") => Counters::abi_drop as *mut (), + _ => return None, + }) + } +} + +// A minimal host runtime: enough of the async intrinsics for one async export +// that awaits one async import and writes one stream, with the host completing +// everything synchronously. Intrinsics the test does not expect are wired to +// functions that panic with their name. +mod async_host { + use super::HOST; + use std::ptr; + use std::sync::Mutex; + use std::sync::atomic::{AtomicPtr, Ordering}; + + // Encodings from `rt::async_support`. + const STATUS_RETURNED: u32 = 2; + const COMPLETED: u32 = 0x0; + + #[derive(Debug, Clone, PartialEq)] + pub enum Returned { + Ok { reader: u32, kind: u32, value: i64 }, + Err(String), + } + + pub static RETURNED: Mutex> = Mutex::new(Vec::new()); + pub static STREAMS: Mutex = Mutex::new(Streams(Vec::new())); + static CONTEXT: AtomicPtr = AtomicPtr::new(ptr::null_mut()); + + /// Table of streams, one per `stream.new`. Writes are buffered so they + /// complete immediately; otherwise the guest could not write before + /// returning the reader. + /// + /// Handles are non-zero: reader = 2i + 1, writer = 2i + 2. + pub struct Streams(Vec); + + pub struct Stream { + pub buf: Vec, + pub writer_dropped: bool, + } + + impl Streams { + fn create(&mut self) -> u64 { + self.0.push(Stream { + buf: Vec::new(), + writer_dropped: false, + }); + let i = (self.0.len() - 1) as u64; + let reader = 2 * i + 1; + let writer = 2 * i + 2; + (writer << 32) | reader + } + + fn write(&mut self, handle: u32, bytes: &[u8]) -> u32 { + self.slot(handle).buf.extend_from_slice(bytes); + ((bytes.len() as u32) << 4) | COMPLETED + } + + fn release_writer(&mut self, handle: u32) { + self.slot(handle).writer_dropped = true; + } + + fn slot(&mut self, handle: u32) -> &mut Stream { + &mut self.0[(handle as usize - 1) / 2] + } + + /// Number of streams created. + pub fn created(&self) -> usize { + self.0.len() + } + + /// The stream for either of its handles. + pub fn get(&self, handle: u32) -> &Stream { + &self.0[(handle as usize - 1) / 2] + } + + // Core-ABI entry points for `[stream-new-0]produce`, + // `[async-lower][stream-write-0]produce`, and + // `[stream-drop-writable-0]produce`. + unsafe extern "C" fn abi_new() -> u64 { + STREAMS.lock().unwrap().create() + } + + unsafe extern "C" fn abi_write(handle: u32, ptr: *const u8, amt: usize) -> u32 { + let bytes = unsafe { std::slice::from_raw_parts(ptr, amt) }; + STREAMS.lock().unwrap().write(handle, bytes) + } + + unsafe extern "C" fn abi_drop_writable(handle: u32) { + STREAMS.lock().unwrap().release_writer(handle) + } + } + + // `fetch: async func(id: s64) -> result, string>`. + // Writes the result to `results` (discriminant at 0, payload at 8 and + // 16) and completes before returning, so the status is `RETURNED` with + // no subtask handle. + unsafe extern "C" fn fetch(id: i64, results: *mut u8) -> i32 { + unsafe { + if id >= 0 { + let kind: u8 = if id < 10 { 0 } else { 1 }; + *results = 0; + *results.add(8) = kind; + *(results.add(16) as *mut i64) = id + 1; + } else { + let msg = "negative id"; + *results = 1; + *(results.add(8) as *mut *mut u8) = super::lower_bytes(msg.as_bytes(), 1); + *(results.add(16) as *mut usize) = msg.len(); + } + } + STATUS_RETURNED as i32 + } + + // `[task-return]produce` takes the flattened + // `result, kind, s64>, string>`. The ok and err arms + // share slots: slot 1 is a pointer holding either the stream handle or + // the string pointer, slot 2 a `usize` holding either the enum + // discriminant or the string length. + unsafe extern "C" fn task_return(disc: i32, a1: *mut u8, a2: usize, a3: i64) { + let returned = match disc { + 0 => Returned::Ok { + reader: a1 as usize as u32, + kind: a2 as u32, + value: a3, + }, + 1 => { + let bytes = unsafe { std::slice::from_raw_parts(a1, a2) }; + Returned::Err(String::from_utf8(bytes.to_vec()).unwrap()) + } + _ => panic!("bad result discriminant {disc}"), + }; + RETURNED.lock().unwrap().push(returned); + } + + unsafe extern "C" fn context_get() -> *mut u8 { + CONTEXT.load(Ordering::Acquire) + } + + unsafe extern "C" fn context_set(value: *mut u8) { + CONTEXT.store(value, Ordering::Release); + } + + // Intrinsics the guest can reach but this test does not expect. + macro_rules! unexpected { + ($($name:ident($($arg:ident: $ty:ty),*) $(-> $ret:ty)?;)*) => {$( + unsafe extern "C" fn $name($($arg: $ty),*) $(-> $ret)? { + $(let _ = $arg;)* + panic!(concat!("guest called `", stringify!($name), "`, which this test does not expect")) + } + )*}; + } + unexpected! { + waitable_set_new() -> u32; + waitable_set_drop(set: u32); + waitable_join(waitable: u32, set: u32); + waitable_set_wait(set: u32, event: *mut u32) -> u32; + waitable_set_poll(set: u32, event: *mut u32) -> u32; + subtask_drop(handle: u32); + subtask_cancel(handle: u32) -> u32; + task_cancel(); + thread_yield() -> bool; + backpressure_inc(); + backpressure_dec(); + stream_read(handle: u32, ptr: *mut u8, amt: usize) -> u32; + stream_cancel_write(handle: u32) -> u32; + stream_cancel_read(handle: u32) -> u32; + stream_drop_readable(handle: u32); + } + + pub fn resolve(module: &str, name: &str) -> Option<*mut ()> { + Some(match (module, name) { + (HOST, "[async-lower]fetch") => fetch as *mut (), + + ("$root", "[context-get-0]") => context_get as *mut (), + ("$root", "[context-set-0]") => context_set as *mut (), + ("$root", "[waitable-set-new]") => waitable_set_new as *mut (), + ("$root", "[waitable-set-drop]") => waitable_set_drop as *mut (), + ("$root", "[waitable-join]") => waitable_join as *mut (), + ("$root", "[waitable-set-wait]") => waitable_set_wait as *mut (), + ("$root", "[waitable-set-poll]") => waitable_set_poll as *mut (), + ("$root", "[subtask-drop]") => subtask_drop as *mut (), + ("$root", "[subtask-cancel]") => subtask_cancel as *mut (), + ("$root", "[thread-yield]") => thread_yield as *mut (), + ("$root", "[backpressure-inc]") => backpressure_inc as *mut (), + ("$root", "[backpressure-dec]") => backpressure_dec as *mut (), + + ("[export]$root", "[task-return]produce") => task_return as *mut (), + ("[export]$root", "[task-cancel]") => task_cancel as *mut (), + ("[export]$root", "[stream-new-0]produce") => Streams::abi_new as *mut (), + ("[export]$root", "[async-lower][stream-write-0]produce") => { + Streams::abi_write as *mut () + } + ("[export]$root", "[async-lower][stream-read-0]produce") => stream_read as *mut (), + ("[export]$root", "[stream-cancel-write-0]produce") => stream_cancel_write as *mut (), + ("[export]$root", "[stream-cancel-read-0]produce") => stream_cancel_read as *mut (), + ("[export]$root", "[stream-drop-writable-0]produce") => { + Streams::abi_drop_writable as *mut () + } + ("[export]$root", "[stream-drop-readable-0]produce") => stream_drop_readable as *mut (), + + _ => return None, + }) + } +} + +// Host-side + +unsafe extern "C" fn resolver(ctx: *mut (), module: *const c_char, name: *const c_char) -> *mut () { + assert_eq!(ctx, CTX, "resolver was handed the wrong context"); + let module = unsafe { CStr::from_ptr(module) }.to_str().unwrap(); + let name = unsafe { CStr::from_ptr(name) }.to_str().unwrap(); + sync_host::resolve(module, name) + .or_else(|| async_host::resolve(module, name)) + .unwrap_or(ptr::null_mut()) +} + +/// Builds `tests/native-e2e` with a nested `cargo build` and returns the +/// path of the shared library. +fn build_plugin() -> PathBuf { + let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")); + let target_dir = std::env::var_os("CARGO_TARGET_DIR") + .map(PathBuf::from) + .unwrap_or_else(|| manifest_dir.join("../../target")) + .join("native-e2e"); + let status = Command::new(env!("CARGO")) + .args(["build", "--quiet"]) + .current_dir(manifest_dir.join("tests/native-e2e")) + .env("CARGO_TARGET_DIR", &target_dir) + .status() + .expect("failed to run cargo"); + assert!(status.success(), "building the native-e2e plugin failed"); + target_dir + .join("debug") + .join(format!("{DLL_PREFIX}native_e2e_plugin{DLL_SUFFIX}")) +} + +/// The plugin, built and loaded once for all tests, with the world marker +/// checked, the allocator saved, and the import resolver installed. +fn plugin() -> &'static Library { + static PLUGIN: OnceLock = OnceLock::new(); + PLUGIN.get_or_init(|| { + let lib = unsafe { Library::new(build_plugin()) }.expect("failed to dlopen the plugin"); + + // Check the world marker first. Its name identifies the world, and + // calling it returns that name. + let marker: Symbol *const c_char> = + unsafe { export(&lib, &format!("__wit_bindgen_world_{WORLD}")) }; + assert_eq!(unsafe { CStr::from_ptr(marker()) }.to_str().unwrap(), WORLD); + + // Both of these are defined by the runtime crate. + let realloc: Symbol = unsafe { lib.get(b"__wit_bindgen_cabi_realloc\0") }.unwrap(); + REALLOC.store(*realloc as *mut (), Ordering::Release); + let set_resolver: Symbol = + unsafe { lib.get(b"__wit_bindgen_set_import_resolver\0") }.unwrap(); + unsafe { set_resolver(Some(resolver), CTX) }; + + lib + }) +} + +/// Looks up a core export by its unencoded core export name. +unsafe fn export<'a, T>(lib: &'a Library, core_name: &str) -> Symbol<'a, T> { + let symbol = format!("{}\0", make_external_component(core_name)); + unsafe { lib.get(symbol.as_bytes()) } + .unwrap_or_else(|e| panic!("plugin does not export `{core_name}`: {e}")) +} + +#[test] +fn native_plugin_round_trip() { + use sync_host::{COUNTERS, LOG}; + + let lib = plugin(); + + // `greet: func(name: string, times: u32) -> string` covers a string in + // each direction, a plain import, and a resource constructor, method, + // and drop. + let greet: Symbol *mut u8> = + unsafe { export(lib, "greet") }; + let post_greet: Symbol = + unsafe { export(lib, "cabi_post_greet") }; + + let name = "world"; + let arg = unsafe { lower_bytes(name.as_bytes(), 1) }; + let ret = unsafe { greet(arg, name.len(), 3) }; + // The return area holds the string as a `(ptr, len)` pair. + let out = unsafe { + let ptr = *(ret as *const *mut u8); + let len = *(ret.add(size_of::()) as *const usize); + String::from_utf8(std::slice::from_raw_parts(ptr, len).to_vec()).unwrap() + }; + unsafe { post_greet(ret) }; + + // The counter starts at 10 and is bumped three times: 11 + 12 + 13. + assert_eq!(out, "hello world: 36"); + assert_eq!(LOG.lock().unwrap().as_slice(), ["greet(world, 3)"]); + let counters = COUNTERS.lock().unwrap(); + assert_eq!(counters.created(), 1, "expected exactly one counter"); + assert_eq!(counters.live(), 0, "the counter was never dropped"); + drop(counters); + + // `sum: func(xs: list) -> u32` covers a non-byte list argument. + let sum: Symbol i32> = unsafe { export(lib, "sum") }; + let xs: [u32; 4] = [1, 2, 3, 4]; + let bytes = unsafe { std::slice::from_raw_parts(xs.as_ptr().cast::(), size_of_val(&xs)) }; + let arg = unsafe { lower_bytes(bytes, align_of::()) }; + assert_eq!(unsafe { sum(arg, xs.len()) }, 10); +} + +#[test] +fn native_async_plugin_round_trip() { + use async_host::{RETURNED, Returned, STREAMS}; + + let lib = plugin(); + + // An async export takes flattened parameters and returns a callback + // code. `0` means the task completed, via `task.return`, without needing + // the `[callback]` export. + let produce: Symbol i32> = + unsafe { export(lib, "[async-lift]produce") }; + + // Ok path: `fetch(3)` returns `(small, 4)`, the guest writes `1..=4` to + // the stream and returns `(reader, small, 8)`. + assert_eq!(unsafe { produce(3) }, 0, "task should exit immediately"); + let returned = RETURNED + .lock() + .unwrap() + .pop() + .expect("task.return was not called"); + let Returned::Ok { + reader, + kind, + value, + } = returned + else { + panic!("expected the ok arm, got {returned:?}"); + }; + assert_eq!((kind, value), (0, 8)); + let streams = STREAMS.lock().unwrap(); + assert_eq!(streams.created(), 1, "expected exactly one stream"); + assert_eq!( + reader, 1, + "reader handle should be the one stream.new handed out" + ); + let stream = streams.get(reader); + assert_eq!(stream.buf, [1, 2, 3, 4]); + assert!(stream.writer_dropped, "the guest should drop its writer"); + drop(streams); + + // Err path: the host lowers the error string into guest memory and it + // comes back through the err arm of `task.return`. + assert_eq!(unsafe { produce(-1) }, 0); + assert_eq!( + RETURNED.lock().unwrap().pop(), + Some(Returned::Err("negative id".to_string())) + ); + assert_eq!( + STREAMS.lock().unwrap().created(), + 1, + "no stream on the error path" + ); +} From 5ceeb2ef744c8dfe7f61fc17316d67674e321790 Mon Sep 17 00:00:00 2001 From: Bjorn Beishline <75190918+BjornTheProgrammer@users.noreply.github.com> Date: Thu, 3 Sep 2026 04:25:02 -0700 Subject: [PATCH 5/6] feat(rust) add comment for broken tests fix --- crates/rust/tests/codegen.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/crates/rust/tests/codegen.rs b/crates/rust/tests/codegen.rs index bc45b7542..d932fe239 100644 --- a/crates/rust/tests/codegen.rs +++ b/crates/rust/tests/codegen.rs @@ -24,8 +24,10 @@ mod multiple_paths { #[allow(unused, reason = "testing codegen, not functionality")] mod inline_and_path { wit_bindgen::generate!({ - // Deliberately not `test:paths` like `multiple_paths` above: two - // different worlds under one name collide on the native world marker. + // A different package from `multiple_paths` above. Two different + // worlds under one name corrupt each other's `component-type` sections + // on wasm, and collide on the world marker symbol natively. So this + // technically is a fix to the existing tests inline: r#" package test:inline-and-path-root; @@ -242,7 +244,7 @@ mod owning_method_chaining { mod borrowing_method_chaining { wit_bindgen::generate!({ inline: r#" - package test:method-chaining; + package test:borrowing-method-chaining; world test { resource a { constructor(); @@ -253,9 +255,7 @@ mod borrowing_method_chaining { } "#, generate_all, - chainable_methods: ["&all"], - // `owning_method_chaining` above binds the same world; keep the markers apart. - type_section_suffix: "-borrowing", + chainable_methods: ["&all"] }); } From 850e186d95c6b58ee8046017952c9258caa93dbf Mon Sep 17 00:00:00 2001 From: Bjorn Beishline <75190918+BjornTheProgrammer@users.noreply.github.com> Date: Thu, 3 Sep 2026 04:49:02 -0700 Subject: [PATCH 6/6] feat(ci) add a native e2e action --- .github/workflows/main.yml | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 64e778be9..e3d33e4de 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -211,6 +211,23 @@ jobs: - run: rustup component add rust-src - run: cargo miri test -p wit-bindgen --all-features + native_e2e: + name: Native E2E + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, macos-latest, windows-latest] + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v4 + with: + submodules: true + - name: Install Rust + run: rustup update stable --no-self-update && rustup default stable + # Builds the plugin in `crates/rust/tests/native-e2e` as a native cdylib, + # loads it, and drives it through the import resolver and core ABI. + - run: cargo test -p wit-bindgen-rust --test native_e2e + check: name: Check runs-on: ubuntu-latest @@ -305,6 +322,7 @@ jobs: needs: - test - test_unit + - native_e2e - rustfmt - build - verify-publish