WebAssembly can be represented in intermediate, human readable text format. This form is designed to be exposed in text editors and essentially human readable, unlike actual WebAssembly binary output.
S-expressions
S-expression, "symbolic expression", or sexpr
, sexp
is a notation for tree representations.
In S-expression, each node in the tree goes inside a pair of parenthesis - (...)
.
First label inside the parenthesis indicates what type of node it is and after there's a space separated list of attributes or child nodes.
WebAssembly S-expression would look like this:
(module (memory 1) (func))
Which represents a tree with a root node module
and two child nodes, a memory
node with the attribute 1
and a func
node.
Simplest Module
The simplest possible module is
(module)
It is empty, but still a valid module.
Functionality in the module
All code in WebAssembly module is grouped into functions, which have the following code structure:
(func <signature> <local vars> <body>)
Where:
- The signature declares what are the function parameters and its return value
- The local vars are local variables for the function with explicit types
- The body is linear list of low-level instructions
Signatures and parameters
The signature is a sequence of parameter type declarations followed by a list of return type declarations.
Note, that the absence of a (result)
means the function does not return anything.
Since each parameter has explicit type declaration, wasm
currently provides the following four types:
i32
: 32 integeri64
: 64 integerf32
: 32 floatf64
: 64 float
A single parameter is written (param i32)
and the return type is written (result i32)
. For example:
(func (param i32) (param i32) (result f64) ...)
This function takes two 32-bit integers and returns a 64-bit float. After the signature, local variables are listed with their type, like (local i32)
.
Parameters are just like locals except they are initialized with the value passed by the caller.
Getting and setting local parameters
Local variables/parameters can be read and written by the body of the function with local.get
and local.set
instructions.
These instructions refer to the item by its numeric index:
(func (param i32) (param f32) (local f64)
local.get 0
local.get 1
local.get 2
)
Therefore, local.get 0
would get param i32
, local.get 1
gets param f32
and etc.
Numeric addresses can be replaced with labels:
(func (param $p1 i32) (param $p2 f32) (local $l1 f64)
local.get $p1
local.get $p2
local.get $l1
)
Stack machines
wasm execution is defined in terms of stack machine, where the basic idea is that every type of instruction pushes and/or pops a certain number of defined type to/from a stack.
For example, local.get
is defined to push the value of the local it read onto the stack, and i32.add
pops two i32
values (it implicitly grabs the previous two values pushed onto the stack), computes their sum (modulo ) and pushes the resulting i32
value.
When a function is called, it starts with an empty stack which is gradually filled up and emptied as the body's instructions are executed. For example:
(func (param $p i32)
local.get $p
local.get $p
i32.add
)
The stack would look like this:
Before add:
[-----top-----]
[ $p ]
[ $p ]
[----bottom---]
After add:
[-----top-----]
[ $p + $p ]
[----bottom---]
The WebAssembly validation rules ensure the stack matches exaclty. So declaring (result f32)
will require you to have exactly one f32
at the end. So, if there's no result, the resulting stack would be empty.
Function body
Function body is a list of instruction that are followed as the function is called. For example:
(module
(func (param $lhs i32) (param $rhs i32) (result i32)
local.get $lhs
local.get $rhs
i32.add
)
)
The function gets two parameters, adds them together and returns the result.
Full list of opcodes available at execution spec page.
Calling the function
Like in ES module, wasm functions must be export
ed. Like local vars, functions are identified by an index by default, but can use labels:
(func $add ...)
Adding export:
(export "add" (func $add))
Here, add
is the name of the function that can be accessed in JavaScript and $add
picks out which WebAssembly function inside the module is being export
ed.
Final module:
(module
(func $add (param $lhs i32) (param $rhs i32) (result i32)
local.get $lhs
local.get $rhs
i32.add
)
(export "add" (func $add))
)
Exploring fundamentals
This section covers more advanced features of wasm text format.
Calling functions from other functions in the same module
The call
instruction calls a single function, given an index or a name.
For example, the following module contains two functions - one returns 42
, the other returns result + 1:
(module
(func $getAns (result i32)
i32.const 42
)
(func (export "getAnswerPlus1") (result $i32)
call $getAns
i32.const 1
i32.add
)
)
i32.const
defines a constant value and pushes it onto the stack.
In order to call the code in JavaScript:
WebAssembly.instantiateStreaming(fetch('call.wasm'))
.then(o => console.log(o.instance.exports.getAnswerPlus1()));
Importing functions (from JavaScript)
In previous section, we called WebAssembly function from JavaScript. Now we're going to import JavaScript function into WebAssembly module.
(module
(import "console" "log" (func $log (param i32)))
(func (export "logIt")
i32.const 13
call $log
)
)
We're asking to import the log
function from console
module.
Exported logIt
function calls imported function using call
instruction.
Imported functions are just like regular wasm functions, they have a signature that WebAssembly validation checks statically, and they are given an index and can be named and called.
We can also pass any JavaScript to our wasm module. For example:
const obj = {
console: {
log: arg => console.log(arg),
},
};
WebAssembly.instantiateStreaming(fetch('logger.wasm'), obj)
.then(o => o.instance.exports.logIt());
WebAssembly globals
WebAssembly has the ability to create global variable instances, accessible from both JavaScript and importable/exportable across one or more WebAssembly.Module
instances.
This allows dynamil linking of multiple modules.
In wat
, it would look something like this:
(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))
)
)
)
We specify a global value using global
keyword and using mut
to specify that value is mutable.
Equivalent JavaScript expression:
const global = new WebAssembly.Global({
value: 'i32',
mutable: true,
}, 0);