Skip to Content
i documentation
The language standardChapters

Appendix B. The binary interface

B.1 The module

A compiled program is one Wasm module. It needs the Wasm features garbage collection, typed function references, tail calls, and exception handling with exnref. Exceptions are used only inside the module: its tags are not exported, every export catches them, and an import must not throw one into the module. It exports:

  • memory: the linear memory used to pass buffers.
  • ilang_alloc(size: i32) -> i32: gives the address of a new buffer of size bytes in memory. For size 0 it may give 0.
  • ilang_free(address: i32, size: i32): frees a buffer made by ilang_alloc.
  • ilang_start(): starts the program by working out the top-level values (§2.5). It has no parameters and no results. The host calls it one time, after it makes the instance and before it calls any other export but ilang_alloc and ilang_free. During ilang_start, imports may be called as at any other time.

Checked Wasi buffers are separate table-owned allocations (§B.3, §B.7.1); they do not change these host allocation exports.

The module has no Wasm start function, so that imports called while top-level values are worked out may call ilang_alloc. The module has one custom section named ilang.abi with the metadata of §B.6.

This version runs one instance for each run of the program. Workers (§11.9) come with a later version of this interface.

B.2 Values

A value of a type that the host can take (§10.3) is passed as zero or more Wasm values:

TypeWasm values
Nilnone
Booli32: 0 is False, 1 is True
Floatf64
Inti32 sign, i32 address or inline value, i32 count. The sign is −1, 0, or 1. When count is 0 and sign is not 0, the second i32 is the signed 32-bit value, its sign must match, and there is no buffer. Otherwise the size of the number is count unsigned 32-bit parts at the address, lowest part first, each part little-endian. The last part is not 0. Zero is sign 0 and count 0. Values outside the signed 32-bit range use parts; receivers also accept parts for nonzero values inside that range.
Stringi32 address, i32 length: the UTF-8 bytes
BitArrayi32 address, i32 number of bits. There are ⌈bits / 8⌉ bytes. In each byte the highest bit comes first. Bits after the end in the last byte are 0.
An opaque typeexternref

An inline Int has no buffer and its second i32 is never checked as an address or freed. Senders use the inline form for nonzero signed 32-bit values and (0, 0, 0) for zero. In versions 3 and 4 there is no inline form: every nonzero Int uses parts. The size of the buffer of a non-inline Int is 4 × count bytes, of a String its length, and of a BitArray ⌈bits / 8⌉ bytes.

A function (P1, …, Pn) <- (Fail(E)) -> A, or (P1, …, Pn) -> A with E = Never, is passed like this. Services are not passed.

  • Parameters: the values of P1, then P2, and so on.
  • Results, when E is Never: the values of A.
  • Results, when E has members: first an i32 tag, then the values of A, then the values of each member of E in the canonical order: Nil, Bool, Int, Float, String, BitArray. Tag 0 means a normal value; tag i means an error of the i-th member. The values that are not used are 0, or null for an externref.

When the host gives a value that is not correct (a String that is not UTF-8, an Int with a wrong sign or a last part of 0, a BitArray whose bits after the end are not 0, a Float that is not finite, a Bool that is not 0 or 1, a buffer that is not inside memory, also an empty one, or a tag that is too big), the program stops (§11.7).

B.3 Buffers

  • A buffer given as a parameter belongs to the caller. It may only be used during the call. The callee makes a copy if it needs one.
  • A buffer given as a result is made with ilang_alloc and belongs to the receiver, which frees it with ilang_free.

Checked Wasi buffers (§B.7.1) are separate table-owned allocations and are not addresses supplied to ilang_free by i code. The allocation exports remain host machinery; checked tokens cannot name ABI buffers.

So for an export, the host makes the parameter buffers with ilang_alloc, frees them after the call, and frees the result buffers. For an import, the module makes and frees the parameter buffers, and the host makes the result buffers by calling ilang_alloc before it returns. During an import, the host may only call ilang_alloc and ilang_free.

