WASM Basics using WAST

#notes#webassembly
under construction This post is a work in progress, and may be updated at some point in the potentially distant future!

This document is my notes from my experiments with using WebAssembly. Use at your own risk!

You might want to check out these examples in a real repo, with a Makefile: github.com/richinfante/wasm-examples.

Environment

There’s a couple requirements to get this to work. First, we need a webserver which serves application/wasm content-types for your compiled webassembly modules. Browsers won’t load them otherwise. You can generate .wasm binaries using the WABT Toolkit, specifically the wat2wasm tool.

To compile, you simply run something like the following: wat2wasm <file> -o <outfile>

You can then instantiate the module. Theres examples in the MDN Docs and I also provide pages with examples in my wasm examples repo.

Another useful tool is this Reference Guide which list the available instructions and opcodes.

Basic Module Declaration

This module contains one function, which just returns a constant 32-bit integer 1337. In wasm modules, the only I/O types allowed for functions are numbers. We can get around this using memory and indices/lengths as you’ll see later.

We save this in a file (I called it example1/main.wasm). Then, I ran wat2wasm example1/main.wast -o example1/main.wasm to compile it.

(module
  (func (export "hello_world") (result i32)
    ;; Return a constant number
    (i32.const 1337)
  )
)

Next, I serve an web page containing the following script. For this, I’m using my simple server from Here.

<script>
  WebAssembly.instantiateStreaming(fetch('http://localhost:8888/example1/main.wasm'))
    .then(mod => {
      let x = mod.instance.exports.hello_world()
      // Using with this JavaScript, we can run the module and get it's return value
      // This should print `1337` to the dev console.
      console.log(x)
    })
</script>

Constants

As you saw above, that function just prints 1337. To accomplish this can use this instruction which is a rough equivalent of loading an immediate in normal assembly:

i32.const 1337 ;; This just pushes the constant 1337 onto the stack.

Stack Machine

Wasm functions as a basic stack machine. Operations like get_local push onto the stack, and operations like i32.add or i32.mul both pull two elements and push a single one. This module just adds two numbers together:

(module
  (func $add_i32 (export "add_i32") (param $p1 i32) (param $p2 i32) (result i32)
    ;; All Wasm items are accessed via their index.
    ;; $p1 and $p2 are just aliases for the numbers 0 and 1, respectively.
    ;; Therefore, we could substitute them below and it'd work fine, but this is clearer:
    get_local $p1
    get_local $p2
    i32.add
  )
)

Function Calls

If we add this function to the above module, we can see how we can use our own functions in the same manner. Each call to $add_i32 pops two i32 from the stack and pushes one. You’ll notice the $add_i32 in the declaration of the function in the previous step, which is a labeling scheme just like the function params above. This follows the same numbering conventions as the local variables. You could do (call 0) in the function below, but that’s a bad practice because adding one new function will make all your references wrong.

;; Function to add three numbers.
(func (export "add_i32x3") (param $p1 i32) (param $p2 i32) (param $p3 i32) (result i32)
    ;; Load params onto stack
    get_local $p1
    get_local $p2

    ;; Call add func
    call $add_i32

    ;; Load third param
    get_local $p3

    ;; Call add again.
    call $add_i32
  )

Memory

In order to send more data to and from javascript, we can use “memory”, which is represented as a TypedArray in JavaScript. We can access it in wasm, and also set data in it. To pass data across the boundary between javascript, we can pass offsets and ranges in the shared memory:

(module
  ;; Console.log via js environment
  (import "console" "log" (func $log (param i32 i32)))

  ;; Memory semgent (imported)
  (import "js" "mem" (memory 1))

  ;; Constant data at offset 0:
  (data (i32.const 0) "Hello, world!")
  
  ;; Some function
  (func (export "main")
    ;; "Hello, world!" string has offset 0 and length 13 in shared memory:
    (call $log (i32.const 0) (i32.const 13))
  )
)

Globals

(module
  ;; Global mutable counter
  (global $counter (mut i32) (i32.const 0))

  ;; Increase the counter
  (func (export "inc_counter") (result i32)
    ;; Load + Add
    i32.const 1
    get_global $counter
    i32.add

    ;; Save Result
    set_global $counter

    ;; Return result
    get_global $counter
  )

  ;; Get current counter value
  (func (export "get_counter") (result i32)
    get_global $counter
  )
)

Change Log

  • 1/3/2020 - Initial Revision

Found a typo or technical problem? file an issue!