WASI Preview 1 entry point
WAST has no runtime, no allocator, and no standard library. A module that wants to talk to
the outside world imports host functions and calls them, and WASI Preview 1 is just a
particular set of module/field name pairs a host agrees to supply. Nothing in the compiler
knows about WASI; these are ordinary imports that happen to spell wasi_snapshot_preview1.
A clean exit
// The two strings are the WebAssembly import module and field, verbatim.
// There is no namespace sugar and no mangling -- get a character wrong and
// the host reports an unknown import at instantiation, not compile time.
import "wasi_snapshot_preview1"."proc_exit" as func proc_exit(i32);
// Never read or written here. WASI Preview 1 command modules are required to
// export a memory named "memory" anyway; without it `wasmtime run` refuses
// with `missing required memory export`.
memory Heap(1);
// `_start` is an ordinary unit function. The name means something to the
// host convention, nothing to WAST -- what actually makes it the entry
// point is the `start` declaration below.
func _start() {
// In WebAssembly proc_exit never returns, but WAST has no divergent
// return type, so it type-checks as a plain call returning ().
proc_exit(0);
}
export memory Heap as "memory";
// Writes the module start section. At most one per module; the target must
// take no parameters and return (). It runs at instantiation, after globals
// and active data segments are in place and before any export is callable.
start _start;
start and _start are two different mechanisms that happen to coincide here. The start
section is a WebAssembly feature: the engine calls it during instantiation. _start is a
WASI convention: the host looks for an export with that name. This module uses the start
section, so it needs no export at all — wasmtime run instantiates it, instantiation calls
_start, and proc_exit terminates the process before control returns to the host.
Actually writing something
fd_write is the first Preview 1 call that needs real memory layout, and it is a fair
sample of what the language does and does not do for you. There are no string values, no
pointers, and no structs in linear memory — an iovec is bytes you write yourself.
// (fd, iovs_ptr, iovs_len, nwritten_ptr) -> errno
import "wasi_snapshot_preview1"."fd_write"
as func fd_write(i32, i32, i32, i32) -> i32;
import "wasi_snapshot_preview1"."proc_exit" as func proc_exit(i32);
memory Heap(1);
// Active segment: bytes are copied in at instantiation. `at 8` requires the
// module to have exactly ONE memory, because the syntax has nowhere to name
// one. String literals are decoded to UTF-8, and \n is one byte.
//
// The passive form, `data Message = "...";`, is currently a dead end: it is
// consumed by memory.init, which is not in the wastc-core-gc-0 catalog.
data Message at 8 = "hello from wast\n";
func _start() {
// A ciovec is two i32s: pointer, then length. Nothing in the language
// knows that -- these two stores ARE the struct definition, and the
// 8 and 16 have to match the data segment by hand.
wasm.i32.store<Heap, offset=0, align=4>(0, 8);
wasm.i32.store<Heap, offset=4, align=4>(0, 16);
// fd 1 (stdout), one iovec at byte 0, bytes-written result at byte 24 --
// just past the message, which occupies 8..24. Nothing checks that these
// regions do not overlap.
let errno: i32 = fd_write(1, 0, 1, 24);
// errno is a value, not a trap. Ignoring it is a silent failure.
proc_exit(errno);
}
export memory Heap as "memory";
start _start;
Manual layout is the cost of having no runtime. There is no allocator to ask for those 28
bytes, so the addresses are constants chosen by reading the program, and moving the data
segment means editing the store in _start to match. That is workable for a handful of
buffers and does not scale; a real toolchain would generate the offsets. WAST is not
trying to be that toolchain.
Expected result
wastc build wasi-write.wast -o wasi-write.wat
wasm-tools parse wasi-write.wat -o wasi-write.wasm
wasmtime run wasi-write.wasm
The first module exits 0 and prints nothing. The second prints hello from wast and
exits 0. Neither exports a check() — the start function ends the process, so there is
nothing left for a host to invoke.