I’ve been pretty bullish on WebAssembly—or Wasm—for quite some time, as I believe that it offers a degree of portability and operational simplicity that goes beyond that of Linux virtual machines and even OCI containers. Run it in the browser, run it on your laptop, run it on Kubernetes, run it on a dedicated Wasm platform like Fermyon. While I’m not convinced that it will fully supplant VMs or containers any time soon, I do think that there’s a strong case that Wasm is already a superior technology in domains like edge functions and platform extensions.
But alas, there’s a bit of a snag: Wasm is extremely portable once you’ve already built it. But building Wasm isn’t trivial for three reasons:
- You can compile many languages to Wasm and each has its own tools and approaches.
- There are numerous Wasm-specific tools that you may want to include in your toolchain, such as wasm-tools and the WebAssembly Binary Toolkit (WABT).
- There are several Wasm runtimes currently available, including WasmEdge and Wasmtime.
And any time you need a bunch of separate tools to get your work done, there’s room for error and the classic “read the README and use apt-get/Homebrew/whatever to create your environment” approach to dependency management quickly runs into hard limits. Unsurprisingly, I think that using Nix to package Wasm provides a compelling path forward here.
My example project
To show how you can use Nix to work with Wasm, I’ve created an example project in the DeterminateSystems/nix-wasm-example repo.
The actual Wasm app I build here is extremely basic: just a Rust program that outputs the string "Hello there, Nix enthusiast!"
What’s notable here is that the program is compiled to conform to the WebAssembly System Interface (WASI), which essentially means that it’s built to interact with the outside world (by default, WebAssembly can’t act as a command line interface or system tool).
To build the program into WASI-compliant Wasm, I created a special Nix function called buildRustWasiWasm
that wraps Naersk’s buildPackage
derivation function.
That function builds the Rust sources using a special Rust toolchain that includes the wasm32-wasip1
target.
The post-install phase of the derivation also uses wasm-strip
to make the final Wasm binary more lean and wasm-validate
to ensure that the resulting binary is valid.
You should see a hello-wasm.wasm
binary in that directory.
Being able to build WebAssembly from Rust sources deterministically, without needing to use apt-get
or Homebrew or anything else, is nice.
But Nix enables you to do much more, so let’s have a bit more fun.
A full Wasm package
The nix build
command above deterministically builds a single Wasm binary from Rust source.
Build this derivation and inspect the output:
That yields this filesystem tree:
What you see here is a kind of WebAssembly package that includes not just the executable Wasm binary but also some information about it:
-
The
hello-wasm-dump.txt
file is produced by the wasm-objdump tool. It provides information about our binary, including headers, type definitions, function definitions, and more, which can be used by IDEs, debuggers, and other tools. -
The
hello-wasm.dist
file is produced by the wasm-stats tool. It provides information about the size of sections, functions, and more, which can be used to optimize performance, to debug, and more. -
The
hello-wasm.wat
file is a human-readable textual representation of the binary, built by the wasm2wat. Tools like wat2wasm can use these files to generate Wasm from that textual format and runtimes like Wasmtime can run them directly.
This package is built using the buildRustWasmPackage
function, which wraps the buildRustWasiWasm
function mentioned above.
There are plenty of other things we could add to such a “Wasm package,” but this provides a small taste.
A working CLI
While you can run WebAssembly in the browser, in the cloud, on Kubernetes, and in many other places, an emerging use case is running it as a CLI tool. What makes using Wasm as a CLI tool a bit tricky on a lot of systems is that you need to have a Wasm runtime present on the system to convert WASI-compatible Wasm into system calls.
With Nix, we can directly solve this problem by creating derivations that use a Wasm runtime to run a compiled Wasm binary. Let’s run our compiled binary using the Wasmtime runtime:
Here, I created a buildRustWasmtimeExec
function that creates a wrapper script using makeWrapper
that runs WasmEdge and passes in a path to our compiled Wasm binary (all of this happen in the Nix store, of course).
You can also run the binary using the WasmEdge runtime:
As with WasmTime, I created a buildRustWasmEdgeExec
function that creates a wrapper script that runs WasmEdge and passes in a path to our compiled Wasm binary.
Congrats! You just ran two compiled Wasm binaries on your machine using two separate Wasm runtimes, with everything built deterministically and reproducibly using Nix.
Beyond containers and VMs
This example project is quite unambitious but I hope that it shows that Nix provides a wealth of possibilities in the WebAssembly domain (and other domains like it). Wasm is not trivial to build and package and Nix is a far better tool for it than the usual Makefiles and Bash.
From a deployment perspective, Nix is typically seen as a tool that can build things like OCI containers or artifacts for running virtual machines (like ISOs). But the case of Wasm shows that Nix would be indispensable even in some future world where our industry has gone all-in on Wasm and moved beyond both containers and VMs.