B.4 Imports

  • Retained JavaScript value getters (§10.1.1): zero-argument target imports returning externref, described in §B.6.1. They are not new ilang:host/v1 runtime imports.
  • External functions (§10.1): the import module and name of the @external attribute, encoded as in §B.2.
  • Functions of host services (§9.7): the import module is the namespace and the name is the field label, encoded as in §B.2.
  • Async functions of host services: the import has an i32 request id first, then the parameters of §B.2, and no results. The host copies the parameters, keeps the request, and returns at once. The answer comes later through ilang_resume (§B.5). Until then the request is outstanding.
    • The host calls the service during the import, and waits for its work only after the import has returned. So a service can fix anything that depends on the time of the call, such as the deadline of a sleep, during the import. The parameters are copied then too.
    • The module chooses the id: a number of 0 or more that no other outstanding request of the instance has. After a request is answered, cancelled or dropped (§B.5), its id may be used again.
    • The module calls an async function only during a call of an async entry point, of ilang_resume:… or of ilang_continue, and not while an import runs (also not from a call such as ilang_alloc that the host makes during an import). So it does not call one during ilang_start (§B.1) either. If it calls one at another time, or with an id that is below 0 or already outstanding, the import traps and the host does not use the instance again.
    • On the WASI target none of this holds: there, as §B.7 says, an async function is a blocking import, with no request id.
  • Runtime imports, with the import module ilang:host/v1:
    • panic(address: i32, length: i32) gives the stop message (§11.7) as UTF-8. Then the module traps.
    • reference_id(reference: externref) -> i64 gives a number for a host reference: the same number for the same reference, different numbers for different references, and 0 for null. It is used by == (§11.4).
    • cancel(id: i32) drops the outstanding request id (above): the host stops its work if it can and never answers it, and the id may be used again. It may be called at the same times as an async function. With an id that is not outstanding, it traps and the host does not use the instance again.

B.5 Exports

  • Host exports (§10.2) have the name written in [wasm].exports, for example app/math.add, and are encoded as in §B.2.

  • The program entry and tests have the name module path.function name, no parameters, and no results.

  • A function that is both a host export and the program entry or a test, or both the program entry and a test, or a function named two times in [wasm].exports, is a compile error, because their export names would be the same.

  • Async entry points (§10.2) have the parameters above and one result, an i32 status. 0 means the entry point has ended. 1 means it is waiting: it has one or more outstanding requests (§B.4), and it goes on only when the host answers one. 2 means it is runnable: it can go on without an answer. Another status is a fault of the module.

  • When the status is 2, the host lets its own work run once without waiting, then answers a request whose work is done, if there is one. If there is none, it calls the export ilang_continue() -> i32, with no parameters, whose result is a status again. The host calls ilang_continue only then, right after status 2; a call at another time breaks the instance. A module that gives status 2 has this export; if it does not, it is a fault of the module. The names ilang_continue, ilang_start, ilang_alloc, ilang_free, memory and those that start with ilang_resume: or ilang_result: are reserved: the metadata may not use them for host exports or entry points.

  • The host answers a request by calling the export ilang_resume:namespace:label of the function that started it. The first parameter is the request id, and the others are the results of the function as in §B.2. The result is a status again. The host may answer outstanding requests in any order, but one at a time. It makes the result buffers with ilang_alloc just before the call that gives them. A module that starts a request of a function and has no such export for it, or one of the wrong type, has a fault, and the host does not use the instance again.

  • A call of ilang_resume:… with an id that is not outstanding, or that another function started, traps.

  • When the status is 1 and no request is outstanding, the program can never go on. This is a fault of the module, and the host does not use the instance again.

  • When the status is 0, the host calls the export ilang_result:name of the entry point, with no parameters, to get its results as in §B.2. Requests that are still outstanding are dropped: the host stops their work if it can and never answers them. This lets a program end work that it no longer needs; a module made by this compiler has no outstanding request at that point.

  • When the work of an async function fails in the host (not an error raised through its Fail, which is an answer), the host drops every outstanding request and does not use the instance again.

  • While an async entry point is waiting or runnable, the host may only call ilang_resume:…, ilang_continue, ilang_alloc, and ilang_free. So an instance runs one async entry point at a time. The module does not check every export for this; a host that calls another export breaks the instance.

  • When the program stops, the export traps after the panic import (§B.4). After a trap, the host may not use that instance of the module again. Every outstanding request is dropped, and an answer that comes later is ignored. The host makes a new instance and calls ilang_start on it.

