From 50650fa0ff6617e29e8a40be41a3bfab221c66d3 Mon Sep 17 00:00:00 2001 From: sunnymar Date: Thu, 13 Aug 2026 19:07:29 +0300 Subject: [PATCH 1/6] Add Func::eq/Func::hash instead of a debug_function_index lookup Per review on the original debug_function_index approach: rather than adding a new Instance lookup coupled to VMContext's internal layout (imported-function array / func_refs array offsets, FuncRefIndex assignment order, etc.), expose plain identity equality and hashing on Func instead. Func now implements PartialEq/Eq/Hash as pointer-identity equality (same store, same underlying VMFuncRef). This lets a caller build a Func -> id map itself, e.g. by walking an instance's function index space once with the existing Instance::debug_function and inserting into a HashMap, which is exactly what a debug-snapshot tool needs to invert a captured funcref back to a serializable index. This keeps wasmtime's own surface small and avoids depending on unstable internal invariants of the funcref layout. Adds a regression test that walks a module whose functions are placed into a table out of index order (so FuncRefIndex assignment during translation doesn't track FuncIndex order), builds a Func -> index map via debug_function + the new Eq/Hash impls, and confirms every function round-trips through it. --- crates/wasmtime/src/runtime/func.rs | 33 +++++++++++++++ tests/all/debug.rs | 64 +++++++++++++++++++++++++++++ 2 files changed, 97 insertions(+) diff --git a/crates/wasmtime/src/runtime/func.rs b/crates/wasmtime/src/runtime/func.rs index b9520fdfb522..1a2a815ca314 100644 --- a/crates/wasmtime/src/runtime/func.rs +++ b/crates/wasmtime/src/runtime/func.rs @@ -291,6 +291,39 @@ const _: () = { assert!(core::mem::offset_of!(Func, store) == 0); }; +// Two `Func`s are equal if and only if they reference the exact same +// function: the same store, and the same underlying `VMFuncRef`. This is +// pointer-identity equality, not a deep comparison of behavior (e.g. two +// distinct closures that happen to compute the same result are *not* +// considered equal). +// +// This is useful for building a mapping from a `Func` back to some +// caller-defined identifier (for example, reconstructing the function index +// a `Func` came from within its instance) without wasmtime needing to expose +// such an inverse lookup itself: collect `(Func, id)` pairs by walking the +// forward direction once, and use `Func`'s `Eq`/`Hash` impls to build a +// `HashMap` from that. +// +// Comparing/hashing only reads the `StoreId` and the raw pointer bits of the +// `VMFuncRef` pointer, neither of which requires dereferencing the pointer, +// so this is safe to do even without an ambient `StoreOpaque` in scope. +impl PartialEq for Func { + #[inline] + fn eq(&self, other: &Func) -> bool { + self.store == other.store && self.unsafe_func_ref == other.unsafe_func_ref + } +} + +impl Eq for Func {} + +impl core::hash::Hash for Func { + #[inline] + fn hash(&self, state: &mut H) { + self.store.hash(state); + self.unsafe_func_ref.hash(state); + } +} + macro_rules! for_each_function_signature { ($mac:ident) => { $mac!(0); diff --git a/tests/all/debug.rs b/tests/all/debug.rs index 6ca80676eb45..573643b172ea 100644 --- a/tests/all/debug.rs +++ b/tests/all/debug.rs @@ -516,6 +516,70 @@ fn private_entity_access() -> wasmtime::Result<()> { Ok(()) } +// `Func` implements `PartialEq`/`Eq`/`Hash` as pointer-identity equality (the +// same store, and the same underlying `VMFuncRef`). This lets a debugger +// invert `Instance::debug_function` itself -- building a `Func -> index` map +// by walking every index once with `debug_function` and inserting into a +// `HashMap` -- without wasmtime needing to expose that inverse lookup as its +// own API. This module deliberately places functions into a table out of +// index order, so a naive "assume escape order tracks function index order" +// approach would not happen to work by coincidence. +#[test] +fn debug_function_identity_round_trips_through_a_caller_built_map() -> wasmtime::Result<()> { + let (module, mut store) = get_module_and_store( + |_| {}, + r#" + (module + (import "" "f" (func)) + (table 4 funcref) + (elem (i32.const 0) $f3 $f2 $f1 $f0) + (func $f0 (result i32) i32.const 0) + (func $f1 (result i32) i32.const 1) + (func $f2 (result i32) i32.const 2) + (func $f3 (result i32) i32.const 3)) + "#, + )?; + let host_func = Func::wrap(&mut store, || {}); + let instance = Instance::new(&mut store, &module, &[Extern::Func(host_func)])?; + + // The full function index space: 1 import + 4 defined functions. + let mut func_to_index = std::collections::HashMap::new(); + for index in 0..5u32 { + let f = instance.debug_function(&mut store, index).unwrap(); + func_to_index.insert(f, index); + } + assert_eq!( + func_to_index.len(), + 5, + "every index maps to a distinct Func" + ); + + for index in 0..5u32 { + let f = instance.debug_function(&mut store, index).unwrap(); + assert_eq!( + func_to_index.get(&f), + Some(&index), + "function {index} must round-trip through a caller-built map, \ + even though its funcref slot (for defined functions) was \ + assigned out of index order" + ); + } + + // A function that was never inserted into the map is not present, + // whether or not it happens to alias some other function's identity. + let unrelated_host_func = Func::wrap(&mut store, || {}); + assert_eq!(func_to_index.get(&unrelated_host_func), None); + assert_ne!(unrelated_host_func, host_func); + + // Two `Func` handles for the same underlying function -- even fetched + // independently -- compare equal. + let f2_again = instance.debug_function(&mut store, 2).unwrap(); + let f2 = instance.debug_function(&mut store, 2).unwrap(); + assert_eq!(f2, f2_again); + + Ok(()) +} + #[test] #[cfg_attr(miri, ignore)] #[cfg(target_pointer_width = "64")] // Threads not supported on 32-bit systems. From bd242e57272f670b16ac0bd541fd77267a065781 Mon Sep 17 00:00:00 2001 From: sunnymar Date: Thu, 13 Aug 2026 20:00:32 +0300 Subject: [PATCH 2/6] Address review: trim doc comment, move/simplify Func Eq test - Drop the narrative/use-case paragraph on the Func PartialEq/Eq/Hash impls; keep only the note that comparison only reads raw pointer bits without dereferencing. - Replace the debug.rs-specific test (which built a Func -> index map over a deliberately-scrambled table) with a smaller test living alongside the other Func tests, checking that the same function fetched multiple ways (by export name, via exports(), through a table) compares equal and hashes equal, and that a different function does not. --- crates/wasmtime/src/runtime/func.rs | 13 ------ tests/all/debug.rs | 64 ----------------------------- tests/all/func.rs | 51 +++++++++++++++++++++++ 3 files changed, 51 insertions(+), 77 deletions(-) diff --git a/crates/wasmtime/src/runtime/func.rs b/crates/wasmtime/src/runtime/func.rs index 1a2a815ca314..395aa49d61ff 100644 --- a/crates/wasmtime/src/runtime/func.rs +++ b/crates/wasmtime/src/runtime/func.rs @@ -291,19 +291,6 @@ const _: () = { assert!(core::mem::offset_of!(Func, store) == 0); }; -// Two `Func`s are equal if and only if they reference the exact same -// function: the same store, and the same underlying `VMFuncRef`. This is -// pointer-identity equality, not a deep comparison of behavior (e.g. two -// distinct closures that happen to compute the same result are *not* -// considered equal). -// -// This is useful for building a mapping from a `Func` back to some -// caller-defined identifier (for example, reconstructing the function index -// a `Func` came from within its instance) without wasmtime needing to expose -// such an inverse lookup itself: collect `(Func, id)` pairs by walking the -// forward direction once, and use `Func`'s `Eq`/`Hash` impls to build a -// `HashMap` from that. -// // Comparing/hashing only reads the `StoreId` and the raw pointer bits of the // `VMFuncRef` pointer, neither of which requires dereferencing the pointer, // so this is safe to do even without an ambient `StoreOpaque` in scope. diff --git a/tests/all/debug.rs b/tests/all/debug.rs index 573643b172ea..6ca80676eb45 100644 --- a/tests/all/debug.rs +++ b/tests/all/debug.rs @@ -516,70 +516,6 @@ fn private_entity_access() -> wasmtime::Result<()> { Ok(()) } -// `Func` implements `PartialEq`/`Eq`/`Hash` as pointer-identity equality (the -// same store, and the same underlying `VMFuncRef`). This lets a debugger -// invert `Instance::debug_function` itself -- building a `Func -> index` map -// by walking every index once with `debug_function` and inserting into a -// `HashMap` -- without wasmtime needing to expose that inverse lookup as its -// own API. This module deliberately places functions into a table out of -// index order, so a naive "assume escape order tracks function index order" -// approach would not happen to work by coincidence. -#[test] -fn debug_function_identity_round_trips_through_a_caller_built_map() -> wasmtime::Result<()> { - let (module, mut store) = get_module_and_store( - |_| {}, - r#" - (module - (import "" "f" (func)) - (table 4 funcref) - (elem (i32.const 0) $f3 $f2 $f1 $f0) - (func $f0 (result i32) i32.const 0) - (func $f1 (result i32) i32.const 1) - (func $f2 (result i32) i32.const 2) - (func $f3 (result i32) i32.const 3)) - "#, - )?; - let host_func = Func::wrap(&mut store, || {}); - let instance = Instance::new(&mut store, &module, &[Extern::Func(host_func)])?; - - // The full function index space: 1 import + 4 defined functions. - let mut func_to_index = std::collections::HashMap::new(); - for index in 0..5u32 { - let f = instance.debug_function(&mut store, index).unwrap(); - func_to_index.insert(f, index); - } - assert_eq!( - func_to_index.len(), - 5, - "every index maps to a distinct Func" - ); - - for index in 0..5u32 { - let f = instance.debug_function(&mut store, index).unwrap(); - assert_eq!( - func_to_index.get(&f), - Some(&index), - "function {index} must round-trip through a caller-built map, \ - even though its funcref slot (for defined functions) was \ - assigned out of index order" - ); - } - - // A function that was never inserted into the map is not present, - // whether or not it happens to alias some other function's identity. - let unrelated_host_func = Func::wrap(&mut store, || {}); - assert_eq!(func_to_index.get(&unrelated_host_func), None); - assert_ne!(unrelated_host_func, host_func); - - // Two `Func` handles for the same underlying function -- even fetched - // independently -- compare equal. - let f2_again = instance.debug_function(&mut store, 2).unwrap(); - let f2 = instance.debug_function(&mut store, 2).unwrap(); - assert_eq!(f2, f2_again); - - Ok(()) -} - #[test] #[cfg_attr(miri, ignore)] #[cfg(target_pointer_width = "64")] // Threads not supported on 32-bit systems. diff --git a/tests/all/func.rs b/tests/all/func.rs index 4c94c67e646a..c6eeb1a764fd 100644 --- a/tests/all/func.rs +++ b/tests/all/func.rs @@ -759,6 +759,57 @@ fn import_works() -> Result<()> { Ok(()) } +#[test] +fn func_eq_and_hash() -> Result<()> { + use std::collections::hash_map::DefaultHasher; + use std::hash::{Hash, Hasher}; + + fn hash_of(f: &Func) -> u64 { + let mut hasher = DefaultHasher::new(); + f.hash(&mut hasher); + hasher.finish() + } + + let mut store = Store::<()>::default(); + let module = Module::new( + store.engine(), + r#" + (module + (table (export "t") 1 1 funcref) + (func (export "f") (result i32) i32.const 0) + (elem (i32.const 0) 0)) + "#, + )?; + let instance = Instance::new(&mut store, &module, &[])?; + + // Fetching the same function twice yields equal `Func`s, even though + // each fetch adds a distinct `StoreData` entry, and this holds whether + // it's fetched the same way each time or via different paths (by + // export name vs. by walking `exports()` vs. through a table). + let f1 = instance.get_func(&mut store, "f").unwrap(); + let f2 = instance.get_func(&mut store, "f").unwrap(); + assert_eq!(f1, f2); + assert_eq!(hash_of(&f1), hash_of(&f2)); + + let f3 = instance + .exports(&mut store) + .find_map(|e| e.into_func()) + .unwrap(); + assert_eq!(f1, f3); + assert_eq!(hash_of(&f1), hash_of(&f3)); + + let t = instance.get_table(&mut store, "t").unwrap(); + let f4 = *t.get(&mut store, 0).unwrap().unwrap_func().unwrap(); + assert_eq!(f1, f4); + assert_eq!(hash_of(&f1), hash_of(&f4)); + + // A different function is not equal. + let other = Func::wrap(&mut store, || {}); + assert_ne!(f1, other); + + Ok(()) +} + #[test] #[cfg_attr(miri, ignore)] fn trap_smoke() -> Result<()> { From bbf8e5e9d3f437366f913c8db37544662dbd5cbb Mon Sep 17 00:00:00 2001 From: sunnymar Date: Thu, 13 Aug 2026 20:42:44 +0300 Subject: [PATCH 3/6] Replace Func Eq/Hash with is_same/identity_key that see through imports cfallin pointed out that plain PartialEq/Eq/Hash on Func was surprising: equality didn't hold when the same function was reached through a second instance (imported, then re-exported), because each instance holds its own copy of an imported function's VMFunctionImport record at a distinct address, even though it calls the same underlying code. Replace those trait impls with Func::is_same(store, a, b) and Func::identity_key(store), which take a store borrow so they can safely dereference into the VMFuncRef's own identity (its vmctx and calling convention entry points) instead of comparing the wrapper pointer's own address. That identity is preserved across the import/export copy boundary, so these correctly recognize the same function regardless of how it was reached. Extends the regression test to cover exactly that case: a function passed as an import to a second instance and re-exported from there. --- crates/wasmtime/src/runtime/func.rs | 65 +++++++++++++++++++---------- tests/all/func.rs | 50 +++++++++++++++------- 2 files changed, 78 insertions(+), 37 deletions(-) diff --git a/crates/wasmtime/src/runtime/func.rs b/crates/wasmtime/src/runtime/func.rs index 395aa49d61ff..e672996dd2e4 100644 --- a/crates/wasmtime/src/runtime/func.rs +++ b/crates/wasmtime/src/runtime/func.rs @@ -1,9 +1,9 @@ use crate::error::OutOfMemory; use crate::prelude::*; use crate::runtime::vm::{ - self, InterpreterRef, SendSyncPtr, StoreBox, VMArrayCallHostFuncContext, + self, InterpreterRef, SendSyncPtr, StoreBox, VMArrayCallFunction, VMArrayCallHostFuncContext, VMCommonStackInformation, VMContext, VMFuncRef, VMFunctionImport, VMOpaqueContext, - VMStoreContext, + VMStoreContext, VMWasmCallFunction, VmPtr, }; use crate::store::{Asyncness, AutoAssertNoGc, InstanceId, StoreId, StoreOpaque}; use crate::type_registry::RegisteredType; @@ -291,26 +291,6 @@ const _: () = { assert!(core::mem::offset_of!(Func, store) == 0); }; -// Comparing/hashing only reads the `StoreId` and the raw pointer bits of the -// `VMFuncRef` pointer, neither of which requires dereferencing the pointer, -// so this is safe to do even without an ambient `StoreOpaque` in scope. -impl PartialEq for Func { - #[inline] - fn eq(&self, other: &Func) -> bool { - self.store == other.store && self.unsafe_func_ref == other.unsafe_func_ref - } -} - -impl Eq for Func {} - -impl core::hash::Hash for Func { - #[inline] - fn hash(&self, state: &mut H) { - self.store.hash(state); - self.unsafe_func_ref.hash(state); - } -} - macro_rules! for_each_function_signature { ($mac:ident) => { $mac!(0); @@ -1226,6 +1206,47 @@ impl Func { Ok(()) } + /// Returns whether `a` and `b` refer to the same underlying function + /// within `store`, regardless of how each was reached (e.g. one fetched + /// directly and the other passed as an import to another instance and + /// re-exported from there). + /// + /// # Panics + /// + /// Panics if `a` or `b` are not owned by `store`. + pub fn is_same(store: impl AsContext, a: &Func, b: &Func) -> bool { + let store = store.as_context().0; + a.identity_key_raw(store) == b.identity_key_raw(store) + } + + /// Returns a key that uniquely identifies the underlying function this + /// `Func` refers to within `store`, suitable for use as a `HashMap` key. + /// + /// Two `Func`s produce equal keys if and only if [`Func::is_same`] would + /// return `true` for them. + /// + /// # Panics + /// + /// Panics if this `Func` is not owned by `store`. + pub fn identity_key(&self, store: impl AsContext) -> impl core::hash::Hash + Eq { + self.identity_key_raw(store.as_context().0) + } + + fn identity_key_raw( + &self, + store: &StoreOpaque, + ) -> ( + VmPtr, + Option>, + VmPtr, + ) { + // SAFETY: `vm_func_ref` validates that this pointer belongs to + // `store`, which we're borrowing for the duration of this call, so + // dereferencing it here is sound. + let func_ref = unsafe { self.vm_func_ref(store).as_ref() }; + (func_ref.vmctx, func_ref.wasm_call, func_ref.array_call) + } + #[inline] pub(crate) fn vm_func_ref(&self, store: &StoreOpaque) -> NonNull { self.store.assert_belongs_to(store.id()); diff --git a/tests/all/func.rs b/tests/all/func.rs index c6eeb1a764fd..9b4644a7325d 100644 --- a/tests/all/func.rs +++ b/tests/all/func.rs @@ -760,13 +760,13 @@ fn import_works() -> Result<()> { } #[test] -fn func_eq_and_hash() -> Result<()> { +fn func_is_same_and_identity_key() -> Result<()> { use std::collections::hash_map::DefaultHasher; use std::hash::{Hash, Hasher}; - fn hash_of(f: &Func) -> u64 { + fn hash_of(k: impl Hash) -> u64 { let mut hasher = DefaultHasher::new(); - f.hash(&mut hasher); + k.hash(&mut hasher); hasher.finish() } @@ -782,30 +782,50 @@ fn func_eq_and_hash() -> Result<()> { )?; let instance = Instance::new(&mut store, &module, &[])?; - // Fetching the same function twice yields equal `Func`s, even though - // each fetch adds a distinct `StoreData` entry, and this holds whether - // it's fetched the same way each time or via different paths (by - // export name vs. by walking `exports()` vs. through a table). + // Fetched by name and by name again: same function. let f1 = instance.get_func(&mut store, "f").unwrap(); let f2 = instance.get_func(&mut store, "f").unwrap(); - assert_eq!(f1, f2); - assert_eq!(hash_of(&f1), hash_of(&f2)); + assert!(Func::is_same(&store, &f1, &f2)); + assert_eq!( + hash_of(f1.identity_key(&store)), + hash_of(f2.identity_key(&store)) + ); + // Fetched via `exports()`: same function. let f3 = instance .exports(&mut store) .find_map(|e| e.into_func()) .unwrap(); - assert_eq!(f1, f3); - assert_eq!(hash_of(&f1), hash_of(&f3)); + assert!(Func::is_same(&store, &f1, &f3)); + // Fetched through a table: same function. let t = instance.get_table(&mut store, "t").unwrap(); let f4 = *t.get(&mut store, 0).unwrap().unwrap_func().unwrap(); - assert_eq!(f1, f4); - assert_eq!(hash_of(&f1), hash_of(&f4)); + assert!(Func::is_same(&store, &f1, &f4)); + + // Passed as an import to another instance and re-exported: still the + // same function, even though the two `Func`s wrap distinct pointers. + let importer = Module::new( + store.engine(), + r#" + (module + (import "" "f" (func $g (result i32))) + (export "reexported" (func $g))) + "#, + )?; + let importer_instance = Instance::new(&mut store, &importer, &[f1.into()])?; + let f5 = importer_instance + .get_func(&mut store, "reexported") + .unwrap(); + assert!(Func::is_same(&store, &f1, &f5)); + assert_eq!( + hash_of(f1.identity_key(&store)), + hash_of(f5.identity_key(&store)) + ); - // A different function is not equal. + // A different function is not the same. let other = Func::wrap(&mut store, || {}); - assert_ne!(f1, other); + assert!(!Func::is_same(&store, &f1, &other)); Ok(()) } From de3b122071ea1e39010b17b25eccf1ca90fa7b40 Mon Sep 17 00:00:00 2001 From: sunnymar Date: Fri, 14 Aug 2026 08:08:11 +0300 Subject: [PATCH 4/6] identity_key: drop wasm_call, add type_index per review cfallin suggested shrinking the identity key to just wasm_call, and Alex suggested folding in type_index for extra safety. Checked wasm_call empirically first: for a host function created via Func::wrap, wasm_call starts out None and is filled in lazily, in place, the first time that Func is paired with a module that has a matching trampoline (confirmed via a throwaway probe test). That means: - Two distinct never-imported host functions both read as wasm_call=None, so a key built from wasm_call alone would collide them. - Even for a single Func, the key would change over the store's lifetime as wasm_call flips from None to Some, which breaks HashMap usability outright rather than merely causing an occasional collision. array_call has neither problem (always present, stable), so the key is now (vmctx, array_call, type_index) instead of (vmctx, wasm_call, array_call). Also fixes the accompanying regression test, which previously wrapped two *different* closures (`|| {}` twice) to check that distinct host functions aren't `is_same` -- but two `|| {}` expressions are two distinct Rust types, so the assertion was passing for the wrong reason (different array_call, not different vmctx). Changed it to wrap the same closure value twice, so array_call is shared and only vmctx can discriminate, plus a regression test asserting identity_key is stable across the wasm_call None -> Some transition. --- crates/wasmtime/src/runtime/func.rs | 6 +++--- tests/all/func.rs | 18 ++++++++++++++++++ 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/crates/wasmtime/src/runtime/func.rs b/crates/wasmtime/src/runtime/func.rs index e672996dd2e4..5090d691752a 100644 --- a/crates/wasmtime/src/runtime/func.rs +++ b/crates/wasmtime/src/runtime/func.rs @@ -3,7 +3,7 @@ use crate::prelude::*; use crate::runtime::vm::{ self, InterpreterRef, SendSyncPtr, StoreBox, VMArrayCallFunction, VMArrayCallHostFuncContext, VMCommonStackInformation, VMContext, VMFuncRef, VMFunctionImport, VMOpaqueContext, - VMStoreContext, VMWasmCallFunction, VmPtr, + VMStoreContext, VmPtr, }; use crate::store::{Asyncness, AutoAssertNoGc, InstanceId, StoreId, StoreOpaque}; use crate::type_registry::RegisteredType; @@ -1237,14 +1237,14 @@ impl Func { store: &StoreOpaque, ) -> ( VmPtr, - Option>, VmPtr, + VMSharedTypeIndex, ) { // SAFETY: `vm_func_ref` validates that this pointer belongs to // `store`, which we're borrowing for the duration of this call, so // dereferencing it here is sound. let func_ref = unsafe { self.vm_func_ref(store).as_ref() }; - (func_ref.vmctx, func_ref.wasm_call, func_ref.array_call) + (func_ref.vmctx, func_ref.array_call, func_ref.type_index) } #[inline] diff --git a/tests/all/func.rs b/tests/all/func.rs index 9b4644a7325d..ee29e50350e3 100644 --- a/tests/all/func.rs +++ b/tests/all/func.rs @@ -827,6 +827,24 @@ fn func_is_same_and_identity_key() -> Result<()> { let other = Func::wrap(&mut store, || {}); assert!(!Func::is_same(&store, &f1, &other)); + // Two `Func`s wrapping the same closure *type* (so they share a + // monomorphized trampoline) are still not the same: only their + // individual `vmctx`s (heap-allocated per `wrap` call) distinguish them. + let c = || {}; + let host1 = Func::wrap(&mut store, c); + let host2 = Func::wrap(&mut store, c); + assert!(!Func::is_same(&store, &host1, &host2)); + + // A host function's `identity_key` is stable even though Wasmtime fills + // in its Wasm-calling-convention trampoline lazily, the first time it's + // paired with a module that has one -- so `identity_key` can't rely on + // that trampoline either. + let key_before = hash_of(host1.identity_key(&store)); + let importer_of_host1 = Module::new(store.engine(), r#"(module (import "" "" (func)))"#)?; + Instance::new(&mut store, &importer_of_host1, &[host1.into()])?; + let key_after = hash_of(host1.identity_key(&store)); + assert_eq!(key_before, key_after); + Ok(()) } From 2dfd4a94b7e55776e42c488d91926d387c8c18ba Mon Sep 17 00:00:00 2001 From: sunnymar Date: Fri, 14 Aug 2026 08:18:48 +0300 Subject: [PATCH 5/6] fix: avoid VMArrayCallFunction type name to fix c-api build VMArrayCallFunction is only re-exported from crate::runtime::vm under the component-model feature (crates/wasmtime/src/runtime/vm.rs), but wasmtime-c-api builds wasmtime with just ['runtime', 'std']. Naming the type directly in identity_key_raw's signature broke that build. VmPtr::addr() doesn't require naming the pointee type at the call site, so use that (a NonZero) instead of the typed VmPtr for the array_call field -- same identity, no cfg-gated import. --- crates/wasmtime/src/runtime/func.rs | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/crates/wasmtime/src/runtime/func.rs b/crates/wasmtime/src/runtime/func.rs index 5090d691752a..50507efa86a8 100644 --- a/crates/wasmtime/src/runtime/func.rs +++ b/crates/wasmtime/src/runtime/func.rs @@ -1,7 +1,7 @@ use crate::error::OutOfMemory; use crate::prelude::*; use crate::runtime::vm::{ - self, InterpreterRef, SendSyncPtr, StoreBox, VMArrayCallFunction, VMArrayCallHostFuncContext, + self, InterpreterRef, SendSyncPtr, StoreBox, VMArrayCallHostFuncContext, VMCommonStackInformation, VMContext, VMFuncRef, VMFunctionImport, VMOpaqueContext, VMStoreContext, VmPtr, }; @@ -1237,14 +1237,21 @@ impl Func { store: &StoreOpaque, ) -> ( VmPtr, - VmPtr, + core::num::NonZero, VMSharedTypeIndex, ) { // SAFETY: `vm_func_ref` validates that this pointer belongs to // `store`, which we're borrowing for the duration of this call, so // dereferencing it here is sound. let func_ref = unsafe { self.vm_func_ref(store).as_ref() }; - (func_ref.vmctx, func_ref.array_call, func_ref.type_index) + // `array_call`'s type (`VMArrayCallFunction`) is only re-exported + // under the `component-model` feature, so use its address rather + // than naming the type here. + ( + func_ref.vmctx, + func_ref.array_call.addr(), + func_ref.type_index, + ) } #[inline] From 0a8ed473585d29a0c44aa10b7d44471b960ba59f Mon Sep 17 00:00:00 2001 From: sunnymar Date: Fri, 14 Aug 2026 21:35:52 +0300 Subject: [PATCH 6/6] Explain why these identity key fields, per review nit --- crates/wasmtime/src/runtime/func.rs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/crates/wasmtime/src/runtime/func.rs b/crates/wasmtime/src/runtime/func.rs index 50507efa86a8..efacd63c146b 100644 --- a/crates/wasmtime/src/runtime/func.rs +++ b/crates/wasmtime/src/runtime/func.rs @@ -1244,9 +1244,10 @@ impl Func { // `store`, which we're borrowing for the duration of this call, so // dereferencing it here is sound. let func_ref = unsafe { self.vm_func_ref(store).as_ref() }; - // `array_call`'s type (`VMArrayCallFunction`) is only re-exported - // under the `component-model` feature, so use its address rather - // than naming the type here. + // `vmctx` disambiguates statically-same functions belonging to + // different instances of the same module; `array_call` is always + // present and unique per function; `type_index` is extra insurance + // that we disambiguate by Wasm-level signature too. ( func_ref.vmctx, func_ref.array_call.addr(),