100% Free Forever
AI-Powered Learning
Industry Expert Content
Certificates & Badges
Learn At Your Own Pace
WebAssembly

Wasm Tables and Functions

How WebAssembly represents functions as first-class values, why linear memory can't hold them, and how Table enables indirect calls and function pointers.

Running WasmAdvanced9 min readJul 10, 2026
Analogies

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.

wat
(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 runtime

Growing Tables and Sharing Them From JavaScript

WebAssembly.Table is constructible from JavaScript exactly like Memorynew 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

Was this page helpful?

Topics covered

#WebAssembly#WebAssemblyStudyNotes#WebDevelopment#WasmTablesAndFunctions#Wasm#Tables#Functions#Can#StudyNotes#SkillVeris