I've recently started exploring WebAssembly, focusing on the language itself, by writing and testing small snippets of handwritten WASM text (WAT). This post describes what I've learned about using indirect calls via function tables.

It shows how to invoke WASM-defined and imported functions via indirect calls, and discusses some related nuances of the WASM value stack.

The full code sample is on GitHub, but it's also short enough for me to reproduce here. This is the entire WAT file for this post:

(module
    ;; The common type we use throughout the sample.
    (type $int2int (func (param i32) (result i32)))

    ;; Import a function named jstimes3 from the environment and call it
    ;; $jstimes3 here.
    (import "env" "jstimes3" (func $jstimes3 (type $int2int)))

    ;; Simple function that adds its parameter to itself and returns the sum.
    (func $wasmtimes2 (type $int2int)
        (i32.add (local.get 0) (local.get 0))
    )

    ;; Declare the dispatch function table to have 32 slots, and populate slots
    ;; 16 and 17 with functions.
    ;; This uses the WASMv1 default table 0.
    (table 32 funcref)
    (elem (i32.const 16) $wasmtimes2 $jstimes3)

    ;; The following two functions are exported to JS; when JS calls them, they
    ;; invoke functions from the table.

    (func (export "times2") (type $int2int)
        ;; Place the value of the first parameter on the stack for the function
        ;; call_indirect will invoke.
        local.get 0

        ;; This call_indirect invokes a function of the given type from table at
        ;; offset 16. The parameters to this function are expected to be on
        ;; the stack.
        (call_indirect (type $int2int) (i32.const 16))
    )

    (func (export "times3") (type $int2int)
        ;; This is the same as times2, except it takes the function to call from
        ;; offset 17 in the table.
        local.get 0
        (call_indirect (type $int2int) (i32.const 17))
    )
)

It starts by declaring a type that all functions in this sample use: a function with a single i32 parameter and an i32 return type.

Then, it defines two functions: one ($jstimes3) is imported from the environment; we'll see the actual function shortly. The other is a simple WASM function that adds its parameter to itself and returns the result.

Next, it adds these functions to a table, which in WASM parlance is a dispatch table for functions. This is what WASM uses to perform indirect function calls; when you think about function pointers, or references to functions, or more generally first-class functions - this is how they work in WASM.

In WASM v1, there's only a single table in a module, at implicit index 0. With v2 there can be multiple tables and the index has to be specified explicitly, but we'll focus on v1 here. The following code:

(table 32 funcref)
(elem (i32.const 16) $wasmtimes2 $jstimes3)

First declares the table to have 32 slots of function references [1]. Then, it populates the table (starting at offset 16) with the two functions previously defined in the module. I'm using these indices (and not just 0 and 1) to help weed out potential value confusion errors; you can replace them by any offset you wish, as long as it's consistent across the code.

Next, we export two functions to the embedding environment. These perform the actual dynamic call through the table. We'll get back to these functions later; first let's see how to run the example.

Compiling and running the WAT sample

This WAT file is saved in table.wat in my sample. To compile it to the binary WASM format (which can be directly loaded by browsers and other embedding environments), we'll run the wat2wasm tool from the WebAssembly Binary Toolkit:

$ wat2wasm table.wat

This creates a table.wasm file in the same directory. Now we're ready to embed it and see it run; there are many embedding environments supporting WASM these days, but the easiest to work with from the command-line is probably Node.js, because it emulates the browser embedding environment very well [2]. We'll write the following JS and save it in table.js:

const fs = require('fs');
const wasmfile = fs.readFileSync(__dirname + '/table.wasm');

// This object is imported into wasm.
const importObject = {
    env: {
        jstimes3: (n) => 3 * n,
    }
}

WebAssembly.instantiate(new Uint8Array(wasmfile), importObject).then(obj => {
    // Get two exported functions from wasm.
    let times2 = obj.instance.exports.times2;
    let times3 = obj.instance.exports.times3;

    console.log('times2(12) =>', times2(12));
    console.log('times3(12) =>', times3(12));
});

If you've compiled the WAT file to WASM as instructed and it's in the same directory as table.js, this should work:

$ node table.js
times2(12) => 24
times3(12) => 36

Let's trace what happens when Node invokes times3(12):

  1. times3 in the JS code is taken from the exports of the loaded WASM object.
  2. In the WAT code, the times3 function performs an indirect call through the function table, calling the function at offset 17 and forwarding it the parameter of the call.
  3. What's at offset 17? Looking at the elem command, it's the function $jstimes3.
  4. Looking further up in the WAT code, $jstimes3 identifies a function imported from the embedding environment's env object, named jstimes3.
  5. Now looking in the JS again, jstimes3 is defined as (n) => 3 * n in the env key of the import object: a function that multiplies its parameter by 3.

