Chapter 2

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:

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:

  1. Compile the module, and look at what it imports.
  2. 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
  1. Instantiate the module, then call the exports the example names, or main if it names none, and print whatever each one returns.
  2. 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):

called as 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.