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 arei32
,i64
,f32
, andf64
.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.