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¶
Create a project directory and change into it:
dev@ubuntu:~$mkdir -p ~/zig/hello && cd ~/zig/helloInitialize the project:
dev@ubuntu:~/zig/hello$zig initzig initcreates the following project structure:dev@ubuntu:~/zig/hello$tree. ├── build.zig ├── build.zig.zon └── src ├── main.zig └── root.zigbuild.zig- the build script, written in Zig itselfbuild.zig.zon- package manifest (name, version, dependencies)src/main.zig- the executable entry pointsrc/root.zig- a library module stub
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 initgenerates themain.zigfile with copious comments, as well as other logic, including example tests and an import of thehello_liblibrary 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¶
Build and run the project using
zig build run:dev@ubuntu:~/zig/hello$zig build runAll your codebase are belong to us.
Replace the message in
src/main.zigwith ‘Hello, world!’:src/main.zig¶const std = @import("std"); pub fn main() !void { std.debug.print("Hello, world!\n", .{}); }
Build and run again:
dev@ubuntu:~/zig/hello$zig build runHello, world!
Compiling a single file¶
Use zig build-exe to compile a single source file without a project structure.
Create a standalone source file:
hello.zig¶const std = @import("std"); pub fn main() !void { std.debug.print("Hello, world!\n", .{}); }
Compile it:
dev@ubuntu:~/zig/hello$zig build-exe hello.zigThis produces an executable named
helloin the current directory.Run the executable:
dev@ubuntu:~/zig/hello$./helloHello, 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.
Review the
src/root.zigexample 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); }
Run the test:
dev@ubuntu:~/zig/hello$zig build --summary all testBuild 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¶
Create a C source file:
hello.c¶#include <stdio.h> int main(void) { printf("Hello, world!\n"); return 0; }
Compile and run:
dev@ubuntu:~/zig/hello$zig cc hello.c -o hello && ./helloHello, world!
Compiling for other architectures¶
The zig cc command compiles for any supported target without additional toolchain packages.
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-arm64Verify the binary targets ARM64:
dev@ubuntu:~/zig/hello$file hello-arm64hello-arm64: ELF 64-bit LSB executable, ARM aarch64, version 1 (SYSV), statically linked, not strippedInstall QEMU user-mode emulation:
dev@ubuntu:~/zig/hello$sudo apt install qemu-user-binfmtUbuntu’s
qemu-user-binfmtpackage registersbinfmt_mischandlers automatically, so foreign-architecture binaries run transparently.Run the Aarch64 executable:
dev@ubuntu:~/zig/hello$./hello-arm64Hello, 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!