Back to demos

A simple WASM app

This website is served as static files. JavaScript and Zig compiled to WebAssembly handle the interactive parts entirely in your browser, with no server-side application logic. This demo shows how the two languages work together!

For this demo, I chose to execute the simplest Zig function: an function that adds two numbers.

Try it!

Enter two integers from 0 to 1000 to add together.

Loading WebAssembly...

How it works

  1. Zig defines an add function.
  2. The Zig compiler turns it into a .wasm module.
  3. JavaScript loads that module, validates the form inputs, and calls the exported function.

Of course, adding two numbers does not need WebAssembly. I'm doing this to show the basic principle behind interfacing Zig and javascript.

The Zig source

This is the complete add.zig file. Simple right?

export fn add(a: i32, b: i32) i32 {
    return a + b;
}

The export keyword exposes the function to javascript. Both arguments and the result are signed 32-bit integers (i32).

Building the WebAssembly module

The module is built with a simple script calling the zig compiler. Here is the complete script:

mkdir -p public

zig build-exe add.zig \
  -target wasm32-freestanding \
  -fno-entry \
  --export=add \
  -Osafe \
  -femit-bin=public/add.wasm

You can download the compiled module. It's simply a binary file

The browser code

The page loads app.js as a JavaScript module.

WebAssembly.instantiate compiles and instantiates the WebAssembly module. First, we retrieve its exported Zig function:

const add = instance.exports.add;

Then, inside the form's submit handler, this line calls it with the validated inputs and displays the result:

result.textContent = `${a} + ${b} = ${add(a, b)}`;

The add(a, b) call looks like an ordinary JavaScript function call, but the addition runs inside the WebAssembly module compiled from our Zig code.

Here is the complete browser code, including loading the module, validating the inputs, and updating the form's result:

const result = document.querySelector("#result");
const button = document.querySelector("#add");

try {
  const response = await fetch(new URL("./add.wasm", import.meta.url));
  if (!response.ok) throw new Error("WASM download failed");

  const { instance } = await WebAssembly.instantiate(
    await response.arrayBuffer(),
    {}
  );

  const add = instance.exports.add;

  document.querySelector("#calculator").onsubmit = event => {
    event.preventDefault();

    const a = document.querySelector("#a").valueAsNumber;
    const b = document.querySelector("#b").valueAsNumber;

    if (![a, b].every(n =>
      Number.isInteger(n) && n >= 0 && n <= 1000
    )) {
      result.textContent = "Enter integers from 0 to 1000.";
      return;
    }

    result.textContent = `${a} + ${b} = ${add(a, b)}`;
  };

  button.disabled = false;
  result.textContent = "Ready.";
} catch (error) {
  result.textContent = "Failed to load WebAssembly.";
  console.error(error);
}

The form supplies the calculator, a, b, and add element IDs. And when the user submits it, the JavaScript code calls the add function we wrote in Zig and compiled to WebAssembly, before displaying the result. How neat is that?

Final words

This could seem like a lot of work to simply add two numbers. But with this setup, you can call any Zig function from JavaScript. Think about it, any Zig function!

For example, you could write a Zig function to:

(Okay, not any function. I lied. Of course, the code must compile for WebAssembly and expose an interface JavaScript can call. For instance, strings and pixel buffers will require some extra glue through the module's memory, and code that relies on operating-system APIs needs adaptation for the browser. But hey, if that's what it takes to write more Zig and less JavaScript, I take it anyday!)

And that's it!