WAT: WASM by Hand

Let's start to learn WASM ground up by hand from

Empty module

chapter_wat/wat/empty_module.wat

(module)
wat2wasm wat/empty_module.wat -o wasm/empty_module.wasm

chapter_wat/js/empty_module.js

(async () => {
    var importObject = {};
    const response = await fetch('chapter_wat/wasm/empty_module.wasm');
    const bytes = await response.arrayBuffer();
    const { instance } = await WebAssembly.instantiate(bytes, importObject);
    const { exports } = instance;
    console.log('Empty WASM Module', {response, bytes, instance, exports});
    var u8 = new Uint8Array(bytes);
    var u16 = new Uint16Array(bytes);
    var u32 = new Uint32Array(bytes);
    const byteLength = bytes.byteLength;
    window.memory = {bytes, byteLength, u8, u16, u32};
    console.log({memory});
    document.getElementById('empty_module').innerHTML = `FROM WASM:
${JSON.stringify({memory}, null, 2)}
`;
})();
<pre id="empty_module"></pre>
<script src="chapter_wat/js/empty_module.js"></script>


ArrayBuffer

So what does the output above mean? To understand it, we need to dive into ArrayBuffer.

An empty WASM module is nothing but 8 bytes.

$ hexdump -C wasm/empty_module.wasm   
00000000  00 61 73 6d 01 00 00 00                           |.asm....|
00000008

See hexdump.

The first 4 bytes represent WASM_BINARY_MAGIC

00 61 73 6d ('\0asm')

The next 4 bytes represent WASM_BINARY_VERSION

01 00 00 00

See binary module specification.

A simple function that returns the input

chapter_wat/wat/return_module.wat

(module
  (func $return (param $input i32) (result i32)
    local.get $input )
  (export "return" (func $return))
)
$ wat2wasm wat/return_module.wat -o wasm/return_module.wasm
$ hexdump -C wasm/return_module.wasm
00000000  00 61 73 6d 01 00 00 00  01 06 01 60 01 7f 01 7f  |.asm.......`....|
00000010  03 02 01 00 07 0a 01 06  72 65 74 75 72 6e 00 00  |........return..|
00000020  0a 06 01 04 00 20 00 0b                           |..... ..|
00000028

chapter_wat/js/return_module.js

(async () => {
    var importObject = {};
    const response = await fetch('chapter_wat/wasm/return_module.wasm');
    const bytes = await response.arrayBuffer();
    const { instance } = await WebAssembly.instantiate(bytes, importObject);
    const { exports } = instance;
    console.log('WASM Module', {response, bytes, instance, exports});
    var u8 = new Uint8Array(bytes);
    var u16 = new Uint16Array(bytes);
    var u32 = new Uint32Array(bytes);
    const byteLength = bytes.byteLength;
    const input = 42;
    window.memory = {bytes, byteLength, u8, u16, u32};
    console.log({memory});
    document.getElementById('return_module').innerHTML = `FROM WASM:
${JSON.stringify({memory}, null, 2)}
return(${input}) = ${exports.return(input)}
`;
})();
<pre id="return_module"></pre>
<script src="chapter_wat/js/return_module.js"></script>


You can see this module has 40 (0x28) bytes.

Let's shave a few more bytes off.

chapter_wat/wat/r_module.wat

(module
  (func $return (param $input i32) (result i32)
    local.get $input )
  (export "r" (func $return))
)
$ wat2wasm wat/r_module.wat -o wasm/r_module.wasm
$ hexdump -C wasm/r_module.wasm
00000000  00 61 73 6d 01 00 00 00  01 06 01 60 01 7f 01 7f  |.asm.......`....|
00000010  03 02 01 00 07 05 01 01  72 00 00 0a 06 01 04 00  |........r.......|
00000020  20 00 0b                                          | ..|
00000023

You can see this module has 35 (0x23) bytes. We shaved 5 bytes eturn from the wasm.

Since we only have one function in the WAT, we can simplify the WAT source further:

(module
  (func (param $p i32) (result i32)
    local.get $p )
  (export "r" (func 0))
)

Which produces exactly the same WASM.

$ diff wat/r_module.wat wat/r_func_0_module.wat
2,4c2,4
<   (func $return (param $input i32) (result i32)
<     local.get $input )
<   (export "r" (func $return))
---
>   (func (param $p i32) (result i32)
>     local.get $p )
>   (export "r" (func 0))
$ diff wasm/r_module.wasm wasm/r_func_0_module.wasm

All things work together

chapter_wat/js/r_module.js

