zena:wasi
import {…} from 'zena:wasi';
Low-level WebAssembly Component Model and WASI runtime integration.
Provides Canonical ABI string marshaling, memory management hooks, asynchronous subtask integration for futures, and byte stream bridges.
Classes
ByteStreamPair
final class ByteStreamPair
A fresh stream's two ends.
Type aliases
Waitable
type Waitable = i32
A waitable's handle: a subtask, or a stream or future end. One host table holds them all, so one type names them all.
WaitableSet
type WaitableSet = i32
A waitable set's handle. Zero is never one — the host never hands out 0 — which is what lets a field mean "no set yet".
CallbackCode
type CallbackCode = i32
What the entry and the callback return to the host: the low four
bits are the code — EXIT or WAIT — and, for WAIT, the rest is the
waitable set to wake the task for, so a WAIT is
WAIT | (set << 4).
ReadableByteStream
type ReadableByteStream = i32
The two ends of a stream<u8>, as unforgeable handle types: a
readable end is what a WIT import like write-via-stream is handed,
a writable end is what the guest keeps and writes, and nothing but
newByteStreamPair can produce either. On the wire each is a
component-model handle index (a core i32).
WritableByteStream
type WritableByteStream = i32
Functions
realloc
function realloc(ptr: i32, oldSize: i32, align: i32, newSize: i32): i32
The canonical ABI's allocator, as the realloc option names it.
The host calls this to build values in the guest's memory before
handing them over — a string argument to an export arrives already
copied into a buffer this returned. Its contract is C's realloc
plus an alignment: ptr == 0 allocates, newSize == 0 frees, and
anything else moves.
align is not passed on to the allocator. FreeListAllocator puts
an 8-byte header in front of every block and rounds every size up to
8, so the pointers it hands out are already 8-aligned, and the
canonical ABI never asks for more than 8. Going through
allocAligned instead would align a pointer inside a block and
return that, which free cannot then look up.
Exhaustion throws, and this is the one function in the language the host calls with no Zena frame above it — lowering an argument happens before the export is entered. So the throw unwinds out of the module and becomes a trap. That is the right outcome: returning 0 would make the canonical ABI write the value at address 0 and carry on.
stringFromMemory
function stringFromMemory(ptr: i32, len: i32): String
A string as the guest sees it: len bytes of UTF-8 at ptr,
copied into a fresh Zena String.
Copies rather than viewing, and does not free: a Zena String is a view
over a garbage-collected byte array, and there is no linear-memory
buffer it could keep. Whether the buffer was the guest's to release is
the caller's to know — see releaseStringArgument for the case where
it is.
releaseStringArgument
function releaseStringArgument(ptr: i32, len: i32): void
Free the buffer a string argument arrived in.
Lowering an argument into an export uses the lifted function's
realloc, so the bytes come out of this allocator and are the
guest's to release once they have been copied. The canonical ABI's
post-return is specified for the return value and cannot reach
them; a guest that never calls this leaks a block per string
argument, and the free list never gets it back.
The guard is on the length rather than the pointer. A zero-length
string is lowered through realloc(0, 0, 1, 0), which returns
align — an aligned address that was never allocated. Freeing that
would walk into a block header that is not there.
takeStringArgument
function takeStringArgument(ptr: i32, len: i32): String
A string argument taken whole: copied out, its buffer released.
For a string inside an aggregate parameter, where the generated
lift is one expression.
takeBytesArgument
function takeBytesArgument(ptr: i32, len: i32): FixedArray<u8>
A list<u8> argument taken whole, with the same ownership story.
stringResultArea
function stringResultArea(s: String): i32
A string result, as the host reads it: the bytes copied into linear
memory, and the address of an 8-byte area holding (ptr, len).
A single string flattens to two core values, one more than a lifted
function may return, so the canonical ABI returns it indirectly: the
core function's i32 result is this area's address, and the host
reads the pair out of it. postReturn frees both halves once it
has.
The bytes are copied verbatim, which assertWtf8 guards — true for
every string a component can build today, since WTF-16 strings come
from JS interop and the js target is not this one, but an
assertion rather than an assumption.
notePostReturn
function notePostReturn(mark: i32): void
Note that the return area a wrapper is about to return, and
everything staged since mark (a string's bytes, a list's
elements — what the area points into), stays allocated until the
host's post-return.
postReturn
function postReturn(area: i32): void
The post-return option: free the return area the host has just
read — a staged range a wrapper noted, or else what
stringResultArea allocated.
The host calls this after it has read the result out, which is the only moment at which the guest can know the buffer is dead. Without it the component still works and leaks a result per call.
stringToMemory
function stringToMemory(s: String): (i32, i32)
Stage a String's bytes for a lowered call.
bytesToMemory
function bytesToMemory(bytes: FixedArray<u8>): (i32, i32)
Stage a byte buffer for a lowered call: list<u8> and string have
the same wire shape. The cast widens: Memory's indexer trades in
i32 — the language has no implicit numeric conversions, widening
included — so the u8 element is spelled up to it.
resultArea
function resultArea(size: i32): i32
Allocate the return area a spilled result is written into.
liftStringAt
function liftStringAt(ptr: i32): String
A string lifted in place: the (ptr, len) pair at ptr, its
bytes copied out and the buffer they arrived in freed — the
canonical ABI allocated it through realloc above, so it is ours.
The eight bytes of the pair itself belong to whatever contains them
(a return area, a record field, a list element) and stay put.
liftBytesAt
function liftBytesAt(ptr: i32): FixedArray<u8>
A list<u8> lifted in place, with the same ownership story.
freeLifted
function freeLifted(ptr: i32): void
Free a host-written allocation once its contents are lifted: a return area, or a list's element buffer.
loadU8
function loadU8(ptr: i32): i32
loadS8
function loadS8(ptr: i32): i32
loadU16
function loadU16(ptr: i32): i32
loadS16
function loadS16(ptr: i32): i32
loadI32
function loadI32(ptr: i32): i32
loadI64
function loadI64(ptr: i32): i64
loadF32
function loadF32(ptr: i32): f32
loadF64
function loadF64(ptr: i32): f64
storeU8
function storeU8(ptr: i32, value: i32): void
storeU16
function storeU16(ptr: i32, value: i32): void
storeI32
function storeI32(ptr: i32, value: i32): void
storeI64
function storeI64(ptr: i32, value: i64): void
storeF32
function storeF32(ptr: i32, value: f32): void
storeF64
function storeF64(ptr: i32, value: f64): void
stageBuffer
function stageBuffer(size: i32): i32
Allocate size bytes of staging space, freed by releaseStaged.
Zero bytes is zero: realloc(…, 0) returns an unowned aligned
sentinel that must never reach free.
stageString
function stageString(s: String): (i32, i32)
Stage a String's bytes, arena-tracked: (ptr, len), with (0, 0)
for an empty string — nothing is allocated, nothing to free.
stageByteList
function stageByteList(bytes: FixedArray<u8>): (i32, i32)
Stage a byte buffer, arena-tracked: list<u8> and string have
the same wire shape.
stagedMark
function stagedMark(): i32
The arena's current position, taken before a wrapper stages
anything. Release is by range (releaseStagedFrom), not pop-all:
an async-lowered call's arguments may be lifted by the callee after
the call returns (the STARTING state), so a wrapper suspended on
its subtask still owns live staging — and any wrapper that ran in
the meantime must free only its own.
releaseStagedFrom
function releaseStagedFrom(mark: i32): void
Free everything staged since mark. Ranges nest like the calls
that created them, so popping to a mark never frees an outer
wrapper's still-live staging.
waitableSetNew
declare function waitableSetNew(): WaitableSet
waitableJoin
declare function waitableJoin(waitable: Waitable, set: WaitableSet): void
Put waitable into set; a set of 0 removes it from whichever set
it is in. A join is persistent until the next join.
beginTask
function beginTask(): void
Start a task. Called by the synthesized adapter at the top of every
export the host lifted async — the entry, a service's handle —
before the program's function runs, so what that function waits
on and returns belongs to the new task and not to whichever task
ran last. Without this, two requests in flight in one instance
would share a context: both joined to one waitable set, the second
value parked where the first was already returned, and the host
answering the wrong request or none.
awaitPacked
function awaitPacked(packed: i32): Future<void>
An async-lowered import's packed result, as a future. packed is
the raw (subtask << 4) | state the lowering returned (see
STATE_MASK), taken as an i32 because that is what a raw import
hands back.
State RETURNED means the call finished inside the lowering — the timer had already expired, the write already flushed — and the future arrives completed, with no subtask to own. Anything else is a subtask: joined to the running task's set, registered, resolved by the callback.
awaitWaitable
function awaitWaitable(end: i32): Future<i32>
A stream or future end's next event, as a future of the packed
(progress << 4) | CopyResult payload. The caller has just issued
an async copy on end and got BLOCKED back: the copy is in flight,
and the payload arrives when the host has moved what it can. end
is the raw handle as the copy builtin took it.
finishTask
async function finishTask<T>(result: Future<T>, ret: (value: T) => void): Future<void>
Hand the running task's result to the host once result settles:
ret is the typed task.return for the task's result type, and
the driver calls it from the task's own entry — the one running now
if the value is already known by the time the entry asks what to
do next, else the callback re-entry after which it is. A failure
is reported from that entry instead. Called by the entry wrapper
the compiler writes for an async main that returns a value
(lib/component-entry.zena in the compiler).
componentPoll
function componentPoll(): CallbackCode
Called by the synthesized entry after main returns: run the
microtask checkpoint, then either exit or ask to be woken.
componentResume
function componentResume(event: i32, fired: i32, code: i32): CallbackCode
The callback the host calls when a waitable fires. It resolves the completer parked on that waitable, runs the microtask checkpoint — the runtime's, at the host boundary, where a JS engine runs its own — and answers for the task that was waiting on the waitable.
A SUBTASK event's code is its new state — a subtask is delivered
more than once, and only RETURNED resolves anything (dropping an
unresolved subtask traps, so the state is checked rather than
assumed). A stream or future event's code is the packed copy
payload, delivered exactly once per parked copy, and it resolves
the end's completer with the payload for the parked reader or
writer to decode.
newByteStreamPair
function newByteStreamPair(): ByteStreamPair
writeStringBlocking
function writeStringBlocking(end: WritableByteStream, s: String, newline: boolean): void
Write a String's bytes to a writable end, appending a newline when
asked, blocking until every byte is consumed. The String staging is
the one convenience the current consumer (zena:console) needs; a
byte-buffer entry point over the same flushBlocking core arrives
with its first consumer, the Stream<u8> boundary binding.
lowerByteStream
function lowerByteStream(source: Stream<u8>): i32
A guest Stream<u8> as a canonical stream<u8> value: a fresh
pair, a background pump copying the guest stream into the writable
end, and the readable end — which IS the transferable value —
returned as the wire handle. The pump ends the canonical stream
when the source ends, and stops the source when the host reader
drops its end.
liftByteStream
function liftByteStream(handle: i32): Stream<u8>
A canonical stream<u8> handle as a guest Stream<u8>: a
background pump reads the readable end on the event loop and
delivers into a StreamWriter, whose stream is returned — with the
writer's backpressure, so the pump never reads the host faster than
the guest consumes. The pump closes the stream when the host drops
its writable end, and drops the readable end when the guest reader
stops.
Symbols
rawHandle
symbol rawHandle
The marshaling keys every synthesized resource class shares. A WIT
resource wrapper exposes its wire handle and its moved flag through
symbol-keyed members, and the symbols live HERE — one module, one
identity — because a synthesized interface may hand its resources
to another synthesized interface (use types.{request}), and a
per-module symbol would make the consumer's [rawHandle] a
different member than the class declares.
markMoved
symbol markMoved