Why Functions Can't Live in Linear Memory
Unlike C function pointers on native hardware, WebAssembly deliberately forbids storing a raw callable function reference as data inside linear memory — this is a security boundary, preventing a corrupted or attacker-controlled buffer from ever being reinterpreted as executable code. Instead, WebAssembly introduces a separate address space specifically for callable references: WebAssembly.Table, a resizable, typed array whose elements are funcref (or, with the reference-types/GC proposals, externref) values, indexed by ordinary integers just like an array but stored completely outside the linear-memory byte space.
Cricket analogy: It's like a stadium keeping the umpires' decision-review system on a separate secure network from the public Wi-Fi used for scoreboard updates — you never want spectator-writable data to be able to trigger an actual umpiring decision.
Indirect Calls: call vs call_indirect
A direct call instruction invokes a statically-known function by its index in the module — the equivalent of calling a named function directly in source code. call_indirect instead takes a runtime i32 index into a Table, looks up the funcref stored at that slot, verifies its type signature matches what's expected at the call site (throwing a trap if it doesn't), and only then invokes it — this is precisely how compiled C/C++ function pointers, C++ virtual method dispatch (vtables), and language-level closures/callbacks work once lowered to WebAssembly.
Cricket analogy: call is like a captain naming a specific bowler by name to bowl the next over; call_indirect is like the captain pointing to 'whoever's standing at mark 3 on the bowling rotation board' and the umpire checking that player is actually certified to bowl before allowing it.
(module
(type $binop (func (param i32 i32) (result i32)))
(func $add (type $binop) (param i32 i32) (result i32)
local.get 0
local.get 1
i32.add)
(func $mul (type $binop) (param i32 i32) (result i32)
local.get 0
local.get 1
i32.mul)
(table 2 funcref)
(elem (i32.const 0) $add $mul) ;; slot 0 = add, slot 1 = mul
(func (export "apply") (param $op i32) (param $a i32) (param $b i32) (result i32)
local.get $a
local.get $b
local.get $op
call_indirect (type $binop))) ;; picks $add or $mul at runtimeGrowing Tables and Sharing Them From JavaScript
WebAssembly.Table is constructible from JavaScript exactly like Memory — new WebAssembly.Table({ element: 'funcref', initial: 4, maximum: 16 }) — and grows via table.grow(delta, fillValue). Crucially, table.get(index) returns a function reference usable directly as a JavaScript callable (calling it just invokes the underlying Wasm function), and table.set(index, fn) lets JavaScript install a JS function into a slot a Wasm module will later call indirectly — this is the mechanism that lets a Wasm module invoke a JavaScript callback through an indirect call rather than an import.
Cricket analogy: It's like a substitutes' bench that the team management can both fill (table.set) and read from (table.get) — a physio can slot a specific reserve player into bench position 3, and later the captain can look up exactly who's sitting there and bring them on.
Table-based indirect calls with type-checking at the call site are exactly what makes WebAssembly's control-flow integrity guarantee possible: even if an attacker corrupts data inside linear memory, they cannot forge a valid funcref out of raw bytes to hijack execution, because funcrefs only ever originate from the table itself, never from memory.
call_indirect always checks the target's function signature against the type expected at the call site and traps (throws a RuntimeError on the JS side) on mismatch — but it does not protect against calling the wrong-but-same-signature function (e.g. accidentally invoking a 'subtract' function through a slot meant for 'add' when both share type (i32,i32)->i32); table index management remains the module author's responsibility.
- WebAssembly forbids storing raw callable function references inside linear memory, as a control-flow-integrity security measure.
- WebAssembly.Table is a separate, resizable, typed array holding funcref (or externref) values, indexed like an array.
- The call instruction invokes a statically-known function index directly; call_indirect looks up a Table slot at runtime.
- call_indirect verifies the target's type signature matches the call site's expected signature, trapping on mismatch.
- call_indirect is how compiled function pointers, C++ vtables, and closures/callbacks work once lowered to WebAssembly.
- JavaScript can construct, grow, read (table.get), and write (table.set) a Table just like it can with Memory.
- table.set lets JavaScript install a callback into a slot a Wasm module later invokes via call_indirect, without needing an import.
Practice what you learned
1. Why does WebAssembly forbid storing raw callable function references inside linear memory?
2. What is the key difference between the call and call_indirect instructions?
3. What happens if call_indirect's runtime type check finds the Table slot's function signature doesn't match what's expected at the call site?
4. What does WebAssembly.Table's table.set(index, fn) method allow JavaScript to do?
5. Which compiled-language feature is call_indirect most directly responsible for implementing once lowered to WebAssembly?
Was this page helpful?
You May Also Like
Memory Management in Wasm
How WebAssembly's linear memory model works, how it grows, and how it differs from garbage-collected host environments like JavaScript.
The WebAssembly JavaScript API
A tour of the WebAssembly global object's core classes — Module, Instance, Memory, Table, and Global — and how JavaScript orchestrates Wasm.
Passing Data Between JS and Wasm
Practical techniques for moving numbers, strings, and structured data across the JavaScript/WebAssembly boundary via linear memory.
Related Reading
Related Study Notes in Web Development
Browse all study notesWebSockets Study Notes
Web Development · 30 topics
Web DevelopmentgRPC Study Notes
Protocol Buffers · 30 topics
Web DevelopmentSpring Boot Study Notes
Java · 30 topics
Web DevelopmentFlask Study Notes
Python · 30 topics
Web DevelopmentDjango Study Notes
Python · 30 topics
Web DevelopmentNext.js Study Notes
JavaScript · 30 topics