B.6 Metadata

The custom section ilang.abi is UTF-8 JSON in this form:

{
  "version": 5,
  "entry": { "name": "app/main.main", "async": false },
  "tests": [ { "name": "app/math_test.add", "async": false } ],
  "exports": {
    "app/math.add": { "params": ["int", "int"], "result": "int", "errors": [], "async": false }
  },
  "services": {
    "app:storage/v1": {
      "read": { "params": ["string"], "result": "string", "errors": ["string"], "async": true }
    }
  },
  "externals": {
    "app:math/v1": { "sqrt": { "params": ["float"], "result": "float" } }
  }
}
  • version is 5. Hosts read versions 3, 4 and 5 and use the value encoding of the module’s version. A host refuses a newer module with the module uses i ABI version V, but this host reads version H; update the host; a module older than version 3 must be built again. Version 5 adds the inline signed 32-bit Int form (§B.2). Version 4 added the native multicore worker imports (mc_* in ilang:host/v1). Version 3 differs from version 2 in these ways: an async function has a request id; status 1 means one or more outstanding requests, not exactly one, and the host may answer them in any order; status 2 and the export ilang_continue are new, and ilang_continue is a reserved export name; the runtime import cancel is new; outstanding requests are dropped when the entry point ends, on a trap, and when the work fails in the host; and an async function may only be called during an async entry point, a resume or ilang_continue (§B.4). A runtime import, status or export that a host must handle is added only with a new version.

  • Type names are nil, bool, int, float, string, bit_array, and externref. errors is in the canonical order (§B.2).

  • entry names the selected function by its module path and function name, not its [entries] key. It is null when there is no selected entry. Only that entry is emitted as the program entry; unselected declarations do not add ABI exports. The JavaScript SDK uses this metadata entry. The ABI version and shape stay unchanged. tests is empty unless tests were compiled.

  • services lists every required host-registration function of the host services that the platform provides to the entries, and externals every required external function. A host service that i code provides is not a required host import. Entry async flags and status/result exports follow §10.2, before linking and optimization.

  • The host must give every import in services, externals, and ilang:host/v1 before it makes an instance of the module. If not, making the instance fails.

  • A module of the WASI target (§B.7) also has "target": "wasi". JS targets have "target": "js" or "target": "nodejs" (§B.6.1). Host modules have no target. A host of this appendix does not run a module with a target.

B.6.1 JavaScript target artifacts

name.wasm, name.mjs and name.d.ts are the generated JS artifact. The mjs contains the fixed bridge, retained source expressions and loader; each instance has its own declaration caches and entry resource contexts. It starts the module once, then calls main or the requested host export. JS source may use import("node:...") inside a function and await its opaque promise through the request protocol. There are no static import declarations inside an expression, implicit Node module variables, or compiler-generated user callbacks. Standard Node factories may use their own ESM imports. Their source is fixed support code, not user handwritten JS.

Use the module’s ABI version for ordinary service/request calls. Put foreign declaration text, stable package/module/name identity, generated import name, nominal type identity, source span and availability in a separate versioned target section ilang.js (initial version 1). A matching generated loader requires that section and target : "js" or "nodejs"; old Appendix B hosts reject the non-host target instead of accidentally loading it. This target metadata does not add a v3 runtime export. Mandatory changes to runtime imports, statuses or host-called exports still require a version bump (§B.6). The value getters are target imports listed there, not additions to the ABI’s ilang:host/v1. A digest binds the JS manifest to its Wasm artifact; it is not self-authenticating permission for raw IO (trusted declaration grants still apply).