(async () => {
    var importObject = {};
    const response = await fetch('chapter_wat/wasm/r_module.wasm');
    const bytes = await response.arrayBuffer();
    const { instance } = await WebAssembly.instantiate(bytes, importObject);
    const { exports } = instance;
    console.log('WASM Module', {response, bytes, instance, exports});
    var u8 = new Uint8Array(bytes);
    const byteLength = bytes.byteLength;
    const input = 42;
    window.memory = {bytes, byteLength, u8};
    console.log({memory});
    document.getElementById('r_module').innerHTML = `FROM WASM:
${JSON.stringify({memory}, null, 2)}
return(${input}) = ${exports.r(input)}
`;
})();
<pre id="r_module"></pre>
<script src="chapter_wat/js/r_module.js"></script>
  • Browser: exports context from WebAssembly instance
  • Browser: const input = 42
  • Browser: calls exports.r(input)
  • WASM: invoke func 0, which is the only function in the module
  • WASM: pass 42 of type i32 to $p
  • WASM: local.get $p push 42 to the stack.
  • WASM: The only data left in the stack is 42 of type i32
  • WASM: (func 0) finishes, pop the stack as the result 42
  • Browser: inserts the result 42 into JavaScript template literal
`FROM WASM:
${JSON.stringify({memory}, null, 2)}
return(${input}) = ${exports.r(input)}
`;
document.getElementById('r_module').innerHTML = `...`


Call JS function from WASM

See Importing functions from JavaScript

chapter_wat/wat/call_js_func_module.wat

(module
  (import "dom" "log" (func (param i32 i32 )))
  (func (param $input i32)
    local.get $input ;; pass input to log
    local.get $input ;; pass output to log
    call 0)
  (export "log" (func 1))
)
$ wat2wasm wat/call_js_func_module.wat -o wasm/call_js_func_module.wasm
$ hexdump -C wasm/call_js_func_module.wasm
00000000  00 61 73 6d 01 00 00 00  01 0a 02 60 02 7f 7f 00  |.asm.......`....|
00000010  60 01 7f 00 02 0b 01 03  64 6f 6d 03 6c 6f 67 00  |`.......dom.log.|
00000020  00 03 02 01 01 07 07 01  03 6c 6f 67 00 01 0a 0a  |.........log....|
00000030  01 08 00 20 00 20 00 10  00 0b                    |... . ....|
0000003a
<pre id="call_js_func_module_output"></pre>
<script src="chapter_wat/js/call_js_func_module.js"></script>

chapter_wat/js/call_js_func_module.js

(async () => {
    var importObject = {
        dom : {
            log: function (input, output) {
                console.log('Call from WASM', {input, output});
                document.getElementById(`call_js_func_module_output`).innerHTML += `
input = ${input}
output = ${output}`
            }
        }
    };
    const response = await fetch('chapter_wat/wasm/call_js_func_module.wasm');
    const bytes = await response.arrayBuffer();
    const { instance } = await WebAssembly.instantiate(bytes, importObject);
    const { exports } = instance;
    console.log('WASM Module', {response, bytes, instance, exports});
    var u8 = new Uint8Array(bytes);
    const byteLength = bytes.byteLength;
    const input = 42;
    window.memory = {bytes, byteLength, u8};
    console.log({memory});
    console.log(input);
    exports.log(input);
    exports.log(64); 
})();


Finally the add module

chapter_wat/wat/add_module.wat

(module
  (import "dom" "show_add" (func (param i32 i32 i32))) ;; add(x, y, result)
  (func (param i32 i32) (result i32) ;; add(x, y)
    local.get 0
    local.get 1
    i32.add)
  (func (param i32 i32) 
    local.get 0 ;; for the first param in dom.add
    local.get 1 ;; for the second param in dom.add
    local.get 0
    local.get 1
    call 1 ;; for the third param in dom.add
    call 0 ;;
  )
  (export "add" (func 2))
)
wat2wasm wat/add_module.wat -o wasm/add_module.wasm
hexdump -C wasm/add_module.wasm
00000000  00 61 73 6d 01 00 00 00  01 12 03 60 03 7f 7f 7f  |.asm.......`....|
00000010  00 60 02 7f 7f 01 7f 60  02 7f 7f 00 02 10 01 03  |.`.....`........|
00000020  64 6f 6d 08 73 68 6f 77  5f 61 64 64 00 00 03 03  |dom.show_add....|
00000030  02 01 02 07 07 01 03 61  64 64 00 02 0a 18 02 07  |.......add......|
00000040  00 20 00 20 01 6a 0b 0e  00 20 00 20 01 20 00 20  |. . .j... . . . |
00000050  01 10 01 10 00 0b                                 |......|
00000056
<pre id="add_module_output"></pre>
<script src="chapter_wat/js/add_module.js"></script>

chapter_wat/js/add_module.js

(async () => {
    var importObject = {
        dom : {
            show_add: function (x, y, result) {
                console.log('Call from WASM', {x, y, result});
                document.getElementById(`add_module_output`).innerHTML += `
