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 ofsizebytes inmemory. For size 0 it may give 0.ilang_free(address: i32, size: i32): frees a buffer made byilang_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 butilang_allocandilang_free. Duringilang_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:
| Type | Wasm values |
|---|---|
Nil | none |
Bool | i32: 0 is False, 1 is True |
Float | f64 |
Int | i32 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. |
String | i32 address, i32 length: the UTF-8 bytes |
BitArray | i32 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 type | externref |
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, thenP2, and so on. - Results, when
EisNever: the values ofA. - Results, when
Ehas members: first ani32tag, then the values ofA, then the values of each member ofEin 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 anexternref.
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_allocand belongs to the receiver, which frees it withilang_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 newilang:host/v1runtime imports. - External functions (§10.1): the import module and name of the
@externalattribute, 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
i32request 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 throughilang_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 ofilang_continue, and not while an import runs (also not from a call such asilang_allocthat the host makes during an import). So it does not call one duringilang_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) -> i64gives 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 requestid(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 exampleapp/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
i32status. 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 callsilang_continueonly 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 namesilang_continue,ilang_start,ilang_alloc,ilang_free,memoryand those that start withilang_resume:orilang_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:labelof 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 withilang_allocjust 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:nameof 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, andilang_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
panicimport (§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 callsilang_starton 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" } }
}
} -
versionis 5. Hosts read versions 3, 4 and 5 and use the value encoding of the module’s version. A host refuses a newer module withthe 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-bitIntform (§B.2). Version 4 added the native multicore worker imports (mc_*inilang: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 exportilang_continueare new, andilang_continueis a reserved export name; the runtime importcancelis 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 orilang_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, andexternref.errorsis in the canonical order (§B.2). -
entrynames the selected function by its module path and function name, not its[entries]key. It isnullwhen 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.testsis empty unless tests were compiled. -
serviceslists every required host-registration function of the host services that the platform provides to the entries, andexternalsevery 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, andilang:host/v1before 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 notarget. A host of this appendix does not run a module with atarget.
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_writeandproc_exitfromwasi_snapshot_preview1, and notpanicfromilang: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_idis imported fromilang:host/v1only 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
Iowrites 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 givesNil. 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 namespacei:io/v1is an import like any other. -
Because the runtime imports
fd_writeandproc_exit, an import (§B.4) with the import modulewasi_snapshot_preview1and 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 ofwasi_snapshot_preview1may 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’smemoryorilang_alloc, so it can only give functions whose parameters, values, and errors areNil,Bool, orFloat. An import with anInt,String, orBitArrayneeds 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 importsreference_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:…andilang_result:…exports. -
It exports
_start, which callsilang_startand then the program entry. The program must have a selected entry; if not, it is a compile error. When_startreturns, 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.
| Capability | Raw preview-1 imports and checked layouts |
|---|---|
| Stdio | fd_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 |
| Clock | clock_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 |
| Entropy | random_get: private byte allocation, size 0..16777216; return a copy |
| Arguments / Environment | args_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 |
| Fs | fd_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.
| Field | Type |
|---|---|
| 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.