Develop with Zig on Ubuntu

This tutorial shows how to create, build, run, and test a Zig program on Ubuntu. For instructions on how to install Zig and set up editor tooling, see the dedicated guide on How to set up a development environment for Zig on Ubuntu. This article assumes that tooling suggested in that article has been installed.

Creating a Zig project

  1. Create a project directory and change into it:

    dev@ubuntu:~$
    mkdir -p ~/zig/hello && cd ~/zig/hello
  2. Initialize the project:

    dev@ubuntu:~/zig/hello$
    zig init

    zig init creates the following project structure:

    dev@ubuntu:~/zig/hello$
    tree
    .
    ├── build.zig
    ├── build.zig.zon
    └── src
        ├── main.zig
        └── root.zig
    
    • build.zig - the build script, written in Zig itself

    • build.zig.zon - package manifest (name, version, dependencies)

    • src/main.zig - the executable entry point

    • src/root.zig - a library module stub

  3. Inspect the generated src/main.zig:

    src/main.zig
    const std = @import("std");
    
    pub fn main() !void {
        std.debug.print("All your {s} are belong to us.\n", .{"codebase"});
    }
    

    Note

    zig init generates the main.zig file with copious comments, as well as other logic, including example tests and an import of the hello_lib library that was also created; above is just the bare minimum needed for the example program.

    The project would still compile and run if you stripped all the extra code and only left what we’re showing here.

Building and running

  1. Build and run the project using zig build run:

    dev@ubuntu:~/zig/hello$
    zig build run
    All your codebase are belong to us.
    
  2. Replace the message in src/main.zig with ‘Hello, world!’:

    src/main.zig
    const std = @import("std");
    
    pub fn main() !void {
        std.debug.print("Hello, world!\n", .{});
    }
    
  3. Build and run again:

    dev@ubuntu:~/zig/hello$
    zig build run
    Hello, world!
    

Compiling a single file

Use zig build-exe to compile a single source file without a project structure.

  1. Create a standalone source file:

    hello.zig
    const std = @import("std");
    
    pub fn main() !void {
        std.debug.print("Hello, world!\n", .{});
    }
    
  2. Compile it:

    dev@ubuntu:~/zig/hello$
    zig build-exe hello.zig

    This produces an executable named hello in the current directory.

  3. Run the executable:

    dev@ubuntu:~/zig/hello$
    ./hello
    Hello, world!
    

Testing Zig code

Zig has a built-in test runner. Test blocks are declared with the test keyword and use the std.testing namespace for assertions.

The generated src/root.zig already contains an example test.

  1. Review the src/root.zig example test:

    src/root.zig
    const std = @import("std");
    const testing = std.testing;
    
    pub export fn add(a: i32, b: i32) i32 {
        return a + b;
    }
    
    test "basic add functionality" {
        try testing.expect(add(3, 7) == 10);
    }
    
  2. Run the test:

    dev@ubuntu:~/zig/hello$
    zig build --summary all test
    Build Summary: 5/5 steps succeeded; 1/1 tests passed
    test success
    ├─ run test 1 passed 635us MaxRSS:1M
    │  └─ zig test Debug native success 1s MaxRSS:260M
    └─ run test success 191us MaxRSS:1M
       └─ zig test Debug native success 1s MaxRSS:261M

Testing STDOUT output

To test the actual “Hello, world!” function of the program, you can use a special feature of Zig: duck-typing at compile time. In this example, we create a hello() function that accepts any object that provides a .print() method.

This separates the output destination of the function from its logic, which makes it testable. In this way, the function can write to STDOUT, and the test is able to capture the output by directing it somewhere else where it can check it.

Modify the src/main.zig file:

src/main.zig
const std = @import("std");

// Duck-typing the 'writer' parameter
pub fn hello(writer: anytype) !void {
    try writer.print("Hello, world!\n", .{});
}

// Sending output to STDOUT
pub fn main() !void {
    try hello(std.io.getStdOut().writer());
}

// Sending output to an array
test "hello writes 'Hello, world!' to stdout" {
    var output = std.ArrayList(u8).init(std.testing.allocator);
    defer output.deinit();
    try hello(output.writer());
    try std.testing.expectEqualStrings("Hello, world!\n", output.items);
}

Note

defer ensures that output.deinit() (which frees the allocated memory) is called automatically when the test ends (regardless of whether the function succeeds or not).

Cross-compiling with zig cc

Zig includes a built-in C (Clang LLVM) compiler, zig cc, which works as a drop-in replacement for C compilers with first-class cross-compilation support. No separate cross-toolchain installation is required.

Compiling C code natively

  1. Create a C source file:

    hello.c
    #include <stdio.h>
    
    int main(void) {
        printf("Hello, world!\n");
        return 0;
    }
    
  2. Compile and run:

    dev@ubuntu:~/zig/hello$
    zig cc hello.c -o hello && ./hello
    Hello, world!
    

Compiling for other architectures

The zig cc command compiles for any supported target without additional toolchain packages.

  1. Cross-compile for 64-bit ARM Linux (statically linked against the musl libc implementation):

    dev@ubuntu:~/zig/hello$
    zig cc hello.c -target aarch64-linux-musl -o hello-arm64
  2. Verify the binary targets ARM64:

    dev@ubuntu:~/zig/hello$
    file hello-arm64
    hello-arm64: ELF 64-bit LSB executable, ARM aarch64, version 1 (SYSV),
                 statically linked, not stripped
    
  3. Install QEMU user-mode emulation:

    dev@ubuntu:~/zig/hello$
    sudo apt install qemu-user-binfmt

    Ubuntu’s qemu-user-binfmt package registers binfmt_misc handlers automatically, so foreign-architecture binaries run transparently.

  4. Run the Aarch64 executable:

    dev@ubuntu:~/zig/hello$
    ./hello-arm64
    Hello, world!
    

Note

Similarly, cross-compile for 64-bit Windows and run with Wine:

dev@ubuntu:~/zig/hello$
zig cc hello.c -target x86_64-windows-gnu -o hello.exe
dev@ubuntu:~/zig/hello$
sudo apt install wine
dev@ubuntu:~/zig/hello$
wine hello.exe
Hello, world!