WebAssembly Text Format

January 14, 2020 | 4 min read
  • wasm

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 integer
  • i64: 64 integer
  • f32: 32 float
  • f64: 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 2322^{32}) 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 exported. 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 exported.

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);

WebAssembly Memory

WebAssembly Tables

  • /options/chin317/geo-population
editOnGithub();