Skip to Content
Determinate NixWebAssembly

WebAssembly

Available since Determinate Nix 3.17.0. You can read more about that version's release on our blog.

Determinate Nix includes support for using WebAssembly (Wasm) in Nix expressions via the builtins.wasm function. To use that function, enable it by adding the wasm-builtin feature to the experimental features in your nix.custom.conf file:

/etc/nix/nix.custom.conf
extra-experimental-features = wasm-builtin

Once you’ve done that, you can verify that the function is available:

Check that builtins.wasm is available
nix eval --expr 'builtins.wasm'

You should get «primop wasm» as the output.

To see WebAssembly in action in an example project, see DeterminateSystems/nix-wasm-rust, which provides example functions for the Mandelbrot set, the Fibonacci sequence, YAML parsing, and more.

Use cases

The Nix language is great at describing packages and systems, but it’s not a general-purpose programming language, and it’s not well suited for some tasks. builtins.wasm enables you to write those parts in a language that’s better suited to them, such as Rust, and call them from Nix at evaluation time. Some things you can do with it:

  • Parse file formats that Nix doesn’t support natively. Nix has builtins.fromJSON and builtins.fromTOML but nothing for YAML, INI, XML, and other formats. A Wasm function can use a mature parsing library to turn those files into Nix values, and serialize Nix values back into them. The nix-wasm-rust examples include parsers for YAML and INI.
  • Run computationally heavy logic. Nix evaluation is relatively slow for number crunching, string manipulation, and deeply recursive algorithms. Compiled Wasm runs these much faster, which can make a substantial difference in large evaluations.
  • Analyze source code during evaluation. Wasm functions can read files and directories, so they can inspect your source tree and compute derivation inputs from it. The nix-make example scans C and C++ sources for #include dependencies so that you can build each compilation unit as its own derivation with exactly the headers it needs. The Determinate Nix project includes an experimental build of Nix that uses it.
  • Share logic across projects. A compiled .wasm module is a single portable file that behaves identically on every platform, so you can distribute it in a flake and use it anywhere that Determinate Nix runs.

Calling a Wasm function

builtins.wasm takes two arguments:

  1. An attribute set that tells Nix which module to load (path) and which of its exports to call (function).
  2. The Nix value to pass to that function. This can be any Nix value: an integer, float, Boolean, string, path, null, list, attribute set, or function. To pass several inputs, bundle them into an attribute set.

Here’s a call to the fib function from the nix-wasm-rust examples, which computes a number in the Fibonacci sequence:

fib.nix
builtins.wasm { path = ./nix_wasm_plugin_fib.wasm; function = "fib"; } 40
Evaluate the expression
nix eval --file fib.nix
Output
warning: 'nix_wasm_plugin_fib.wasm' function 'fib': greetings from Wasm! 165580141

The warning comes from the module itself. Wasm functions can emit Nix warnings, which is handy for debugging.

Turn Wasm functions into ordinary Nix functions

Like other builtins, builtins.wasm is curried. If you supply only the first argument, you get back a regular Nix function that you can name, pass around, and call as many times as you like:

let fromINI = builtins.wasm { path = ./wasm/nix_wasm_plugin_ini.wasm; function = "fromINI"; }; in fromINI (builtins.readFile ./app.ini)

This is generally the most convenient way to work with Wasm in practice. You define each function once and the rest of your Nix code doesn’t need to know that it’s backed by WebAssembly.

WASI modules

builtins.wasm can also run modules that target the WebAssembly System Interface (WASI), but only as a compatibility measure. WASI support exists for projects that, for whatever reason, can’t be built as regular non-WASI modules. If you’re writing a new Wasm function, we don’t recommend targeting WASI because WASI modules don’t get the capabilities that WASI normally provides, such as accessing the host filesystem.

If you do need to use WASI, bear in mind that a WASI module has a single entry point, so you omit function when you call it:

builtins.wasm { path = ./wasm/nix_wasm_plugin_fib_wasi.wasm; } 20

Using Wasm functions in a flake

Here’s a more complete example: a flake that reads an INI file using a Wasm parser, uses one of its values, and renders a YAML document using a Wasm serializer. It expects the compiled modules in a wasm directory next to flake.nix:

