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!