Skip to content

Commit ae12065

Browse files
feat(rust): identify native imports by module#name via a host resolver
Replaces the per-import __wit_bindgen_register_* hooks with a single __wit_bindgen_set_import_resolver entry point in the runtime crate, adds a per-world __wit_bindgen_world_* marker symbol so hosts can verify the world before calling anything, and un-prefixes __wit_bindgen_cabi_realloc. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent 3f87282 commit ae12065

6 files changed

Lines changed: 214 additions & 52 deletions

File tree

crates/guest-rust/src/lib.rs

Lines changed: 48 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -906,21 +906,59 @@ extern crate std;
906906
/// Imports are not resolved by the native linker. Each import calls through
907907
/// a function pointer that starts out null and is looked up on first use
908908
/// from a host-installed resolver: after loading the library a host calls
909-
/// the exported `__wit_bindgen_set_import_resolver` function (see
910-
/// `wit_bindgen::rt::ImportResolver`) with a callback that maps an import's
911-
/// core module and function names to a function pointer with the import's
912-
/// core signature. This means everything links whether or not a host is
913-
/// present, the bindings themselves define no global symbols (so any number
914-
/// of `generate!` invocations can coexist in one binary), and a host only
915-
/// needs to implement the imports it cares about — calling an import the
916-
/// resolver doesn't provide aborts with a message naming it.
909+
/// the exported `__wit_bindgen_set_import_resolver` function with a callback
910+
/// that maps an import's core module and function names to a function
911+
/// pointer with the import's core signature. Its signature is
912+
///
913+
/// ```c
914+
/// void __wit_bindgen_set_import_resolver(
915+
/// void *(*resolver)(void *ctx, const char *import),
916+
/// void *ctx);
917+
/// ```
918+
///
919+
/// where `import` is the import's core module and function name joined by
920+
/// `#` as one NUL-terminated string, e.g. `my:pkg/iface@1.0.0#[method]res.frob`
921+
/// (see `wit_bindgen::rt::ImportResolver`). Passing a null `resolver`
922+
/// uninstalls the current one.
923+
///
924+
/// This means everything links whether or not a host is present, the import
925+
/// shims define no global symbols at all, and a host only needs to implement
926+
/// the imports it cares about — calling an import the resolver doesn't
927+
/// provide aborts with a message naming it.
928+
///
929+
/// The resolver should be installed once, before any export is called: each
930+
/// import caches the pointer it was given, so a later
931+
/// `__wit_bindgen_set_import_resolver` call won't be seen by imports that
932+
/// have already been resolved. The resolver is also shared by every
933+
/// `generate!` invocation linked into the library and is keyed only by core
934+
/// module and function name, so two worlds importing the same core name
935+
/// necessarily get the same implementation.
917936
///
918937
/// Exports, including post-return functions, async callbacks, and resource
919938
/// destructors, are exported under their hex-encoded core export names. A
920939
/// `__wit_bindgen_cabi_realloc` function is also exported so hosts can
921940
/// allocate guest-owned memory when lowering arguments, as the canonical ABI
922-
/// requires. These two symbols and `__wit_bindgen_set_import_resolver` are
923-
/// defined once in the `wit-bindgen` runtime crate rather than per world.
941+
/// requires. That symbol and `__wit_bindgen_set_import_resolver` are defined
942+
/// once in the `wit-bindgen` runtime crate rather than per world.
943+
///
944+
/// Each world additionally exports a marker function
945+
/// `const char *__wit_bindgen_world_<world>(void)`, where `<world>` is the
946+
/// hex-encoded fully qualified world name including its package version and
947+
/// `type_section_suffix`, e.g. `my:pkg@1.0.0/my-world`. Since `dlsym` can't
948+
/// tell a host whether the library it opened implements the world it expects
949+
/// — and the canonical ABI lowering of a world changes with the world, so
950+
/// guessing wrong corrupts memory instead of failing cleanly — a host should
951+
/// look this symbol up before installing a resolver or calling an export, and
952+
/// treat a missing symbol as the wrong plugin. Calling it returns the world
953+
/// name as a NUL-terminated string, for the resulting error message.
954+
///
955+
/// This marker is keyed the same way as the `component-type` custom section a
956+
/// wasm build emits, so the rules are the ones a wasm build already imposes.
957+
/// Binding the same world twice needs a `type_section_suffix` to tell the
958+
/// markers apart (plus `export_prefix` for the core export names). Two
959+
/// *different* worlds sharing one fully qualified name is not a supported
960+
/// configuration at all: here the linker rejects the duplicate marker, and on
961+
/// wasm `wit-component` refuses to merge the two packages.
924962
///
925963
/// [WIT package]: https://component-model.bytecodealliance.org/design/packages.html
926964
#[cfg(feature = "macros")]

