Functions
Functions are where all WebAssembly code lives. There is no code outside them: no top-level statements, no initialisers that run arbitrary code (apart from one optional start function), no code in data. This chapter covers defining and calling functions, multiple results, and the call stack, which is the first place WebAssembly differs sharply from a native target.
#Signatures and calls5.1
A function has a type: a list of parameter types and a list of result
types. call takes a function index, pops that function's parameters off
the stack, runs it, and pushes its results:
(module
(func $square (param i32) (result i32)
(i32.mul (local.get 0) (local.get 0)))
(func (export "cube") (param $x i32) (result i32)
(i32.mul (call $square (local.get $x)) (local.get $x))))
cube(3) and cube(-4)27 -64
Arguments are pushed in order, first argument first, exactly like the operands of an instruction. A call is, as far as the stack is concerned, just an instruction whose type is the callee's signature.
The return instruction leaves a function early, taking the function's
results from the top of the stack:
(module
(func (export "abs") (param $x i32) (result i32)
(if (i32.ge_s (local.get $x) (i32.const 0))
(then (return (local.get $x))))
(i32.sub (i32.const 0) (local.get $x))))
abs(-5) and abs(5)5 5
(if is the subject of the next chapter.)
#Recursion5.2
Functions can call themselves, and each other, freely:
(module
(func $fac (export "fac") (param $n i64) (result i64)
(if (result i64) (i64.le_u (local.get $n) (i64.const 1))
(then (i64.const 1))
(else
(i64.mul
(local.get $n)
(call $fac (i64.sub (local.get $n) (i64.const 1))))))))
fac(10n) and fac(20n)3628800 2432902008176640000
The function takes an i64, so the host passed the arguments as BigInts,
10n and 20n. Passing a plain JavaScript number where an i64 is
expected is an error, raised before any WebAssembly code runs:
(module
(func $fac (export "fac") (param $n i64) (result i64)
(if (result i64) (i64.le_u (local.get $n) (i64.const 1))
(then (i64.const 1))
(else
(i64.mul
(local.get $n)
(call $fac (i64.sub (local.get $n) (i64.const 1))))))))
fac(10)TypeError: Cannot convert 10 to a BigInt
#Multiple results5.3
A function can return more than one value. This was added in WebAssembly 2.0, and every engine supports it:
(module
(func (export "divmod") (param $a i32) (param $b i32) (result i32 i32)
(i32.div_s (local.get $a) (local.get $b))
(i32.rem_s (local.get $a) (local.get $b))))
divmod(17, 5) and divmod(-17, 5)3 2 -3 -2
The results are left on the stack in order, the first result deepest. A caller in WebAssembly gets them all pushed onto its stack. A caller in JavaScript gets an array, which the book's host prints separated by spaces.
(module
(import "env" "print_i32" (func $print (param i32)))
(func $divmod (param $a i32) (param $b i32) (result i32 i32)
(i32.div_s (local.get $a) (local.get $b))
(i32.rem_s (local.get $a) (local.get $b)))
(func (export "main")
(local $q i32)
(local $r i32)
(call $divmod (i32.const 100) (i32.const 7))
local.set $r ;; the last result is on top
local.set $q
(call $print (local.get $q))
(call $print (local.get $r))))
14 2
#Types and the type section5.4
Every function's signature lives in the module's type section, and the function refers to it by index. The text format has let you write signatures inline so far, but you can also declare a type by name and refer to it:
(module
(type $binop (func (param i32 i32) (result i32)))
(func (export "add") (type $binop)
(i32.add (local.get 0) (local.get 1)))
(func (export "sub") (type $binop)
(i32.sub (local.get 0) (local.get 1))))
add(1, 2) and sub(1, 2)3 -1
When you write a signature inline, the assembler looks for an existing type with that signature and uses it, or adds one if there is none. Named types matter more once function references and indirect calls are involved (Chapter 11), because there you have to name the type of the function you expect to call.
#The function index space5.5
Functions are numbered in one index space, and imported functions come first. A module with two imports and three definitions has functions 0 and 1 imported, and 2, 3 and 4 defined. The same is true of every other kind of thing a module can import: memories, tables, globals and tags each have an index space, with imports first.
(module
(import "env" "print_i32" (func $print_i32 (param i32)))
(import "env" "print_f64" (func $print_f64 (param f64)))
(func $a)
(func $b)
(func $c (export "main")
(call $print_i32 (i32.const 1))))
wasm-objdump -x -j Import indices.wasm | tail -n +6
wasm-objdump -x -j Function indices.wasm | tail -n +6
Import[2]: - func[0] sig=0 <print_i32> <- env.print_i32 - func[1] sig=1 <print_f64> <- env.print_f64 Function[3]: - func[2] sig=2 <a> - func[3] sig=2 <b> - func[4] sig=2 <c>
The names in angle brackets come from the module's name section, an
optional part of the binary where the assembler recorded the $names from
the text, for tools and debuggers to show. Chapter 14 covers it.
This matters to a compiler in one practical way: if you decide to add an import late in compilation, every function index you have already emitted shifts. Most compilers collect their imports first, or number functions symbolically and resolve the numbers when they write the binary.
#The invisible call stack5.6
Here is the most important thing in this chapter. In WebAssembly, you cannot see the call stack.
There is no stack pointer register. There is no instruction to read a
return address, or walk up the frames, or take the address of a local
variable. Locals are not in memory at all, as far as WebAssembly code can
tell; they are just numbered slots that local.get and local.set reach.
The engine keeps return addresses and spilled registers on a stack of its
own, in memory the module cannot read or write.
This is part of WebAssembly's safety. A buffer overflow in linear memory cannot overwrite a return address, because return addresses are not in linear memory. A module cannot jump to an address it computed, because it has no way to name an address in code.
It also means a lot of things a native compiler takes for granted need another approach:
- Address-taken locals. If your language lets you take a pointer to a
local variable (
&xin C or Rust, or passingxby reference), that variable has to live in linear memory, not in a WebAssembly local. Most compilers keep a second stack in linear memory for exactly this, called the shadow stack. Chapter 10 builds one. - Garbage collectors that scan the stack for roots cannot do so. They have to keep roots in a shadow stack too, or use WebAssembly's own garbage collection (Chapter 22).
- Exceptions, coroutines and
setjmpcannot be built by saving and restoring stack pointers. Exceptions have their own instructions (Chapter 20); coroutines need either a transformation of your code or the stack-switching proposal still being standardised. - Stack traces come from the engine, not from walking frames yourself.
#Stack overflow5.7
The engine's stack is finite, and a call that would overflow it is
reported as a JavaScript RangeError, the same error as runaway recursion
in JavaScript:
(module
(func $forever (export "main")
(call $forever)))
RangeError: Maximum call stack size exceeded
How deep you can recurse depends on the engine, its settings, and the size of each frame, which depends on how many locals the engine had to spill. In V8 with default settings it is typically some tens of thousands of frames. A compiler for a language with deep recursion, or a language that promises unbounded recursion, cannot rely on it. Chapter 21 shows how tail calls help with one important case.
#What you have seen5.8
Functions have types made of parameter and result lists, which live in a
type section shared by the whole module. call pops arguments and pushes
results, and a function can return several values. Functions are numbered
in an index space with imports first. The call stack belongs to the engine:
you cannot read it, take addresses in it, or rely on its depth.
That ends Part I. Part II is about control flow inside functions, and about modules as a whole.