bindings/javascript remains the Host SDK. Generated-target and explicit SDK usage share encoding and request lifecycle code. Host exports always use the real SDK conversions and d.ts representations, never a dynamically replaced Js service; a custom outgoing converter cannot change an export’s ABI.

B.7 The WASI target

i build --target wasi makes the module a WASI preview 1 command, which a WASI host such as wasmtime run or Node’s node:wasi runs with no other step. It is the module of §B.1 to §B.6 with these changes:

  • It imports fd_write and proc_exit from wasi_snapshot_preview1, and not panic from ilang:host/v1. The stop message (§11.7) and a line feed are written to file descriptor 2, then the process ends with exit code 1. reference_id is imported from ilang:host/v1 only when a type of an import or export is a custom type with no constructors, because only the host can make such a value.

  • The platform’s Io writes UTF-8 text plus LF to file descriptor 1 through the checked Wasi adapter (§B.7.1). Positive short writes are retried; zero progress or errors give up that write; print gives Nil. It attempts the text first, then LF even if writing the text gave up, and frees buffers on every returning path. It is absent from required-import metadata because it is internally provided. Another host service with the namespace i:io/v1 is an import like any other.

  • Because the runtime imports fd_write and proc_exit, an import (§B.4) with the import module wasi_snapshot_preview1 and one of these names is a compile error. Such an import is an external function, or a required host-registration field in the entry provider graph (§9.7). Other functions of wasi_snapshot_preview1 may be imported.

  • Every other required host-registration function and external function is an import as in §B.4, so a host that has them can give them. A module that the host makes before the program, such as one given with wasmtime run --preload, cannot use the program’s memory or ilang_alloc, so it can only give functions whose parameters, values, and errors are Nil, Bool, or Float. An import with an Int, String, or BitArray needs a host that defines it with the exports of the instance, such as an embedder, or a module linked in statically. An import with a host reference needs such a host too, because only the host makes host references, and the module then imports reference_id.

  • A function of an async host service is a blocking import, with the results of a function that is not async (§B.2). Nothing is async, and there are no ilang_resume:… and ilang_result:… exports.

  • It exports _start, which calls ilang_start and then the program entry. The program must have a selected entry; if not, it is a compile error. When _start returns, the exit code is 0.

  • Tests are not compiled: a test needs a new instance (§10.2), and a command runs one entry.

When the program runs out of memory or stack (§11.7), there is no stop message: the host gives its own error. The host also decides how deep the stack may be, so ordinary recursion that is not in tail position may stop earlier than in another host.

The WASI platform also provides Stdio, Clock, Entropy, Arguments, Environment and Fs. A need of Net, Tls, Process or WorkingDirectory that the program does not provide is a compile error naming the service, target and entry. Custom imports retain the blocking ABI above. Standard capability adapters operate on checked i values: they copy strings/whole-byte bit arrays, validate integer ranges, construct pointer-bearing layouts privately, check host result extents, and release scratch allocations on returning paths. No guest-supplied bytes are interpreted as pointers. Request operations return completed requests.

CapabilityRaw preview-1 imports and checked layouts
Stdiofd_read/fd_write: one private 8-byte iovec and 4-byte count; count must not exceed the requested extent; writes retry positive short progress; zero progress is io
Clockclock_time_get: private u64 timestamp; poll_oneoff: zeroed 48-byte clock subscription, 32-byte event and u32 count; check count/error and recheck the monotonic deadline; zero polls once, negative is invalid, durations above 2^63-1 nanoseconds give range; monotonic origin is command entry
Entropyrandom_get: private byte allocation, size 0..16777216; return a copy
Arguments / Environmentargs_sizes_get/args_get and environ_sizes_get/environ_get: private u32 pointer vector and byte buffer, checked NUL-terminated UTF-8 extents; args excludes argv[0]; environment lookup validates its name
Fsfd_prestat_get/dir_name selects the longest matching preopen; path_open uses private UTF-8 path and u32 output; fd_read/write/close (flush validates the live handle; writes are unbuffered); path_create_directory/unlink_file/remove_directory/rename/filestat_get; fd_readdir uses checked 24-byte dirents and cookies, retrying truncated entries