crates/guest-rust/src/rt/mod.rs

Lines changed: 42 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -198,67 +198,79 @@ pub unsafe fn cabi_realloc(
198198
/// import shim instead asks a host-installed resolver for its implementation
199199
/// the first time it's called, identifying the import by its core module and
200200
/// function name as plain strings. The host installs the resolver once per
201-
/// loaded library through `__wit_bindgen_set_import_resolver`, so the
202-
/// bindings themselves define no global symbols at all — any number of
203-
/// `generate!` invocations (even of the same world) can coexist in one
204-
/// binary.
201+
/// loaded library through `__wit_bindgen_set_import_resolver`, so the import
202+
/// shims define no global symbols at all and any number of `generate!`
203+
/// invocations can share one resolver. (Export symbols and world markers are
204+
/// a separate matter: those are global, so binding the same world twice in
205+
/// one binary needs `export_prefix` and `type_section_suffix` respectively to
206+
/// avoid duplicate symbols.)
207+
///
208+
/// Because each shim caches the pointer the resolver handed it, installing a
209+
/// resolver a second time has no effect on imports that have already been
210+
/// called. Hosts are expected to install one before calling any export.
205211
#[cfg(not(target_arch = "wasm32"))]
206212
mod native_imports {
213+
use core::ffi::{CStr, c_char};
207214
use core::sync::atomic::{AtomicPtr, Ordering};
208215

209216
/// A host-provided callback returning the implementation of the import
210-
/// named by `module`/`name` (the canonical ABI core import names, e.g.
211-
/// `my:pkg/iface` and `[method]res.frob`), or null if the host doesn't
212-
/// implement it. The returned pointer must be a function with the
213-
/// import's core signature. `ctx` is the value passed alongside the
214-
/// resolver, returned to the host on every call.
215-
pub type ImportResolver = unsafe extern "C" fn(
216-
ctx: *mut (),
217-
module: *const u8,
218-
module_len: usize,
219-
name: *const u8,
220-
name_len: usize,
221-
) -> *mut ();
217+
/// named by `import`, or null if the host doesn't implement it. The
218+
/// returned pointer must be a function with the import's core signature.
219+
/// `ctx` is the value passed alongside the resolver, returned to the host
220+
/// on every call.
221+
///
222+
/// `import` is the import's canonical ABI core module and function name
223+
/// joined by `#`, as a single NUL-terminated string — for example
224+
/// `my:pkg/iface@1.0.0#[method]res.frob`, or `$root#some-func` for a
225+
/// function imported at the top level of a world. (Note the package
226+
/// version trails the interface name here, unlike in a world name.) It
227+
/// points into the guest's static data and stays valid for as long as the
228+
/// library is loaded, so a host may use it as a lookup key without
229+
/// copying it.
230+
pub type ImportResolver = unsafe extern "C" fn(ctx: *mut (), import: *const c_char) -> *mut ();
222231

223232
static RESOLVER: AtomicPtr<()> = AtomicPtr::new(core::ptr::null_mut());
224233
static RESOLVER_CTX: AtomicPtr<()> = AtomicPtr::new(core::ptr::null_mut());
225234

226235
/// Installs the import resolver for this linkage unit. Hosts call this
227236
/// (typically via `dlsym`) after loading the library and before calling
228237
/// any export.
238+
///
239+
/// Passing `None` for `resolver` uninstalls the current one. That only
240+
/// affects imports which haven't been resolved yet; imports already
241+
/// called keep the pointer they cached.
229242
#[unsafe(no_mangle)]
230243
pub unsafe extern "C" fn __wit_bindgen_set_import_resolver(
231-
resolver: ImportResolver,
244+
resolver: Option<ImportResolver>,
232245
ctx: *mut (),
233246
) {
247+
let resolver = match resolver {
248+
Some(resolver) => resolver as *mut (),
249+
None => core::ptr::null_mut(),
250+
};
234251
RESOLVER_CTX.store(ctx, Ordering::Relaxed);
235252
// The release store of the resolver publishes the context above.
236-
RESOLVER.store(resolver as *mut (), Ordering::Release);
253+
RESOLVER.store(resolver, Ordering::Release);
237254
}
238255

239256
/// Called by generated import shims on their first invocation.
240-
pub fn resolve_import(module: &str, name: &str) -> *mut () {
257+
///
258+
/// `import` is the `module#name` string described on [`ImportResolver`].
259+
pub fn resolve_import(import: &CStr) -> *mut () {
260+
let display = import.to_str().unwrap_or("<invalid utf-8>");
241261
let resolver = RESOLVER.load(Ordering::Acquire);
242262
assert!(
243263
!resolver.is_null(),
244-
"import `{module}#{name}` was called before the host installed an \
264+
"import `{display}` was called before the host installed an \
245265
import resolver via `__wit_bindgen_set_import_resolver`"
246266
);
247267
let ctx = RESOLVER_CTX.load(Ordering::Relaxed);
248268
let resolver: ImportResolver = unsafe { core::mem::transmute(resolver) };
249-
let ptr = unsafe {
250-
resolver(
251-
ctx,
252-
module.as_ptr(),
253-
module.len(),
254-
name.as_ptr(),
255-
name.len(),
256-
)
257-
};
269+
let ptr = unsafe { resolver(ctx, import.as_ptr()) };
258270
assert!(
259271
!ptr.is_null(),
260272
"the host's import resolver provided no implementation for \
261-
import `{module}#{name}`"
273+
import `{display}`"
262274
);
263275
ptr
264276
}

crates/rust/src/bindgen.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ impl<'a, 'b> FunctionBindgen<'a, 'b> {
6767
&rust_name,
6868
params,
6969
results,
70-
&self.r#gen.r#gen.runtime_path().to_string(),
70+
self.r#gen.r#gen.runtime_path(),
7171
));
7272
rust_name
7373
}

crates/rust/src/interface.rs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -218,15 +218,15 @@ impl<'i> InterfaceGenerator<'i> {
218218
"new",
219219
&[abi::WasmType::Pointer],
220220
&[abi::WasmType::I32],
221-
&self.r#gen.runtime_path().to_string(),
221+
self.r#gen.runtime_path(),
222222
);
223223
let import_rep = crate::declare_import(
224224
&wasm_import_module,
225225
&format!("[resource-rep]{resource_name}"),
226226
"rep",
227227
&[abi::WasmType::I32],
228228
&[abi::WasmType::Pointer],
229-
&self.r#gen.runtime_path().to_string(),
229+
self.r#gen.runtime_path(),
230230
);
231231
uwriteln!(
232232
self.src,
@@ -1037,7 +1037,7 @@ fn abi_layout(&mut self) -> ::core::alloc::Layout {{
10371037
"call",
10381038
&sig.params,
10391039
&sig.results,
1040-
&self.r#gen.runtime_path().to_string(),
1040+
self.r#gen.runtime_path(),
10411041
);
10421042
let mut args = String::new();
10431043
for i in 0..params_lower.len() {
@@ -3005,7 +3005,7 @@ impl<'a> {camel}Borrow<'a>{{
30053005
"drop",
30063006
&[abi::WasmType::I32],
30073007
&[],
3008-
&self.r#gen.runtime_path().to_string(),
3008+
self.r#gen.runtime_path(),
30093009
);
30103010
uwriteln!(
30113011
self.src,

crates/rust/src/lib.rs

Lines changed: 46 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@ use std::str::FromStr;
1111
use wit_bindgen_core::abi::{Bitcast, WasmType};
1212
use wit_bindgen_core::{
1313
AsyncFilterSet, ChainableMethodFilterSet, ChainingMode, Files, InterfaceGenerator as _, Source,
14-
Types, WorldGenerator, dealias, name_package_module, uwrite, uwriteln, wit_parser::*,
14+
Types, WorldGenerator, dealias, name_package_module, symbol_name, uwrite, uwriteln,
15+
wit_parser::*,
1516
};
1617

1718
mod bindgen;
@@ -549,6 +550,46 @@ impl RustWasm {
549550
Ok(remapped)
550551
}
551552

553+
/// Emits this world's native marker symbol.
554+
///
555+
/// Native bindings are loaded by `dlsym`, which gives a host no way to
556+
/// tell whether the library it opened implements the WIT world it expects
557+
/// or something else — and the canonical ABI lowering of a world changes
558+
/// with the world, so guessing wrong corrupts memory rather than failing
559+
/// cleanly. So each world exports a marker function named for the world,
560+
/// which a host looks up before calling anything else, treating a missing
561+
/// symbol as the wrong plugin. Calling it returns the world name as a
562+
/// NUL-terminated string, for the error message.
563+
///
564+
/// The symbol is keyed on the world's name and `type_section_suffix`,
565+
/// exactly like the `component-type` custom section is on wasm, so the
566+
/// rules match what a wasm build already imposes: binding the same world
567+
/// twice needs a `type_section_suffix` to tell the two apart, and two
568+
/// different worlds sharing a fully qualified name is not a supported
569+
/// configuration in the first place — on wasm `wit-component` refuses to
570+
/// merge them, and here the linker refuses to define the symbol twice.
571+
fn finish_native_world_marker(&mut self, resolve: &Resolve, world: WorldId) {
572+
let world = &resolve.worlds[world];
573+
let pkg = world
574+
.package
575+
.map(|p| resolve.packages[p].name.to_string())
576+
.unwrap_or_default();
577+
let name = format!("{pkg}/{}", world.name);
578+
let suffix = self.opts.type_section_suffix.as_deref().unwrap_or("");
579+
let symbol = symbol_name::make_external_component(&format!("{name}{suffix}"));
580+
uwriteln!(
581+
self.src,
582+
r#"
583+
#[cfg(not(target_arch = "wasm32"))]
584+
#[unsafe(no_mangle)]
585+
#[allow(non_snake_case)]
586+
pub extern "C" fn __wit_bindgen_world_{symbol}() -> *const ::core::ffi::c_char {{
587+
c"{name}".as_ptr()
588+
}}
589+
"#
590+
);
591+
}
592+
552593
fn finish_runtime_module(&mut self) {
553594
if !self.rt_module.is_empty() {
554595
// As above, disable rustfmt, as we use prettyplease.
@@ -1501,7 +1542,7 @@ impl WorldGenerator for RustWasm {
15011542
let exports = mem::take(&mut self.export_modules);
15021543
self.emit_modules(exports);
15031544

1504-
1545+
self.finish_native_world_marker(resolve, world);
15051546
self.finish_runtime_module();
15061547
self.finish_export_macro(resolve, world);
15071548

@@ -1884,8 +1925,8 @@ fn wasm_type(ty: WasmType) -> &'static str {
18841925
/// first use from the host's import resolver (see `rt::resolve_import` and
18851926
/// the exported `__wit_bindgen_set_import_resolver`), identified by its core
18861927
/// module and function names as plain strings. Everything links whether or
1887-
/// not a host is present — the shim defines no global symbols, so any number
1888-
/// of `generate!` invocations can coexist in one binary — and calling an
1928+
/// not a host is present — the shim defines no global symbols, so import
1929+
/// shims never collide between `generate!` invocations — and calling an
18891930
/// import with no resolver (or one the host doesn't implement) aborts with a
18901931
/// message naming the import, just as the old `unreachable!()` stubs
18911932
/// aborted.
@@ -1940,7 +1981,7 @@ fn declare_import(
19401981
::core::sync::atomic::AtomicPtr::new(::core::ptr::null_mut());
19411982
let mut ptr = CACHE.load(::core::sync::atomic::Ordering::Acquire);
19421983
if ptr.is_null() {{
1943-
ptr = {rt}::resolve_import("{wasm_import_module}", "{wasm_import_name}");
1984+
ptr = {rt}::resolve_import(c"{wasm_import_module}#{wasm_import_name}");
19441985
CACHE.store(ptr, ::core::sync::atomic::Ordering::Release);
19451986
}}
19461987
let f: unsafe extern "C" fn{sig} = unsafe {{ ::core::mem::transmute(ptr) }};

0 commit comments

Comments
 (0)