WasiContext
The configuration passed to Wasi.preview1 — args, envs, stdio sinks, clock, random source, preopens, sockets.
WasiContext is the configuration record Wasi.preview1(ctx) accepts. It’s a plain case class with nine fields, all of which have sensible defaults:
final case class WasiContext(
args: Seq[String] = Seq.empty, // argv
envs: Seq[(String, String)] = Seq.empty, // KEY=VALUE pairs
stdout: Int => Unit = WasiContext.defaultStdout, // per-byte sink for fd 1
stderr: Int => Unit = WasiContext.defaultStderr, // per-byte sink for fd 2
stdin: (Array[Byte], Int, Int) => Int = WasiContext.defaultStdin, // reader for fd 0
clock: WasiContext.Clock = WasiContext.systemClock, // realtime + monotonic
random: Int => Array[Byte] = WasiContext.defaultRandom, // random_get source
preopens: Seq[WasiContext.Preopen] = Seq.empty, // fd 3 .. 3+P-1
sockets: Seq[WasiContext.ServerSocket] = Seq.empty, // fd 3+P .. 3+P+S-1
)
The stdin reader has the POSIX read shape: fill dst[off .. off + len) with up to len bytes from the current stream position, return the number of bytes written (0 = EOF, never negative). The default returns 0 immediately, so a program that reads stdin without one being supplied sees a clean empty input rather than EBADF. WasiContext.stdinFromBytes(bytes) builds a cursor-tracking reader from an Array[Byte]; the CLI’s --stdin <path> uses it to stream a file through fd 0.
Every wasi syscall that needs host state reads from this record — args_get walks args, clock_time_get calls clock.realtimeNanos(), random_get calls random(n), and so on. Copy-and-modify (ctx.copy(args = …)) is the only way to construct one; there is no mutation.
Factories
WasiContext.default
val ctx = WasiContext.default
Equivalent to WasiContext() — every field at its default. stdout / stderr go to System.out / System.err, the clock walks System.currentTimeMillis + System.nanoTime, random uses scala.util.Random, no args, no envs, no preopens.
Use it when running a real binary outside tests. Pair it with .copy(...) to override individual fields:
val ctx = WasiContext.default.copy(
args = Seq("myprog", "input.txt"),
preopens = Seq(HostPreopen.fromDir("/var/data", "/data")),
)
WasiContext.collecting(...)
val collecting = WasiContext.collecting(
args = Seq("myprog", "--flag"),
envs = Seq("LANG" -> "C"),
)
val ctx = collecting.context
Same shape as WasiContext.default but redirects stdout and stderr into ArrayBuffer[Byte]s you can read back after the guest runs:
collecting.stdoutBytes // Array[Byte]
collecting.stderrBytes // Array[Byte]
collecting.stdoutString // String (UTF-8 decoded)
collecting.stderrString // String (UTF-8 decoded)
This is what the WASI test suite uses to assert against program output without touching the real stdout/stderr. The clock / random / preopens overrides flow through identically, so the same factory works for any test that wants deterministic clock or random reads as well.
Collecting is single-threaded — the interpreter is single-threaded, and the underlying buffers aren’t synchronized. Don’t share a Collecting across threads.
Constructing from scratch
Both factories produce a WasiContext. If neither fits, build one directly:
val ctx = WasiContext(
args = Seq("prog"),
envs = Seq("PATH" -> "/usr/bin"),
stdout = b => myLogger.write(b),
stderr = b => myLogger.write(b),
clock = myClock,
random = myRandom,
preopens = Seq(myPreopen),
)
The Clock trait
trait Clock:
def realtimeNanos(): Long // wall clock, nanoseconds since Unix epoch
def monotonicNanos(): Long // arbitrary anchor, nanoseconds; only deltas matter
clock_time_get(id, ...) dispatches against this. Four wasi clockid_t values map onto these two methods:
clockid_t | Backing method |
|---|---|
CLOCK_REALTIME | realtimeNanos() |
CLOCK_MONOTONIC | monotonicNanos() |
CLOCK_PROCESS_CPUTIME | monotonicNanos() (folded) |
CLOCK_THREAD_CPUTIME | monotonicNanos() (folded) |
WasiContext.systemClock is the default — System.currentTimeMillis() * 1_000_000L for realtime, System.nanoTime() for monotonic. Cross-platform: JVM, Scala.js (via Date.now / performance.now), and Scala Native all support both calls without conditional code.
For deterministic tests, plug in your own:
val frozenClock = new WasiContext.Clock:
def realtimeNanos(): Long = 1_700_000_000_000_000_000L
def monotonicNanos(): Long = 42L
val ctx = WasiContext.default.copy(clock = frozenClock)
The random closure
random: Int => Array[Byte]
random_get(buf, len) calls ctx.random(len) and copies the result into linear memory at buf. The closure must return an array of exactly len bytes — shorter returns are not detected and would surface as silent zero-fill in the guest. The default implementation (WasiContext.defaultRandom) uses scala.util.Random.nextBytes; not cryptographic, but adequate for getrandom-style entropy in most cases.
Override for deterministic tests:
val ctx = WasiContext.default.copy(
random = (n: Int) => Array.fill(n)(0x42.toByte),
)
Or for a real CSPRNG, wire java.security.SecureRandom (JVM/Native) or crypto.randomBytes (JS) in here.
stdout / stderr sinks
stdout and stderr are per-byte callbacks. Every fd_write(1, …) byte goes through stdout; every fd_write(2, …) byte goes through stderr. They’re called exactly once per byte the guest writes, in order — they’re not aware of newlines or line buffering.
If you want line-buffered output, do the buffering in your callback:
val lineBuf = new StringBuilder
val stdout: Int => Unit = b =>
if b == '\n' then
println(lineBuf.toString)
lineBuf.clear()
else
lineBuf += b.toChar
val ctx = WasiContext.default.copy(stdout = stdout)
The sockets field
sockets: Seq[WasiContext.ServerSocket] — host-provided listening sockets, exposed to the guest one fd at a time starting at 3 + preopens.length. There is no sock_open / sock_bind / sock_listen in Preview 1; the host pre-binds and the guest can only sock_accept against the inherited fds.
JVM and Scala Native get a factory:
val (listener, port) = HostServerSocket.bind(0) // 0 → OS-chosen ephemeral
val ctx = WasiContext.default.copy(sockets = Seq(listener))
Scala.js doesn’t link java.net.ServerSocket; a Node-backed program that needs sockets must supply its own WasiContext.ServerSocket implementation (typically wrapping Node’s net module).
The trait surface is intentionally tiny:
trait ServerSocket:
def accept(): Either[Int, ClientSocket]
def address: String // "host:port" for debug logs
trait ClientSocket extends Wasi.FsFile:
def shutdown(how: Int): Either[Int, Unit]
// close/read/write inherited from FsFile — fd_read/fd_write
// route to them through the standard FdTable lookup.
accept blocks until a client connects; the returned ClientSocket is installed in the per-instance fd table by sock_accept and is observable through both fd_read / fd_write (the generic byte-stream surface) and the dedicated sock_recv / sock_send (the iovec-shape recv/send surface with recvflags/sendflags).
Where to go next
- Preopens — the
preopensfield in depth. - Syscalls — which fields each syscall reads.
- Concepts → ModuleInstance — what
Wasi.preview1(ctx)returns and how to drive it.