Skip to content

Commit 50650fa

Browse files
committed
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.
1 parent 9957ff7 commit 50650fa

2 files changed

Lines changed: 97 additions & 0 deletions

File tree

crates/wasmtime/src/runtime/func.rs

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -291,6 +291,39 @@ const _: () = {
291291
assert!(core::mem::offset_of!(Func, store) == 0);
292292
};
293293

294+
// Two `Func`s are equal if and only if they reference the exact same
295+
// function: the same store, and the same underlying `VMFuncRef`. This is
296+
// pointer-identity equality, not a deep comparison of behavior (e.g. two
297+
// distinct closures that happen to compute the same result are *not*
298+
// considered equal).
299+
//
300+
// This is useful for building a mapping from a `Func` back to some
301+
// caller-defined identifier (for example, reconstructing the function index
302+
// a `Func` came from within its instance) without wasmtime needing to expose
303+
// such an inverse lookup itself: collect `(Func, id)` pairs by walking the
304+
// forward direction once, and use `Func`'s `Eq`/`Hash` impls to build a
305+
// `HashMap` from that.
306+
//
307+
// Comparing/hashing only reads the `StoreId` and the raw pointer bits of the
308+
// `VMFuncRef` pointer, neither of which requires dereferencing the pointer,
309+
// so this is safe to do even without an ambient `StoreOpaque` in scope.
310+
impl PartialEq for Func {
311+
#[inline]
312+
fn eq(&self, other: &Func) -> bool {
313+
self.store == other.store && self.unsafe_func_ref == other.unsafe_func_ref
314+
}
315+
}
316+
317+
impl Eq for Func {}
318+
319+
impl core::hash::Hash for Func {
320+
#[inline]
321+
fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
322+
self.store.hash(state);
323+
self.unsafe_func_ref.hash(state);
324+
}
325+
}
326+
294327
macro_rules! for_each_function_signature {
295328
($mac:ident) => {
296329
$mac!(0);

tests/all/debug.rs

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -516,6 +516,70 @@ fn private_entity_access() -> wasmtime::Result<()> {
516516
Ok(())
517517
}
518518

519+
// `Func` implements `PartialEq`/`Eq`/`Hash` as pointer-identity equality (the
520+
// same store, and the same underlying `VMFuncRef`). This lets a debugger
521+
// invert `Instance::debug_function` itself -- building a `Func -> index` map
522+
// by walking every index once with `debug_function` and inserting into a
523+
// `HashMap` -- without wasmtime needing to expose that inverse lookup as its
524+
// own API. This module deliberately places functions into a table out of
525+
// index order, so a naive "assume escape order tracks function index order"
526+
// approach would not happen to work by coincidence.
527+
#[test]
528+
fn debug_function_identity_round_trips_through_a_caller_built_map() -> wasmtime::Result<()> {
529+
let (module, mut store) = get_module_and_store(
530+
|_| {},
531+
r#"
532+
(module
533+
(import "" "f" (func))
534+
(table 4 funcref)
535+
(elem (i32.const 0) $f3 $f2 $f1 $f0)
536+
(func $f0 (result i32) i32.const 0)
537+
(func $f1 (result i32) i32.const 1)
538+
(func $f2 (result i32) i32.const 2)
539+
(func $f3 (result i32) i32.const 3))
540+
"#,
541+
)?;
542+
let host_func = Func::wrap(&mut store, || {});
543+
let instance = Instance::new(&mut store, &module, &[Extern::Func(host_func)])?;
544+
545+
// The full function index space: 1 import + 4 defined functions.
546+
let mut func_to_index = std::collections::HashMap::new();
547+
for index in 0..5u32 {
548+
let f = instance.debug_function(&mut store, index).unwrap();
549+
func_to_index.insert(f, index);
550+
}
551+
assert_eq!(
552+
func_to_index.len(),
553+
5,
554+
"every index maps to a distinct Func"
555+
);
556+
557+
for index in 0..5u32 {
558+
let f = instance.debug_function(&mut store, index).unwrap();
559+
assert_eq!(
560+
func_to_index.get(&f),
561+
Some(&index),
562+
"function {index} must round-trip through a caller-built map, \
563+
even though its funcref slot (for defined functions) was \
564+
assigned out of index order"
565+
);
566+
}
567+
568+
// A function that was never inserted into the map is not present,
569+
// whether or not it happens to alias some other function's identity.
570+
let unrelated_host_func = Func::wrap(&mut store, || {});
571+
assert_eq!(func_to_index.get(&unrelated_host_func), None);
572+
assert_ne!(unrelated_host_func, host_func);
573+
574+
// Two `Func` handles for the same underlying function -- even fetched
575+
// independently -- compare equal.
576+
let f2_again = instance.debug_function(&mut store, 2).unwrap();
577+
let f2 = instance.debug_function(&mut store, 2).unwrap();
578+
assert_eq!(f2, f2_again);
579+
580+
Ok(())
581+
}
582+
519583
#[test]
520584
#[cfg_attr(miri, ignore)]
521585
#[cfg(target_pointer_width = "64")] // Threads not supported on 32-bit systems.

0 commit comments

Comments
 (0)