Step by step towards wasm tables
Tables in Wasm can be used to store references to functions (= function pointers).
In its core tables are arrays of references. These references can be accessed by index from Wasm code.
Why Wasm needs tables
Normally you can also call Wasm functions by the call instruction. But, this only works when the function is known at compile time.
Tables can store function references at runtime. With that the function to be called can be determined at runtime.
Furthermore tables store the references as pointers, which is more efficient than storing the function directly in memory.
In relation to security it is also better to use tables, as Wasm can limit the function calls to a range of addresses, which reduces the risk of buffer overflow attacks.
The last use case for tables is dynamic linking: With that users of your module are allowed to add own functions from the outside by reference!
Working with tables
A table is initialized through the table instruction:
(module
(table $myTable 2 funcref) ; creates a table with 2 elements, all of which are function references
...
)
In that code the table command creates a table with the name $myTable and size of 2. This can contain all function references (= funcref).
Currently just one table per module is allowed.
Store a function
You can initialize a function reference with the elem instruction.
The following code adds the reference to the $add function to the table:
(module
(table $fns 2 funcref)
(elem (i32.const 0) $add) ;; add function reference at index 0
(func $add (param $x i32) (param $y i32) (result i32)
local.get $x
local.get $y
i32.add
)
)
Access a function
To access a function the call_indirect instruction is used.
This instruction needs two params: The type of the expected function and the address in the table.
The first param is used for type-safety in Wasm.
The following code shows you this in action:
(module
(type $add_type (func (param i32 i32) (result i32)))
(table $fns 2 funcref) ;; creates a table with 2 elements, all of which are function references
(elem (i32.const 0) $add)
(func $add (param $x i32) (param $y i32) (result i32)
local.get $x
local.get $y
i32.add
)
(func $main
i32.const 100
i32.const 100
(call_indirect (type $add_type) (i32.const 0))
drop
)
(start $main)
)
Advanced infos
The last section was the basic introduction to tables in Wasm. Now it is time for some advanced stuff around it.
Other types
The Wasm specification defines the tabletype as follows:
tabletype ::= limits reftype
The reftype can be one of funcref or externref. But there is also a note that other types may be introduced.
So it is possible that in the future other values than func refs could be stored!
The externref type are all objects passed by the host (e.g. the browser or any Wasm runtime). They are used to link dynamically functions into your Wasm module.
Store functions dynamically
You can store functions dynamically by calling the table.set instruction.
Let’s say that you have the following table:
(table $myTable 1 funcref) ;; creates a table with 1 elemnt, all of which are functions
You can store a function by passing the funcref to the table.set instruction.
Here is a function which does it:
;; Stores the function $funcIdx at $idx in $myTable
(func $store (param $idx i32) (param $funcIdx funcref)
;; This is the same as:
;; (table.set $myTable (local.get $idx) (local.get $funcIdx))
local.get $idx
local.get $funcIdx
table.set $myTable
)
Now when you have an $add function, you can store the function with the following instructions:
;; Store the $add function at pos 0 of the table
;; This is the same as the following:
;; (table.set $myTable (i32.const 0) (ref.func $add))
;; If you don't store the function, you'll get the error
;; "Uncaught (in promise) RuntimeError: null function or function signature mismatch"
i32.const 0
ref.func $add
call $store
You see that ref.func $add is the magic instructions which gets your reference.
Declaring the function
When you compile the samples from above, you may get the following error during compilation:
error: function 0 is not declared in any elem sections
ref.func $add
To make this work, you need to declare the function with the following code:
;; Declares the function, but doesn't stores it in the table
(elem declare func $add)
Well, what is the difference to the code from the first example?
(elem (i32.const 0) $add)
The instruction (elem (i32.const 0) $add) adds the $add function to index 0 in the table.
So the function is accessible directly after the compilation and startup.
You can see this in the debugger during the initialization:

On the contrary (elem declare func $add) just declares it. At this point the function is not callable by function references and not available during the compilation.
That’s why you need to use the table.set instruction to make use of it. This enables you to add functions dynamically to the table.
The debugger clearly shows that during the initialization:

A full working code sample
Here you have a full working code sample, that makes use of the table.set instruction and calls the function:
(module
(export "main" (func $main))
(type $add_type (func (param i32 i32) (result i32)))
(table $myTable 1 funcref) ;; creates a table with 1 elemnt, all of which are functions
;; Declares the function, but doesn't stores it. Alternative with storing: (elem (i32.const 0) $add)
(elem declare func $add)
(func $add (param $x i32) (param $y i32) (result i32)
local.get $x
local.get $y
i32.add
)
;; Stores the function $funcIdx at $idx in $myTable
(func $store (param $idx i32) (param $funcIdx funcref)
;;(table.set $myTable (local.get $idx) (local.get $funcIdx))
local.get $idx
local.get $funcIdx
table.set $myTable
)
;; Calls the function at index $idx in $myTable
(func $call (param $idx i32) (param $x i32) (param $y i32) (result i32)
;; Calls the function at pos $idx from $myTable
;; table.get only works when the type is not funcref
;; calls the function
local.get $x
local.get $y
;; Currently you cannot set the type dynamically or pass it via a function argument
;; If you want to dynamically call a function based on arguments, you'd need to use if-else-logic.
(call_indirect (type $add_type) (local.get $idx))
)
(func $main (result i32)
;; Store the $add function at pos 0 of the table
;; If you don't do this, you'll get the error
;; "Uncaught (in promise) RuntimeError: null function or function signature mismatch"
;; This is the same as the following:
;; (table.set $myTable (i32.const 0) (ref.func $add))
i32.const 0
ref.func $add
call $store
i32.const 0
i32.const 100
i32.const 100
call $call
)
)
Compile it and call the $main function to see how it works.
funcref vs. anyfunc
In some tutorials you’ll see two types that can be used to define elements in a table.
One is funcref and the other one is anyfunc.
To make it short: anyfunc is deprecated and has been replaced by funcref. If you use it with newer compilers you may get an error like the following:
error: unexpected token "anyfunc", expected funcref or externref.
See also this Github issue.
Conclusion
This article covered the different usages and gave samples for Wasm tables.
The concepts may be a little bit confusing at the first sight, but they are quite powerful!
Further resources
Even when this article was pretty long, I encourage you to check the Mutating tables and dynamic linking section of the MDN page. This shows you how to use tables to dynamic link different modules together.
MDN: WebAssembly.Table shows the usage of Wasm tables from the host side.