wasm

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_tBacking method
CLOCK_REALTIMErealtimeNanos()
CLOCK_MONOTONICmonotonicNanos()
CLOCK_PROCESS_CPUTIMEmonotonicNanos() (folded)
CLOCK_THREAD_CPUTIMEmonotonicNanos() (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

Search

Esc
to navigate to open Esc to close