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 typei32
to$p
- WASM:
local.get $p
push42
to the stack. - WASM: The only data left in the stack is
42
of typei32
- WASM:
(func 0)
finishes, pop the stack as the result42
- Browser: inserts the result
42
into JavaScript template literal
`FROM WASM:
${JSON.stringify({memory}, null, 2)}
return(${input}) = ${exports.r(input)}
`;
- Browser: render via DOM API:
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.