Introduction

Beginning with 0.4.0 Zig can generate %.wasm files instead of architecture-specific binaries through three targets:

  • wasm32-emscripten: mostly for browser (JavaScript) use.
  • wasm32-freestanding: for standalone use in or outside the browser.
  • wasm32-wasi: for use outside the browser.

This document is maintained by wazero, which is a WebAssembly runtime that embeds in Go applications. Hence, our notes focus on targets used outside the browser, tested by wazero: wasm32-freestanding and wasm32-wasi.

Overview

When Zig compiles a %.zig file with a wasm32-* target, the output %.wasm depends on a subset of features in the WebAssembly 2.0 Core specification and WASI host functions.

Unlike some compilers, Zig also supports importing custom host functions and exporting functions back to the host.

Here’s a basic example of source in Zig:

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

The following is the minimal command to build a Wasm file.

zig build-lib -dynamic -target wasm32-freestanding main.zig

The resulting Wasm exports the add function so that the embedding host can call it, regardless of if the host is written in Zig or not.

Notice we are using zig build-lib -dynamic: this compiles the source as a library, i.e. without a main function.

Disclaimer

This document includes notes contributed by the wazero community for Zig 0.10.1. While wazero includes Zig examples, and maintainers contribute to Zig, this isn’t a Zig official document. For more help, consider the WebAssembly Documentation or joining the #programming-discussion channel on Zig’s Discord.

Meanwhile, please help us maintain this document and star our GitHub repository, if it is helpful. Together, we can make WebAssembly easier on the next person.

Constraints

Please read our overview of WebAssembly and constraints. In short, expect limitations in both language features and library choices when developing your software.

Memory

The Zig language performs no memory management on behalf of the programmer. However, Zig has no default allocator. Instead, functions which need to allocate accept an Allocator parameter.

Host Allocations

Sometimes a host function needs to allocate memory directly. For example, to write JSON of a given length before invoking an exported function to parse it.

pub export fn configure(ptr: [*]const u8, size: u32) void {
    _configure(message[0..size]) catch |err| @panic(switch (err) {
        error.OutOfMemory => "out of memory",
    });
}

The general flow is that the host allocates memory by calling an allocation function with the size needed. Then, it writes data, in this case JSON, to the memory offset (ptr). At that point, it can call a host function, ex configure, passing the ptr and size allocated. The guest Wasm (compiled from Zig) will be able to read the data. To ensure no memory leaks, the host calls a free function, with the same ptr, afterwards and unconditionally.

Note: wazero includes an example project that shows this.

The zig example does a few things of interest:

  • Uses @ptrToInt to change a Zig pointer to a numeric type
  • Uses [*]u8 as an argument to take a pointer and slices it to build back a string
  • It also shows how to import a host function using the extern directive

To allow the host to allocate memory, you need to define your own malloc and free functions:

(func (export "malloc") (param $size i32) (result (;$ptr;) i32))
(func (export "free") (param $ptr i32) (param $size i32))

Because Zig easily allows end-users to [plug their own allocators][12], it relatively easy to export custom malloc/free pairs to the host.

For instance, the following code exports malloc, free from Zig’s page_allocator:

const allocator = std.heap.page_allocator;

pub export fn malloc(length: usize) ?[*]u8 {
    const buff = allocator.alloc(u8, length) catch return null;
    return buff.ptr;
}

pub export fn free(buf: [*]u8, length: usize) void {
    allocator.free(buf[0..length]);
}

System Calls

Please read our overview of WebAssembly and System Calls. In short, WebAssembly is a stack-based virtual machine specification, so operates at a lower level than an operating system.

For functionality the operating system would otherwise provide, you must use the wasm32-wasi target. This imports host functions in WASI.

Zig’s standard library support for WASI is under active development. In general, you should favor use of the standard library when compiling against wasm32-wasi target (e.g. std.io).

Note: wazero includes an example WASI project including source code that implements cat without any WebAssembly-specific code.

Concurrency

Please read our overview of WebAssembly and concurrency. In short, the current WebAssembly specification does not support parallel processing.

Optimizations

Below are some commonly used configurations that allow optimizing for size or performance vs defaults. Note that sometimes one sacrifices the other.

Binary size

Those with %.wasm binary size constraints can change their source, e.g. picking a different allocator or set zig flags to reduce it.

zig flags: Zig provides several flags to control binary size, speed of execution, safety checks. For instance you may use

  • -ODebug: Fast build, enabled safety checks, slower runtime performance, larger binary size
  • -OReleaseSafe: Medium runtime performance, enabled safety checks, slower compilation speed, larger binary size
  • -OReleaseSmall: Medium runtime performance, disabled safety checks, slower compilation speed, smaller binary size

Performance

Those with runtime performance constraints can change their source or set zig flags to improve it.

zig flags:

  • -OReleaseFast: Enable additional optimizations, possibly at the cost of increased binary size.

Frequently Asked Questions

Why is my %.wasm binary so big?

Zig defaults can be overridden for those who can sacrifice features or performance for a smaller binary. After that, tuning your source code may reduce binary size further.