WASI support lands on Cloudflare Workers

Cloudflare has added experimental support for WASI — the WebAssembly System Interface — to Workers, with companion tooling in wrangler2. The move follows the company's earlier adoption of WebAssembly on the platform in 2017 and is aimed at reducing the friction developers face when targeting the Workers runtime with compiled languages.

Why WASI matters

WebAssembly provides a secure, near-native-speed sandbox for compiled code, but it was originally designed to run alongside JavaScript in the browser. That means WebAssembly modules have no built-in way to perform I/O tasks such as reading files, making network requests, or querying the system clock. Developers have to bridge that gap by writing JavaScript glue code to handle events and import/export functions between the two runtimes.

Existing solutions like Emscripten and wasm-bindgen mitigate the problem, but they are language-specific and add complexity and bloat. Cloudflare's own workers-rs library, built on wasm-bindgen, made Rust development feel more native within Workers, but it was hard to maintain and locked developers into Workers-specific code that wasn't portable.

WASI defines a standard system interface that any language compiling to WebAssembly can target. Lin Clark's original Mozilla post describes WebAssembly as an assembly language for a "conceptual machine," while WASI serves as a systems interface for a "conceptual operating system." This standardization lets existing toolchains cross-compile code to the wasm32-wasi target without custom per-runtime adaptations.

Toolchain support has matured considerably. Clang/LLVM through the wasi-sdk, the Rust compiler, and implementations in TinyGo and SwiftWasm all leverage a version of Libc built on WASI system calls. Practically, this means the same "Hello World" program that runs on a local Linux or Mac machine can run in any WASI-compliant WebAssembly runtime.

Same code, multiple runtimes

The practical benefit is demonstrated with a minimal Rust application. A basic program with a main() function and a println to stdout compiles and runs natively.

$ cargo new hello_world
$ cd ./hello_world
$ cargo build --release
   Compiling hello_world v0.1.0 (/Users/benyule/hello_world)
    Finished release [optimized] target(s) in 0.28s
$ ./target/release/hello_world
Hello, world!

Compiling that identical source against the wasm32-wasi target and running it in an off-the-shelf runtime like Wasmtime works without changes.

$ cargo build --target wasm32-wasi --release
$ wasmtime target/wasm32-wasi/release/hello_world.wasm

Hello, world!

Taking the same binary that Wasmtime executed and publishing it to Workers via wrangler2 also works. The same code runs across multiple POSIX environments, and the same compiled binary runs across multiple WebAssembly runtimes.

$ npx wrangler@wasm dev target/wasm32-wasi/release/hello_world.wasm
$ curl http://localhost:8787/

Hello, world!

CLI applications as cloud services

The example works by streaming stdin and stdout to and from a Worker via the HTTP request and response bodies. This enables a neat pattern: command-line programs can be deployed as cloud services without being rewritten.

Hexyl, a hex viewer utility, works completely out of the box. After compiling it with the same steps used for "Hello World," the resulting binary can be previewed in wrangler2 with input piped from a local file.

$ git clone [email protected]:sharkdp/hexyl.git
$ cd ./hexyl
$ cargo build --target wasm32-wasi --release
$ npx wrangler@wasm dev target/wasm32-wasi/release/hexyl.wasm
$ echo "Hello, world\!" | curl -X POST --data-binary @- http://localhost:8787

┌────────┬─────────────────────────┬─────────────────────────┬────────┬────────┐
│00000000│ 48 65 6c 6c 6f 20 77 6f ┊ 72 6c 64 21 0a          │Hello wo┊rld!_   │
└────────┴─────────────────────────┴─────────────────────────┴────────┴────────┘

Hexyl demonstrates the pattern with minimal effort. A more involved example is swc, a JavaScript/TypeScript transpiler. Deploying swc as an on-demand transpilation service requires a few extra steps to minimize the compiled output size, but the code otherwise runs as-is. The steps are documented in the swc example repository.

$ echo "const x = (x, y) => x * y;" | curl -X POST --data-binary @- https://swc-wasi.examples.workers.dev/ --output -

var x=function(a,b){return a*b}

C/C++ code can be deployed similarly, though getting the Makefile right requires more effort. A zstd example shows how to compile the compression library and upload it as a streaming compression service.

https://github.com/zebp/wasi-example-zstd

$ echo "Hello world\!" | curl https://zstd.examples.workers.dev/ -s -X POST --data-binary @- | file -

Calling WASI modules from JavaScript

Wrangler streamlines deployments for developers who don't need to interact with the Workers ecosystem directly. For those who do want to invoke a WASI-based module from JavaScript, a small boilerplate is available. The README is maintained at https://github.com/cloudflare/workers-wasi.

import { WASI } from "@cloudflare/workers-wasi";
import demoWasm from "./demo.wasm";

export default {
  async fetch(request, _env, ctx) {
    // Creates a TransformStream we can use to pipe our stdout to our response body.
    const stdout = new TransformStream();
    const wasi = new WASI({
      args: [],
      stdin: request.body,
      stdout: stdout.writable,
    });

    // Instantiate our WASM with our demo module and our configured WASI import.
    const instance = new WebAssembly.Instance(demoWasm, {
      wasi_snapshot_preview1: wasi.wasiImport,
    });

    // Keep our worker alive until the WASM has finished executing.
    ctx.waitUntil(wasi.start(instance));

    // Finally, let's reply with the WASM's output.
    return new Response(stdout.readable);
  },
};

With the JavaScript boilerplate and the compiled WASM module in hand, deployment uses wrangler's WASI feature.

$ npx wrangler publish
Total Upload: 473.89 KiB / gzip: 163.79 KiB
Uploaded wasi-javascript (2.75 sec)
Published wasi-javascript (0.30 sec)
  wasi-javascript.zeb.workers.dev

A familiar pattern

Long-time developers may notice the resemblance to RFC3875, the Common Gateway Interface (CGI). While the current example doesn't conform to that specification, the stdin/stdout model hints at how a basic command-line application could be extended into a full HTTP handler.

Developers who build on this capability are encouraged to share results on Discord or Twitter.