Fs exposes fresh run-local Int tokens, never raw descriptors or addresses. Closed tokens never name a reused descriptor. Paths outside all preopens give denied; the WASI host enforces traversal and symlink confinement. temp_dir selects the /tmp preopen when present, otherwise the first preopen; no preopens gives not_found. Preview-1 errno values map to the same standard string error codes as native IO (not_found, denied, exists, invalid, closed, interrupted, or io). IO failures do not become runtime traps.

B.7.1 Checked Wasi adapter

The ordinary Wasi service’s memory fields below are synchronous. Token is an Int internally naming a live allocation in this instance’s dedicated checked-buffer table. The table never contains ABI argument/result buffers, runtime scratch, GC data or allocator metadata. Export no operation giving a token’s address. Allocation produces zeroed bytes and a fresh positive token, never reusing one in that instance. End of entry/startup failure/stop frees remaining checked buffers. A token from another instance is not a portable memory reference; no library IO value wraps it. Root APIs expose operations, not the table.

FieldType
alloc(Int) <- (Fail(String)) -> Int
free(Int) <- (Fail(String)) -> Nil
store(Int, Int, BitArray) <- (Fail(String)) -> Nil
load(Int, Int, Int) <- (Fail(String)) -> BitArray
fd_write(Int, Int, Int, Int) <- (Fail(String)) -> Int
proc_exit(Int) <- (Fail(String)) -> Nil

Wasi is an ordinary service type with this checked signature. Its implementation is a checked adapter, not generic Int ABI lowering. Library helpers may wrap buffer tokens privately and add structured cleanup, as ordinary code. Possession of the root grants use of its buffer table; tokens are not secrets.

Validation order is fixed: validate scalar ranges/whole-byte inputs, then look up a live token, then check its extent. Negative sizes/offsets, an fd or exit code outside unsigned 32 bits, and non-whole-byte store input give invalid. Unknown/nonpositive/freed tokens (including double free) give closed. A valid token with offset or size outside its allocation gives bounds. Use mathematical checked addition, never wrapping pointer arithmetic. For empty loads/stores require 0 <= offset <= allocation size; they do not bypass liveness. An allocation size above memory32’s addressable range gives invalid; exhaustion within that range stops for memory exhaustion. Successful free removes exactly one table entry before freeing its allocation; there is no size argument or interior-pointer free. Load returns a copy.

Tokens are identifiers, so token + 1 is not a pointer into token. It may name another allocation only if that token is live in this authorized table; there is no promise of unforgeability. To access a subrange pass token and checked offset. Stale tokens cannot become valid after allocator address reuse. A call pins its allocation until it returns. These synchronous fields do not yield; no i callback can free a pinned buffer inside a host call. For future async adapters, retain a lease or copy before returning; they cannot inherit raw addresses from this synchronous contract.

Checked layouts. For fd_write, validate the inputs above; allocate private scratch for one iovec and a u32 count; translate the checked slice to the real pointer; call the runtime-owned preview-1 import; then free scratch on every returning path. On errno != 0, raise String errno:<unsigned decimal>. On success return the number of bytes written, 0..size; a larger count is an invalid host value and stops. No user-controlled pointer-bearing bytes are ever passed as an iovec. Other preview-1 functions require separately specified pointer/length/output layouts and raw signatures before an adapter is added.

proc_exit validates its unsigned code, runs context teardown, then calls the existing raw (i32) -> () import. Its i signature is Nil because §10.3 has no Never result; successful execution never returns an i Nil. If the host returns unexpectedly, trap and retire the instance. Invalid code raises invalid before teardown or exit. There is no fabricated errno/result from a void Wasm call. Teardown does not run i finals; explicit process exit is termination, not a way to detach work. WASI resource cleanup must finish before this call.

The adapter reuses the runtime-owned fd_write/proc_exit imports; it does not declare duplicates. The independent runtime stop path, including startup stops, source positions, partial-write retries, LF and exit code 1, cannot call a replaceable Io. Replacing Io leaves this stop path intact.