WASM

← Technical Notes
Io

Running Io in the browser via WebAssembly.

About

Historically the Io VM was a native C binary with a per-platform build matrix (macOS, Linux, Windows, BSD), platform-specific coroutine assembly, and a native addon model that compiled C extensions against the host toolchain. That model works, but every new platform multiplies the work: new assembly for coroutines, new build recipes, new binary artifacts, new ways for addons to break.

Compiling the VM to WebAssembly collapses that matrix to a single portable module. The same io_static.wasm runs under wasmtime, Node.js, and directly in the browser, with no platform-specific code paths in the VM itself.

Why it matters

  • One binary, every host — a single WASM module replaces the per-OS and per-architecture build matrix. No cross-compilation toolchains, no CI jobs for each target, no separate releases. If your environment has a WASM runtime, it can run Io.
  • Runs in the browser — the same VM that runs on the command line loads as a script tag. Io programs get direct access to the DOM, fetch, Web Audio, WebGL — any capability the host page exposes — without a separate “web Io” fork.
  • Bidirectional Io↔JavaScript bridge — the old native-addon model is replaced by a symmetric bridge: Io can call any JavaScript function and receive JS objects as Io values; JavaScript can call Io methods and pass JS values as arguments. One mechanism replaces what used to require a per-library C addon.
  • Access to the JavaScript ecosystem — through the bridge, Io programs can reach the roughly two million packages on npm and every Web API the browser exposes. The classic Io distribution shipped a few dozen hand-written addons covering networking, databases, graphics, crypto, and serialization; the JS ecosystem already covers all of those, plus machine learning, 3D rendering, audio synthesis, protocol implementations, cloud SDKs, and much more — without anyone writing a line of binding code. Io inherits decades of JavaScript library work as a side effect of the port.
  • Embeddable by design — a WASM module is an embeddable artifact. Native apps can host Io through wasmtime or wasmer; server-side JS can host it through Node; the browser hosts it directly. Embedding no longer means linking C libraries and matching ABIs.
  • Sandboxed by default — WASM modules only see the capabilities their host grants. File-system and network access flow through WASI or host-supplied JS, not raw syscalls, so an Io program can’t silently reach parts of the system the host didn’t intend to expose.
  • Forces a cleaner core — the WASM target doesn’t expose the native C stack, which ruled out the old ucontext/setjmp coroutine implementations and motivated the stackless evaluator. The discipline that came with the port left the VM smaller, more portable, and easier to reason about.

Trade-offs are real: the WASM target is early-access, JIT throughput depends on the host runtime, and some classic native addons (notably anything linking C libraries) don’t carry over — their roles are now filled by JavaScript libraries reached through the bridge.

WASI 0.3

In 2026 the Bytecode Alliance shipped WASI 0.3, whose headline change is that async is now native to WebAssembly components. The component model's canonical ABI gains three first-class constructs — stream<T>, future<T>, and async functions — and the host runtime takes over scheduling with a single shared, completion-based event loop (in the style of io_uring and IOCP) instead of each component polling readiness through pollable handles. Wasmtime 46 ships these interfaces with async enabled by default, and jco is bringing the same model to JavaScript hosts.

Why this fits Io unusually well

  • Blocking I/O stops blocking the VM — today's build targets WASI preview1, where every read and write is synchronous. A WASM module has a single thread of execution, so one coroutine waiting on I/O stalls every other actor in the VM. Under WASI 0.3 the host event loop owns the wait: a coroutine that performs I/O can be parked on a future<T> while the scheduler runs other coroutines, then resumed when the host completes the operation.
  • The stackless evaluator already paid the entry fee — most language runtimes need the Asyncify transform (which inflates code size and slows execution) or compiler-level async/await to suspend mid-call inside WASM. Io's evaluator keeps all execution state in heap-allocated frames, so suspending on a host future is the same operation as an ordinary coroutine switch — no binary transform, no annotations. WASI 0.3 was explicitly designed to accommodate both stackful and stackless coroutine runtimes; Io is in the second camp by construction.
  • Actors map directly onto host futures — Io's @ (futureSend) and @@ (asyncSend) already give programs a future-based concurrency surface. A WASI 0.3 future<T> is the host-level version of the same idea, so an Io future awaiting a network response could be backed one-to-one by a host future and consume no VM scheduling at all until completion.
  • Streams replace the polling dance — WASI 0.2 I/O required a three-step start/finish/subscribe pattern over pollables. 0.3 collapses that into stream<T> values paired with completion futures that can finally distinguish “stream closed” from “stream failed.” Io's File and any future socket primitives map onto these cleanly.
  • The VM as a component — packaging io_static.wasm as a WebAssembly component gives it typed, language-neutral interfaces. Components compose in-process (“service chaining”), so an Io component could sit in a pipeline next to components written in Rust or Go with nanosecond rather than millisecond call overhead — a substantial upgrade to the embedding story above.

The migration path is incremental rather than architectural, and the first two steps have landed. The VM now also builds as a real WASI 0.2 component (make component, via wasi-sdk's wasm32-wasip2 target) that passes the full correctness suite under wasmtime. And the scheduler gained a timer queue: wait parks the calling coroutine instead of busy-spinning, timed coroutines run concurrently, and when nothing is runnable the VM blocks in a single host wait until the nearest deadline — the one idle point where a WASI 0.3 future<T> will be awaited instead. The remaining step — backing that idle point and the I/O primitives with 0.3 futures and streams — is gated on host and C-toolchain support (Wasmtime 46 and future<T>/stream<T> lowering from C), a change the Bytecode Alliance describes as “entirely mechanical” on the interface side.

Browser Target

Io runs in the browser as a WebAssembly module. The browser build compiles the full VM into a WASM reactor module that JS loads and drives via exported functions.

View →

DOM Interop

Io code running in the browser can query, create, and manipulate HTML elements through the DOM object and Element instances.

View →