Hello, WebAssembly
This chapter writes a module, turns it into a binary, and runs it, first with the book's host and then with a dozen lines of your own JavaScript. Along the way it introduces the tools you will use for the rest of the book.
#The smallest module2.1
Here is a complete, valid WebAssembly module in the text format:
(module)
It does nothing. It has no functions, no memory, no imports and no exports. But it is valid, and it can be instantiated. Turned into a binary, it is eight bytes long:
(module)
wat2wasm empty.wat -o empty.wasm
wc -c empty.wasm
od -An -tx1 empty.wasm
8 empty.wasm 00 61 73 6d 01 00 00 00
The first four bytes are a magic number, a zero byte followed by the
letters asm. The next four are the version of the binary format, 1, as a
little-endian 32-bit integer. Every module starts with these eight bytes,
and everything else is optional. Part IV takes the rest of the format
apart.
The text format is made of S-expressions: parenthesised lists whose first
word says what the list is. (module ...) holds everything else, and
inside it you will write (func ...), (memory ...), (import ...) and
so on. Comments start with ;; and run to the end of the line, or are
written (; like this ;) and can span lines.
#A function that returns a number2.2
A module becomes useful when it exports something. This one exports a function that returns 42:
(module
(func (export "main") (result i32)
i32.const 42))
42
Line by line:
(func ...)defines a function. Functions have no names in the binary format. They are numbered, starting from 0, in the order they appear.(export "main")exports this function under the namemain, so the host can find it. The export name is a string, and it can be anything.(result i32)says the function returns one 32-bit integer.i32.const 42is the whole body: a single instruction that pushes the constant 42 onto the operand stack. When a function's body ends, the values left on the stack are its results.
The book's host called main because it was exported, and printed what it
returned. That is all the host does with a function that returns
something: print the results, separated by spaces.
#Turning text into a binary2.3
Browsers run the binary format, not the text format. The text format is
for people: for reading what a compiler produced, for writing tests, and
for books. The tool that turns one into the other is wat2wasm, from the
WebAssembly Binary Toolkit, wabt (say "wabbit"):
(module
(func (export "main") (result i32)
i32.const 42))
wat2wasm answer.wat -o answer.wasm
wc -c answer.wasm
37 answer.wasm
Thirty-seven bytes. wasm2wat goes the other way, which is what you will
reach for when you want to read what your compiler wrote:
wat2wasm answer.wat -o answer.wasm
wasm2wat answer.wasm
(module
(type (;0;) (func (result i32)))
(func (;0;) (type 0) (result i32)
i32.const 42)
(export "main" (func 0)))
The round trip added two things you did not write. One is the comments
(;0;), which give the index of each definition. The other is a whole
(type ...) definition. In the binary format, a function does not carry
its signature. It refers by index to a signature in the module's type
section, and many functions can share one. The text format lets you write
signatures inline and creates the type entries for you.
The third tool you will use constantly is wasm-objdump, which prints the
structure of a binary:
wasm-objdump -x answer.wasm
answer.wasm: file format wasm 0x1 Section Details: Type[1]: - type[0] () -> i32 Function[1]: - func[0] sig=0 <main> Export[1]: - func[0] <main> -> "main" Code[1]: - func[0] size=4 <main>
A binary module is a sequence of sections, each holding one kind of thing. This module has four: the types, the list of which type each function has, the exports, and the code. Separating a function's signature (in the Function section) from its body (in the Code section) looks fussy, but it lets an engine know every function's type before it reads any code, so it can start compiling bodies in parallel, or while the rest of the file is still downloading.
wasm-objdump -d disassembles the code, showing each instruction's bytes
beside it:
wasm-objdump -d answer.wasm
answer.wasm: file format wasm 0x1 Code Disassembly: 000021 func[0] <main>: 000022: 41 2a | i32.const 42 000024: 0b | end
i32.const is the byte 0x41, and 42 is 0x2a. The function ends with
0x0b, the end instruction, which the text format writes for you.
#Running a module from JavaScript2.4
Every browser and Node has a global WebAssembly object. The shortest way
to run a module is WebAssembly.instantiate, which takes the bytes and an
object of imports, and resolves to the module and an instance of it:
import fs from 'node:fs';
const bytes = fs.readFileSync('answer.wasm');
const { instance } = await WebAssembly.instantiate(bytes, {});
console.log(instance.exports.main());
node run.mjs
42
An exported function is an ordinary JavaScript function. Call it, and the engine runs the WebAssembly code and converts the result to a JavaScript number.
In a browser, you would fetch the file instead of reading it, and use the streaming form, which compiles while the bytes arrive:
const { instance } = await WebAssembly.instantiateStreaming(
fetch('answer.wasm'), {});
console.log(instance.exports.main());
The server must send the file with the MIME type application/wasm for
the streaming form to accept it. Chapter 15 covers the whole JavaScript
API.
#Saying hello2.5
A module cannot print. It has no I/O of any kind. It can only call
functions that its host gives it, so printing means importing a function
from the host. The book's host offers a few, in a module it calls env:
(module
(import "env" "print_i32" (func $print_i32 (param i32)))
(func (export "main")
i32.const 7
call $print_i32
i32.const -7
call $print_i32))
7 -7
An import names a module and a field, here env and print_i32, and
says what kind of thing it expects: a function taking one i32. The
names are just strings that the host uses to look up what to provide. env
is a common convention but nothing more.
$print_i32 is a symbolic name, written with a $. It exists only in
the text format, as a convenience. The assembler replaces each one with an
index. Imported functions get the first indices, so $print_i32 is
function 0 and main is function 1.
main returns nothing this time, so the host prints nothing after it.
To print text, the module needs somewhere to keep the characters: a linear memory, which is an array of bytes the module can read and write. A data segment puts the bytes of a string into the memory when the module is instantiated:
(module
(import "env" "print" (func $print (param i32 i32)))
(memory (export "memory") 1)
(data (i32.const 16) "Hello, WebAssembly!")
(func (export "main")
i32.const 16 ;; where the string starts
i32.const 19 ;; how many bytes it is
call $print))
Hello, WebAssembly!
(memory 1) declares a memory one page long. A page is 64 KiB. The
data segment copies 19 bytes to address 16. The print import takes an
address and a length, and the host reads the bytes out of the memory the
module exported as memory, decodes them as UTF-8, and prints them.
This is how all text crosses between WebAssembly and JavaScript: as bytes in memory, with an address and a length. WebAssembly has no string type. Chapter 16 covers strings in depth.
#The book's host2.6
Every example marked with output in this book is run by the same small
program, scripts/run.mjs, in Node. Here is everything it does:
- Compile the module, and look at what it imports.
- Provide each import it recognises, all from module
env:
| Import | Signature | Does |
|---|---|---|
print_i32 |
(param i32) |
prints a signed 32-bit integer and a newline |
print_u32 |
(param i32) |
the same bits, printed as unsigned |
print_i64 |
(param i64) |
prints a signed 64-bit integer |
print_f32 |
(param f32) |
prints a 32-bit float |
print_f64 |
(param f64) |
prints a 64-bit float |
print |
(param i32 i32) |
prints UTF-8 text from memory, given its address and length, and a newline |
write |
(param i32 i32) |
the same, without the newline |
memory |
(memory 1) |
a memory, for modules that import one rather than defining their own |
- Instantiate the module, then call the exports the example names, or
mainif it names none, and print whatever each one returns. - If anything traps, print the error on stderr and stop.
Some examples call an export with arguments. When they do, the book says which, like this:
(module
(func (export "add") (param i32 i32) (result i32)
local.get 0
local.get 1
i32.add))
The host called add(2, 3) and then add(-1, 1):
add(2, 3) and add(-1, 1)5 0
#What you have seen2.7
A module is a list of definitions: functions, memories, data, imports and exports. The text format writes them as S-expressions; the binary format, which is what engines run, encodes them as sections of bytes. Functions compute by pushing and popping values on an operand stack, and anything that touches the outside world is an import.
The next chapter looks properly at that operand stack.