Understanding Signature Checks in call_indirect
This page answers one question: indirect calls in WebAssembly — calls through function pointers, virtual methods, callbacks — carry a type and are checked at runtime, and code that runs fine natively traps in Wasm with “indirect call signature mismatch”. You want to understand what is checked, why, how compilers lower indirect calls, and what kinds of source code trigger the trap.
Prerequisites
- [ ] Basic understanding of tables and
call_indirect. - [ ] WAT reading skills.
- [ ] Optionally, C or C++ code that uses function pointers.
What call_indirect checks
call_indirect takes a table index and calls the function stored there. The instruction itself names an expected type — (call_indirect (type $sig) ...) —
and the engine checks, at the moment of the call, that the function in the slot has exactly that type: same parameter types in the same order, same result
types. If not, it traps. It also traps if the index is out of the table’s bounds or the slot is null.
The check exists for safety. Wasm’s validator guarantees that every direct call passes the right number and types of arguments. Indirect calls choose their
target at runtime, so the guarantee must be enforced at runtime: without it, calling a (i32) -> i32 function as (f64, f64) -> f64 would read garbage from the
operand stack and break the type safety that the whole sandbox depends on.
Type equivalence is structural
Function types are compared by structure, not by name or index: a function defined as (func (param i32 i32) (result i32)) in one module matches a
call_indirect (type $t) in another module whose $t is also (param i32 i32) (result i32). That allows tables shared between modules (dynamic linking,
JavaScript-provided functions) to work. With the GC proposal, type equivalence for recursive and subtyped types follows more elaborate rules (iso-recursive
canonicalisation), but for plain numeric signatures, “same parameter and result types” is the whole rule.
How compilers lower indirect calls
In C and C++, a function pointer is an index into the module’s function table (the linker places every address-taken function in it). Calling through the
pointer compiles to call_indirect with the pointer’s declared type. Virtual calls in C++ load a function index from the vtable and do the same. In Rust,
trait-object method calls (dyn Trait) and calls through fn pointers and &dyn Fn closures compile to call_indirect with the trait method’s signature.
typedef int (*binop)(int, int);
int add(int a, int b) { return a + b; }
int apply(binop f, int x, int y) { return f(x, y); } // call_indirect (type (func (param i32 i32) (result i32)))
What triggers the trap
The trap appears when the function stored in the table has a different Wasm type from what the call expects. In C and C++, that happens when code casts a function
pointer to an incompatible type and calls it — undefined behaviour that often “works” on native platforms because calling conventions tolerate it (extra
arguments ignored, void versus int return mixed up), but traps in Wasm. Classic examples: calling a void f(void) through an int (*)(void) pointer; callbacks
declared with fewer parameters than the caller passes; qsort comparators with slightly wrong signatures; casting between int and long parameter types on
targets where they differ (on wasm32 long is 32 bits, long long 64).
Avoiding mismatches
Fix the source: declare callbacks with exactly the signature callers use, write small wrapper functions with the expected signature instead of casting, and enable
compiler warnings for incompatible function-pointer casts (-Wcast-function-type in clang). Emscripten has a mode (-sEMULATE_FUNCTION_POINTER_CASTS) that adds
thunks to make some casts work, at a cost in size and speed; it is a porting aid, not a fix. Rust code that avoids unsafe transmutes of function pointers does not
hit this.
Typed function references
The typed function references proposal adds call_ref, which calls a function reference whose static type is already known — the check happens at validation,
not at the call. Compilers for languages with precise function types (and Wasm GC languages) can use it to avoid runtime signature checks and the table
indirection. For C-style function pointers stored as integers, call_indirect remains the mechanism.
Reading the type in WAT
In WAT, the expected type appears in the instruction, either as a reference to a type definition or inline:
(type $cmp (func (param i32 i32) (result i32)))
(table $fns 16 funcref)
(func $sort_with (param $cmp_idx i32) (param $a i32) (param $b i32) (result i32)
(call_indirect $fns (type $cmp) (local.get $a) (local.get $b) (local.get $cmp_idx)))
The last operand is the table index; the preceding operands are the arguments. When you investigate a trap, find the call_indirect in the trapping function,
note its type, then look up which function sits at the index used at runtime (a debugger or a logged index helps) and compare its declared type. The mismatch is
usually obvious once both are side by side — one extra parameter, an i64 where an i32 was expected, or a missing result.
Function pointers crossing the JavaScript boundary
Function pointers are just table indices, and JavaScript code sometimes holds them — for example, Emscripten’s addFunction returns an index for a JavaScript
callback inserted into the table, with a signature string you provide. If that signature string does not match how C code later calls the pointer, the call traps
with the same error. Keep signature strings and C declarations in one place, generate them where possible, and test each callback path once in the Wasm build.
The same applies to function indices passed between dynamically linked modules: both sides must agree on the type, or the first call traps.
Variadic functions
C variadic functions (printf-style) are lowered on wasm32 by passing a pointer to a buffer of arguments, so their Wasm type differs from what the source
signature suggests. Calling a variadic function through a non-variadic function pointer type — or the reverse — is a common source of signature mismatches in
ported code, and another case where native platforms tolerate what Wasm rejects. Declare function-pointer types for variadic functions exactly as variadic.
A habit that prevents most traps
Compile ported C and C++ with -Wcast-function-type and -Werror for that warning, and run the test suite once with UBSan’s function-type checks natively. Most
signature mismatches are found before the first Wasm build runs.
Expected output
You can explain that call_indirect checks bounds, null and exact function type; recognise that function types match structurally across modules; trace a
“signature mismatch” trap to a function-pointer cast in C or C++; and choose fixes — correct declarations, wrapper functions, warnings — over emulation flags.
Gotchas
- Casting function pointers in C. Undefined behaviour that traps in Wasm. Use wrappers.
- Callbacks with fewer parameters. Natively tolerated, traps in Wasm. Match signatures exactly.
longversuslong longassumptions. Sizes differ on wasm32. Use fixed-width types.- Relying on emulation flags. Slower and larger. Fix the code.
- Assuming names matter. Types match structurally; names do not.
- Variadic functions through non-variadic pointers. Their Wasm types differ. Declare pointer types as variadic.
Performance note
The signature check is a comparison of a type identifier — a few instructions — and engines often combine it with the bounds check; call_indirect costs about
one to two nanoseconds more than a direct call in current engines.
Frequently Asked Questions
Why does the error only appear in Wasm? Native calling conventions tolerate many mismatched casts; Wasm checks types exactly.
Can JavaScript functions be stored in tables?
Yes, via WebAssembly.Function (type reflection) or imports; they then have a Wasm type that is checked the same way.
Does the check prevent all control-flow attacks? It restricts targets to same-type functions; see control-flow integrity for what remains.
Are there tools to find bad casts? Clang warnings and sanitizers (UBSan’s function check) catch many before you run the Wasm build.
Why does a callback added with addFunction trap when called? Its signature string does not match the type C code uses for the call; keep them consistent.
How are variadic C functions lowered? Arguments are passed through a buffer pointer, so their Wasm type differs from the source signature’s appearance.
Which operand of call_indirect is the table index? The last one; the operands before it are the call’s arguments.
Do dynamically linked modules check types across modules? Yes — structurally, so both sides must agree on a function pointer’s type or the call traps.
Should cast warnings be errors in ported code?
Yes — making -Wcast-function-type an error catches most mismatches before the Wasm build runs.
Related
- Debugging indirect call signature mismatches — tracing the trap.
- Calling function pointers with call_indirect — the basics.
- Understanding Wasm control-flow integrity — security angle.
- Implementing virtual dispatch in Wasm — vtables.
← Back to Tables & Dynamic Linking