WebAssembly text format

Modules

The simplest module is (module).

Functions

All wasm code is grouped into functions, which have the following structure:

( func <signature> <locals> <body> )
  • signature declares parameters and return values. At most one return value can be declared.
  • locals are typed local variables. Number types are i32, i64, f32, and f64.
  • body is a list of instructions

Parameters are locals that are instantiated with a passed value. The local.get and local.set commands are used to access parameters and locals by index, with parameters being numbered first, and locals last:

(func (param i32) (param i32) (local i32) (result f64)
    local.get 0  ;; get first param
    local.get 1  ;; get second param
    local.get 2  ;; get first local
)

We can refer to functions and locals with names instead, prefixing each name with $:

(func $fn (param $p1 i32) (param $p2 i32) (local $loc i32) (result f64)
    local.get $p1  ;; get first param
    local.get $p2  ;; get second param
    local.get $loc ;; get first local
)

Stacks

local.get pushes the value of a local onto a stack. i32.add pops two i32 values and pushes the sum to the stack. The return value of a function is the final value that was left on the stack.

Exporting functions

To export the function $fn, we use (export "fn" (func $fn)):

(module
    (export "add" (func $add))
    (func $add (param $l i32) (param $r i32) (result i32)
        local.get $l
        local.get $r
        i32.add
    )
)

There’s also a shorthand for this:

(module
    (func (export "add") $add (param $l i32) (param $r i32) (result i32)
        local.get $l
        local.get $r
        i32.add
    )
)

Calling functions

call $name

Calling WASM functions from JavaScript

WebAssembly.instantiateStreaming(fetch("module.wasm")).then((obj) => {
  console.log(obj.instance.exports.add(1, 2)); // "3"
});

Calling JavaScript functions from WASM

(module
  (import "console" "log" (func $log (param i32)))
  (func (export "logIt")
    i32.const 13
    call $log))

The import node is importing the log function from the console module, and names it $log. Namespaces are always two deep. We can now call this function with call as usual.

Finally, we link console.log to this import by passing it to the WASM instantiator via an object:

const importObject = {
  console: {
    log(arg) {
      console.log(arg);
    },
  },
};

WebAssembly.instantiateStreaming(fetch("logger.wasm"), importObject).then(
  (obj) => {
    obj.instance.exports.logIt();
  },
);

Globals

Global values can be accessed from both JavaScript and WASM. The mut keyword makes the value mutable. The global keyword imports the global value, which must be instantiated and passed in on the javascript end.

(module
  (global $g (import "js" "global") (mut i32))
  (func (export "getGlobal") (result i32)
    (global.get $g))
  (func (export "incGlobal")
    (global.set $g
      (i32.add (global.get $g) (i32.const 1))))
)
const global = new WebAssembly.Global({ value: "i32", mutable: true }, 0);

WebAssembly.instantiateStreaming(fetch("global.wasm"), { js: { global } }).then(
  ({ instance }) => {
    console.log(instance.exports.getGlobal()); // 0
    global.value = 42;
    console.log(instance.exports.getGlobal()); // 42
    instance.exports.incGlobal();
    console.log(instance.exports.getGlobal()); // 43
  },
);

Memory

The memory datatype provides access to a contiguous array of bytes, which is accessed as an ArrayBuffer on the JavaScript side. Memory instances can be created by either JavaScript or WebAssembly and exported to the other.