Learning WebAssembly #5: Running Wasm in the Browser

Executing Wasm code in a browser via WebAssembly JavaScript API.


In the previous parts of this series, we already executed Wasm modules in a browser. In this part, we will continue in explaining the WebAssembly JavaScript API that makes all this possible.

Wasm Module Initializing

The easiest way to load and execute Wasm code in a browser is the WebAssembly.instantiateStreaming function:

WebAssembly
  .instantiateStreaming(fetch('some.wasm'))
  .then(obj => {
    ...
  });

The initialized object has two attributes: instance and module. The module object contains stateless Wasm code that can be efficiently shared and initialized multiple times. The instance object is stateful, executable instance of a Wasm module.

To make the Fetch API work you must serve the web page via HTTP(S).

Calling Wasm Functions

The interesting attribute of the instance object is the exports object which contains all exported functions from the Wasm module:

WebAssembly
  .instantiateStreaming(fetch('some.wasm'))
  .then(obj => {
    obj.instance.exports.myfunc1();
    obj.instance.exports.myfunc2();
  });

The JavaScript code can synchronously call the exports, which are exposed as normal JavaScript functions; with parameters and result values:

WebAssembly
  .instantiateStreaming(fetch('some.wasm'))
  .then(obj => {
    obj.instance.exports.myfunc1(1, 2, 3);
    var res = obj.instance.exports.myfunc2();
  });

Importing JavaScript Functions

JavaScript functions can also be synchronously called by Wasm code by passing in as imports to a Wasm module instance:

(module
  (import "js" "log"
    (func $log (param i32)))

  (func (export "logIt")
    i32.const 42
    call $log))

Now, we call the exported function with the imported console.log:

WebAssembly
  .instantiateStreaming(fetch('log.wasm'), {
    js: { log: console.log }
  })
  .then(({instance}) => {
    instance.exports.logIt();
  });

We can see the result in the dev console:

» 42

Advanced Logging Example

In the previous part, we have worked with Wasm memory objects. Now, we put it all together and create proper logging from a Wasm module.

We have to import a function taking two parameters: the offset and length of the data in the memory:

(module
  (import "js" "log"
    (func $log (param i32 i32)))

  (import "js" "mem" (memory 1))
  (data (i32.const 0) "Hello")

  (func (export "logIt")
    i32.const 0  ;; offset to log
    i32.const 5  ;; length to log
    call $log))

In JavaScript, the imported function takes a memory chunk defined by the offset and length parameters, decodes it as a string, and prints it to the console:

var mem = new WebAssembly.Memory({initial:1});

WebAssembly
  .instantiateStreaming(fetch('mem.wasm'), {
    js: {
      mem,
      log: (offset, length) =>
        logMemory(mem, offset, length)
    }
  })
  .then(({instance}) => {
    instance.exports.logIt();
  });

function logMemory(memory, offset, length) {
  var bytes = new Uint8Array(memory.buffer, offset, length);
  var str = new TextDecoder('utf8').decode(bytes);

  console.log(str);
}

We can see the result in the dev console:

» Hello

Global Variables

We can create global variables accessible from both JavaScript and Wasm, importable/exportable across one or more module instances.

First, we must define a variable in the global section:

(module
  (global $g (import "js" "glob") (mut i32))
  ...
)

We call it simply $g and define it as mutable, of type 32-bit integer. Optionally, we will import the global instance from JavaScript code, so we can modify it directly in JavaScript.

When the instance is not imported, a default value must be provided:

(global $g1 (mut i32) (i32.const 42))

Then, we add a getter and a setter for the global variable:

(module
  (global $g (import "js" "glob") (mut i32))

  (func (export "getGlobal")
        (result i32)
    global.get $g)

  (func (export "setGlobal")
        (param $value i32)
    local.get $value
    global.set $g)
)

Now, we can access and modify the same instance of the global variable in both Wasm and JavaScript:

// mutable global variable, default value: 2
var glob = new WebAssembly.Global({
             value: "i32", mutable: true}, 2);

WebAssembly
  .instantiateStreaming(fetch('glob.wasm'), {
    js: { glob }
  })
  .then(({instance}) => {
    console.log(instance.exports.getGlobal());
    // prints '2'

    glob.value = 3;
    console.log(instance.exports.getGlobal());
    // prints '3'

    instance.exports.setGlobal(4);
    console.log(instance.exports.getGlobal());
    // prints '4'
  });

Further Steps

In the next part of this series, we will see how to run Wasm instances in Node.js.

Stay tuned!