Skip to main content

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.