. ├── app.ini ├── flake.lock ├── flake.nix └── wasm ├── nix_wasm_plugin_ini.wasm └── nix_wasm_plugin_yaml.wasm
app.ini
[server] host = 0.0.0.0 port = 8080
flake.nix
{ outputs = { self }: let fromINI = builtins.wasm { path = ./wasm/nix_wasm_plugin_ini.wasm; function = "fromINI"; }; toYAML = builtins.wasm { path = ./wasm/nix_wasm_plugin_yaml.wasm; function = "toYAML"; }; settings = fromINI (builtins.readFile ./app.ini); in { # Expose the functions so that other flakes can use them lib = { inherit fromINI toYAML; }; # INI values are always strings, so convert the port to an integer port = builtins.fromJSON settings.server.port; # toYAML takes a list of YAML documents yaml = toYAML [ { server = { inherit (settings.server) host; inherit (self) port; }; } ]; }; }
Evaluate the flake outputs
nix eval .#port nix eval --raw .#yaml
Output
8080 --- server: host: 0.0.0.0 port: 8080

A few things to note about this example:

  • Paths are relative to the flake. Flakes evaluate in pure mode, so the modules need to live inside the flake’s source tree (and be tracked by Git). To avoid checking compiled modules into your repository, you can get them from a flake input instead.
  • Wasm functions are just values. The lib output exposes fromINI and toYAML like any other Nix function, so a downstream flake can call inputs.my-flake.lib.fromINI without knowing anything about WebAssembly.
  • Anyone who evaluates the flake needs the feature enabled. That includes CI runners and teammates’ machines. Make sure that wasm-builtin appears in extra-experimental-features everywhere that you evaluate your flake.

Distributing Wasm modules as flakes

Rather than checking compiled .wasm files into every repository that uses them, you can publish them as a flake of their own and use that flake as an input. The nix-wasm-rust project does this for the nix-make dependency scanner. On every push to main, its CI workflow builds the module, puts it in a directory next to a minimal flake.nix, and publishes that directory to FlakeHub as DeterminateSystems/nix-wasm-module-make. The published flake has no inputs and exposes the module as a path:

flake.nix
{ outputs = { self }: { nixWasmModules.nix_make = ./nix_wasm_plugin_nix_make.wasm; }; }

Any flake can then add it as an input and call the module:

flake.nix
{ inputs.nix-wasm-module-make.url = "https://flakehub.com/f/DeterminateSystems/nix-wasm-module-make/0"; outputs = { self, nix-wasm-module-make }: let getDeps = builtins.wasm { path = nix-wasm-module-make.nixWasmModules.nix_make; function = "getDeps"; }; in { # The #include dependencies of every C source file in ./src deps = getDeps { inherit builtins; roots = [ { root = ./src; prefix = ""; } ]; sourceExtensions = [ ".c" ]; includeDirs = [ "" ]; }; }; }

Because the module is a prebuilt file in the input’s source, Nix fetches it like any other flake input and doesn’t need to build anything before it can call it. Your flake also gets the usual benefits of flake inputs: the module version is pinned in flake.lock, and you can upgrade it with nix flake update.

Writing your own Wasm functions

You can write a Wasm function in any language that compiles to WebAssembly, as long as the module follows the interface described in the builtins.wasm reference. In Rust, the nix-wasm-rust crate handles the details for you. Here’s the full source of the fib function from earlier:

src/lib.rs
use nix_wasm_rust::{warn, Value}; #[no_mangle] pub extern "C" fn fib(arg: Value) -> Value { warn!("greetings from Wasm!"); fn fib2(n: i64) -> i64 { if n < 2 { 1 } else { fib2(n - 1) + fib2(n - 2) } } Value::make_int(fib2(arg.get_int())) }

The exported function name (fib) is the name you pass as function in builtins.wasm. Value is a handle to a Nix value, with methods for reading Nix values (get_int, get_string, and others) and constructing new ones (make_int, make_string, make_attrset, and others). Compile the crate for the wasm32-unknown-unknown target to produce a module that you can call from Nix.

Last updated on