${x} + ${y} = ${result}`;
            }
        }
    };
    const response = await fetch('chapter_wat/wasm/add_module.wasm');
    const bytes = await response.arrayBuffer();
    const { instance } = await WebAssembly.instantiate(bytes, importObject);
    const { exports } = instance;
    console.log('WASM Module', {response, bytes, instance, exports});
    exports.add(1, 1); 
    exports.add(3, 4);
    const i32max = (0xffffffff - 1)/2;
    exports.add(i32max, 1); 
})();


Import add from another module

In the previous add module, the exported add(x, y) calls internal add(x, y) and imported dom.show_add(x, y, result). Let's see if we can import the add(x, y) from another module.

chapter_wat/wat/add_only_module.wat

(module
  (func (param i32 i32) (result i32) ;; add(x, y)
    local.get 0
    local.get 1
    i32.add)
  (export "add" (func 0))
)
$ wat2wasm wat/add_only_module.wat -o wasm/add_only_module.wasm
$ hexdump -C wasm/add_only_module.wasm
00000000  00 61 73 6d 01 00 00 00  01 07 01 60 02 7f 7f 01  |.asm.......`....|
00000010  7f 03 02 01 00 07 07 01  03 61 64 64 00 00 0a 09  |.........add....|
00000020  01 07 00 20 00 20 01 6a  0b                       |... . .j.|
00000029

chapter_wat/wat/import_add_module.wat

(module
  (import "dom" "show_add" (func (param i32 i32 i32))) ;; add(x, y, result)
  (import "external" "add" (func (param i32 i32) (result i32))) ;; add(x, y)
  (func (param i32 i32) 
    local.get 0 ;; for the first param in dom.add
    local.get 1 ;; for the second param in dom.add
    local.get 0
    local.get 1
    call 1 ;; for the third param in dom.add
    call 0 ;;
  )
  (export "add" (func 2))
)
$ wat2wasm wat/import_add_module.wat -o wasm/import_add_module.wasm
$ hexdump -C wasm/import_add_module.wasm
00000000  00 61 73 6d 01 00 00 00  01 12 03 60 03 7f 7f 7f  |.asm.......`....|
00000010  00 60 02 7f 7f 01 7f 60  02 7f 7f 00 02 1f 02 03  |.`.....`........|
00000020  64 6f 6d 08 73 68 6f 77  5f 61 64 64 00 00 08 65  |dom.show_add...e|
00000030  78 74 65 72 6e 61 6c 03  61 64 64 00 01 03 02 01  |xternal.add.....|
00000040  02 07 07 01 03 61 64 64  00 02 0a 10 01 0e 00 20  |.....add....... |
00000050  00 20 01 20 00 20 01 10  01 10 00 0b              |. . . ......|
0000005c
<pre id="import_add_module_output"></pre>
<script src="chapter_wat/js/import_add_module.js"></script>

chapter_wat/js/import_add_module.js

(async () => {
    // scope calling add_only_module version of add(x, y)
    const importObject = {};
    const response = await fetch('chapter_wat/wasm/add_only_module.wasm');
    const bytes = await response.arrayBuffer();
    const { instance } = await WebAssembly.instantiate(bytes, importObject);
    const { exports } = instance;
    const { add } = exports;
    const i32max = (0xffffffff - 1) / 2;
    {   // scope calling import_add_module version of add(x, y)
        const importObject = {
            dom: {
                show_add: function (x, y, result) {
                    console.log('Call from WASM', { x, y, result });
                    document.getElementById(`import_add_module_output`).innerHTML += `${x} + ${y} = ${result}\n`;
                }
            },
            external: { add }
        };
        const response = await fetch('chapter_wat/wasm/import_add_module.wasm');
        const bytes = await response.arrayBuffer();
        const { instance } = await WebAssembly.instantiate(bytes, importObject);
        const { exports } = instance;
        console.log('WASM Module', { response, bytes, instance, exports });
        exports.add(1, 1);
        exports.add(3, 4);
        exports.add(i32max, 1);
    }
    console.log('calling add_only_module', exports.add(1, 1))
    console.log('calling add_only_module', exports.add(3, 4))
    console.log('calling add_only_module', exports.add(i32max, 1))

})();


Now we successfully import the add(x, y) function from a WASM module add_only_module.wasm into another WASM module import_add_module.wasm and call the exported add(x, y) from both modules in different scopes.