There is an ongoing tug-of-war over the future of server-side WebAssembly.
One side embraces the idea that Wasm was designed in the context of a broader web platform. Their approach to running Wasm outside of the browser is to transplant relevant parts of the web platform onto the server, and run Wasm within this browser-like context.
The other side sees Wasm as a CPU-independent bytecode, for which the browser is just one use case. Their approach is to standardize a syscall-like interface (WASI), akin to the one an operating system provides to native code.
The source of the rift is that Wasm doesn't specify a particular interface with the outside world, by design. While Wasm itself is portable across platforms, it’s only really useful when paired with such an interface.
Although both sides generally imagine the industry converging on common standards so that code is interoperable between platforms, they are pulling towards fundamentally incompatible visions of what those common standards should look like.
Compilers that produce browser-ready Wasm generate (at least) two files: the Wasm module itself, and a JavaScript shim. The JavaScript shim implements a bespoke ABI that exposes any relevant browser APIs to the module.
Platforms that run Wasm outside of the browser piggyback on this compiler infrastructure by running the Wasm module inside a JavaScript runtime, typically V8. They can either mock existing browser APIs, or create their own interfaces that are compatible with the JavaScript shim generator.
This is how Cloudflare supports Rust, for example. It’s also advocated for by AssemblyScript, one of the most popular Wasm-targeting languages.
Interoperability of this approach between runtimes depends on the underlying JavaScript APIs being interoperable. For example, a Wasm module is only cross-compatible between Node.js and Deno to the extent that the JavaScript APIs it calls (via the shim) are available on both.
WinterCG is an effort towards cross-runtime interoperability between browsers and non-browser runtimes. It has the support of a number of vendors including Cloudflare, Deno, Netlify, and Vercel.
The ultimate vision here is a world where JS and Wasm code can both be packaged into modules and run isomorphically in the server and in the browser.
The upside of this approach is that if a platform already supports JavaScript, it can support Wasm without really even knowing: Wasm just becomes an implementation detail of the module.
The downside of this approach is that it is inherently tied to JavaScript. JavaScript runtimes are heavyweight compared to Wasm-only runtimes, and doing interop through them requires spending extra CPU cycles casting datatypes into JavaScript values.
The main alternative to shipping a JavaScript engine is to use a runtime that implements the WebAssembly Standard Interface (WASI).
WASI comes from the Bytecode Alliance, an industry group which counts Google, Mozilla, Docker, and Fastly among its members.
WASI provides a POSIX-inspired (but deliberately non-POSIX) ABI.
Currently, most WASI calls are thin wrappers around OS syscalls that provide access to stdio, filesystems, the system clock, and random number generation. In the future, WASI's scope will likely include network sockets and threads.
Docker creator Solomon Hykes famously tweeted that if WASI was around in 2008, Docker wouldn’t exist.
Four years after that tweet, though, WASI on its own still isn’t powerful enough to do table-stakes things like make an HTTP request.
Typically, vendors provide their own ABIs to fill such gaps: Fermyon uses wasi-experimental-http; wasmCloud provides an HTTP capability; Fastly defines a Request struct; WasmEdge provides a request API. Lunatic goes a level lower and provides a TCP API.
The problem with this state of things is that Wasm code becomes bound to a particular platform, and any language libraries that touch I/O need to be patched or replaced for each one.
For all the work that’s gone into making Wasm portable across CPU architectures, we’ve ended up with modules that are not even portable across platform vendors. After all, wasn’t that the selling point of Docker?
I’m optimistic about the Wasm component model becoming a forcing function for standardization here: as it matures, vendors will feel pressure to rip out their proprietary ABIs and replace them with components, and as they do, standard component interfaces will emerge.
There’s always a risk that the component model will just be a fifteenth competing standard. But I’m hopeful, because the tech looks solid and the industry generally seems willing to embrace it.
As much as I’m broadly in favor of (or at least, resigned to) the web platform becoming a universal operating system of sorts, I’m rooting for the WASI/components side here.
I don't think applications should have to pay the JavaScript tax every time they interact with the outside world. But more importantly, it just feels more right.
Case in point: the Rust crate getrandom
is implemented on both the web and WASI. Compare the web platform implementation to the WASI implementation.
(This is a bit unfair because the former also handles the case of running on Node.js, but also consider that the WASI code can stand on its own whereas the web platform version also needs to generate JavaScript code to run.)
I’m hopeful that WASI will evolve over time to cover enough of libc
that a lot of existing libraries (like database drivers and file loaders) can be compiled to it without major rewrites. If this happens, those proprietary ABIs can be torn down like scaffolding on a completed cathedral.
Until next time,
-- Paul