Debugging Indirect Call Signature Mismatches
This page answers one task: a WebAssembly module traps with RuntimeError: indirect call signature mismatch (or “null function or function signature mismatch”,
depending on the engine), and you need to find which call and which function disagree — then fix the source so the call is correct, not just silenced.
Prerequisites
- [ ] A reproducible trap and a build with function names (name section) and ideally debug info.
- [ ]
wasm-tools printorwasm2watand a browser or Node debugger. - [ ] The source of the module (C, C++, or Rust with unsafe code).
What the trap tells you
The trap happens at a call_indirect instruction whose expected function type differs from the type of the function found in the table at the given index — or
whose slot is empty. Error wording varies: V8 reports “null function or function signature mismatch” for both cases in some versions; other engines distinguish
“indirect call to null” from “indirect call signature mismatch”. Either way, the trapping frame is the caller doing the indirect call, not the target. The
investigation therefore has two parts: find the call site and its expected type, then find what was in the table at that moment.
Step 1 — find the call site
The top Wasm frame in the stack trace is the function that executed the call_indirect. With the name section, it has a readable (possibly mangled) name. Print
the module and locate that function:
wasm-tools print app.wasm > app.wat
grep -n "(func \$qsort_r\b" app.wat # then read forward to the call_indirect instructions
A function may contain several call_indirects; note each one’s (type ...). With DWARF and Chrome’s C/C++ DevTools extension, pausing on the exception shows
the exact source line instead.
Step 2 — find the index and the target
Enable “Pause on exceptions” in DevTools and reproduce. When paused at the trap, the operand stack (or the local holding the function pointer in debug builds)
shows the table index. Look it up: the module’s element segments list which functions occupy which table slots at instantiation, and the exported table (or
__indirect_function_table in Emscripten builds) can be read from the console: instance.exports.__indirect_function_table.get(idx) returns the function, and
its name property often carries the Wasm function index or name. Compare that function’s type with the call’s expected type.
Step 3 — map back to the source
Common causes, in rough order of frequency:
- Function pointer casts in C/C++. A callback declared as
void cb(void*)passed whereint (*)(void*)is expected, or a comparator withconst void*vsvoid*differences that change nothing natively but the code casts the function pointer anyway. - Missing or extra parameters. Registering a handler with fewer parameters than the caller passes — tolerated natively, fatal in Wasm.
- Integer width differences.
longis 32-bit on wasm32; code mixinglongandlong longorsize_tanduint64_tin function-pointer types produces different Wasm types. - Emscripten JavaScript callbacks. A function added with
addFunction(fn, "vii")but called from C as returningint. - Dynamic linking. A side module compiled against a different header version than the main module.
- Null pointers. Calling an uninitialised function pointer (index 0 is reserved as null in toolchain conventions) gives the “null” variant.
Step 4 — fix the declaration, not the symptom
Replace casts with correctly typed wrapper functions:
// before: cast hides a mismatch
signal_register(SIGNAL_RESIZE, (handler_fn)on_resize); // on_resize(void) vs handler_fn(int, void*)
// after: an adapter with the exact type
static void on_resize_adapter(int sig, void* ctx) { (void)sig; (void)ctx; on_resize(); }
signal_register(SIGNAL_RESIZE, on_resize_adapter);
Enable -Wcast-function-type (and treat it as an error in ported code), and compile with -fsanitize=function (UBSan) in native test builds to find mismatched
calls before they reach Wasm. Emscripten’s -sEMULATE_FUNCTION_POINTER_CASTS makes some casts work by adding thunks; use it only as a temporary bridge
during porting, since it costs size and speed and hides real bugs.
Step 5 — verify and prevent regressions
Re-run the failing scenario, then add a test that exercises the indirect call path in the Wasm build. For codebases with many callbacks, add a CI job that builds with the cast warning as an error and runs UBSan natively; signature mismatches are cheap to prevent and expensive to debug after release.
Rust code
Safe Rust does not produce mismatched indirect calls. If a Rust module traps this way, look at unsafe code that transmutes function pointers, FFI declarations
for C functions with wrong signatures (extern "C" { fn cb(x: i32); } when C defines void cb(long long)), or mismatched versions of C libraries linked in.
Finding every risky cast in a large codebase
In large ported codebases, one trap is rarely the only mismatch; others wait on code paths not yet exercised. Rather than fixing them one trap at a time, search
systematically. Compile the whole project natively with -Wcast-function-type and collect every warning; each is a candidate. Run the native test suite with
UBSan’s function sanitizer, which reports calls through mismatched function-pointer types at runtime. In the Wasm build, list all call_indirect types and all
table entries’ types (a short script over wasm-tools print output can do this) and flag table entries whose type no call_indirect in the module uses — a sign
that something calls them through a different type. Prioritise callbacks registered with event systems, plugin interfaces and comparison functions, which are
the usual offenders.
When the mismatch is in a third-party library
Sometimes the cast lives in a dependency you do not control — an older C library that registers callbacks loosely. Options, in order of preference: upgrade to a version that fixed it (search the project’s issue tracker for “Wasm” or “Emscripten”), patch it locally with a typed adapter and contribute the fix upstream, or isolate the problematic path behind a wrapper that calls the library with exactly typed functions. Emulation flags are a last resort for code you cannot change, and their cost applies to the whole module, so measure before accepting them.
Engines report the same bug differently
Error messages vary between engines and versions, which matters when reading user reports. Collect the error type (RuntimeError) and message per browser, but
diagnose from the stack trace and WAT, which are consistent; treat “null function or function signature mismatch” in one browser and “indirect call signature
mismatch” in another as the same family of bug.
Keeping a record
Note each mismatch found — the callback, the cast, the fix — in the port’s documentation. Patterns repeat across a codebase, and the list becomes a checklist for reviewing similar code before it reaches the Wasm build.
Expected output
The trap is traced to call_indirect (type $vii) in dispatch_event, called with table index 147, which holds on_resize of type () -> (); the source registered
on_resize through a function-pointer cast; a typed adapter replaces the cast; -Wcast-function-type -Werror now catches similar code; and the event path is
covered by a Wasm-build test.
Gotchas
- Looking at the target function only. The trap is in the caller. Start there.
- Relying on emulation flags. They hide bugs and cost speed. Fix declarations.
- Assuming
longis 64-bit. On wasm32 it is 32-bit. - Confusing null and mismatch traps. Check whether the index is zero.
- Debugging stripped builds. Names are missing. Keep the name section for debugging.
- Fixing one trap at a time. Other mismatches wait on untested paths. Search systematically.
Performance note
Removing -sEMULATE_FUNCTION_POINTER_CASTS after fixing the casts shrank the module by 6% and sped up a callback-heavy benchmark by 9%.
Frequently Asked Questions
Why does the same code work natively? Native calling conventions tolerate many mismatches; Wasm checks exact types.
Can I see the table contents in DevTools?
Read the exported table from the console with table.get(i).
Does the GC proposal change this?
Typed function references (call_ref) catch mismatches at validation for code that uses them.
Is index 0 always null? By toolchain convention, slot 0 is left empty so null function pointers trap.
How do I find all risky casts, not just the one that trapped? Collect -Wcast-function-type warnings, run native tests with UBSan’s function sanitizer, and compare call_indirect types against table entries.
What if the bad cast is in a third-party library? Upgrade if fixed upstream, otherwise patch with a typed adapter and contribute it; emulation flags are a last resort.
Why do browsers report the same trap differently? Message wording varies by engine and version; diagnose from the stack trace and WAT, which are consistent.
Which callbacks are most often wrong? Event handlers, plugin interfaces and comparison functions registered through casts.
Related
- Understanding signature checks in call_indirect — the rules.
- Reading Wasm stack traces — finding the caller.
- Registering JavaScript callbacks with addFunction — Emscripten callbacks.
- Debugging unreachable executed traps — another trap class.
← Back to Tables & Dynamic Linking