background grid image
Image for post nix-wasm
Jun 13, 2024 by Luc Perkins

Nix as a WebAssembly build tool

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.

Build hello-wasm and inspect the result
nix build "https://flakehub.com/f/DeterminateSystems/nix-wasm-example/*.tar.gz#hello-wasm"
ls ./result/lib

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:

Build hello-wasm-pkg and inspect the result
nix build "https://flakehub.com/f/DeterminateSystems/nix-wasm-example/*.tar.gz#hello-wasm-pkg"
tree result

That yields this filesystem tree:

Filesystem tree for ./result
result
├── lib
└── hello-wasm.wasm
└── share
├── hello-wasm-dump.txt
├── hello-wasm.dist
└── hello-wasm.wat
3 directories, 4 files

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:

Run the compiled binary using Wasmtime
nix run "https://flakehub.com/f/DeterminateSystems/nix-wasm-example/*.tar.gz#hello-wasmtime-exec"

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:

Run the compiled binary using WasmEdge
nix run "https://flakehub.com/f/DeterminateSystems/nix-wasm-example/*.tar.gz#hello-wasmedge-exec"

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.


Share
Avatar for Luc Perkins
Written by Luc Perkins

Luc is a technical writer, software engineer, and Nix advocate who's always on the lookout for qualitatively better ways of building software. He originally hails from the Pacific Northwest but has recently taken to living abroad.