Phew! Quite a journey - from JS into WASM, stored in a dispatch table, and called from JS again. This sample kicks the tires rather thoroughly.

call_indirect in detail

Since our two functions that perform an indirect call are similar, let's just focus on one of them:

(func (export "times3") (type $int2int)
  local.get 0
  (call_indirect (type $int2int) (i32.const 17))
)

This invocation uses the WAT folded instruction capability of allowing s-exprs instead of a linear instruction sequence. The only required static parameter to call_direct is the type index, so we can rewrite the function's contents as follows:

local.get 0
i32.const 17
call_indirect (type $int2int)

When call_indirect is called, it takes the function index in the table from the top of the value stack. That's why i32.const 17 comes immediately before the call.

The (type $int2int) parameter is required; it we try to omit it, the wat2wasm compilation will not complain (I wonder why), but during execution we'll run into an error:

RuntimeError: null function or function signature mismatch

As mentioned above, our original usage of call_indirect relies on a folded instruction to provide the function index in-line in the call expression. Maybe we can place the whole thing inline:

(call_indirect (type $int2int) (i32.const 17) (local.get 0))

This produces a runtime error again:

RuntimeError: null function or function signature mismatch

Interestingly, it will work fine if we flip the order of the last two arguments:

(call_indirect (type $int2int) (local.get 0) (i32.const 17))

At first sight, this is paradoxical: don't we have to pass in the function index first, and only then the parameter to the dynamically-called function?

To understand why there's no paradox here, we have to learn a bit about how the WASM stack works. The first thing to learn is that binary instructions and calls expect their arguments on the stack in reverse order (the first argument deepest in the stack while the last is on top of the stack). To demonstrate this, here's a function that performs subtraction of two i32 values:

(func (export "dosub1")
(param i32) (param i32)
(result i32)
  local.get 0
  local.get 1
  i32.sub
)

We push the first argument on the stack first (local.get 0), and then the second argument. This means that when i32.sub is called, the stack looks like this:

| param 1 |    <<-- top of stack
|---------|
| param 0 |
-----------

The second thing to learn is how WASM's folded instructions are compiled [3]. An equivalent subtraction function that uses folded instructions is:

(func (export "dosub2")
(param i32) (param i32)
(result i32)
  (i32.sub (local.get 0) (local.get 1))
)

This is exactly equivalent to the dosub1 function. The WAT compiler unfolds the folded instruction into the same sequence:

local.get 0
local.get 1
i32.sub

So far so good; now let's get back to our first attempt at fully folding the indirect call:

(call_indirect (type $int2int) (i32.const 17) (local.get 0))

As discussed, this is equivalent to:

i32.const 17
local.get 0
call_indirect (type $int2int)

But herein lies the catch. When the call_indirect instruction executes, it only needs a single stack argument - the function index. The value on top of the stack when it executes is the local.get 0, which is not the function index - so we get an error. If you recall, the fully unfolded form that does work is:

local.get 0
i32.const 17
call_indirect (type $int2int)

This is because i32.const 17 is the argument to call_indirect; it takes it from the top of the stack and executes to find a function in the table. Then, once the function is found, that function takes its own arguments from the stack in the usual order, and that's where it finds the local.get 0 it needs.

This is why listing the arguments in reverse in the folded form works:

(call_indirect (type $int2int) (local.get 0) (i32.const 17))

The folded form is definitely useful when all of the arguments are actually passed to the instruction/call that heads the s-expr. For more complex stack interactions like call_indirect, the folded form seems more confusing than helpful. This is why I originally wrote this function as:

(func (export "times3") (type $int2int)
  local.get 0
  (call_indirect (type $int2int) (i32.const 17))
)

It clearly distinguishes between the different kinds of arguments; i32.const 17 belongs to the call_indirect, so it's part of the s-expr. local.get 0 has to be on the stack for the dynamically-called function, so it's left out separately.


[1]The type used for function references is funcref; in some older code samples you'll see anyfunc used instead, but this is outdated. The WASM standard settled on funcref and tools like wat2wasm will reject anyfunc.
[2]I've also written the same program using Go and the wazero runtime; it's on GitHub too.
[3]In one sense, WAT is to WASM like assembly language is to machine code; therefore, it may make sense of talking about assembling WAT code into WASM. On the other hand, with syntactic sugar like folded instructions WAT is much higher level than most assembly languages, so the term compiling makes some sense too. In this post I'm using compiling for consistency.