Zig is emerging as a compelling alternative for systems programming. It offers C interoperability, manual memory management with safety features, and compile-time execution that enables powerful metaprogramming—all without the complexity of C++ or Rust’s borrow checker.

Why Zig?

Zig targets the same niche as C: operating systems, embedded systems, and performance-critical applications. But it addresses C’s footguns while remaining simple:

  • No hidden control flow: No operator overloading, no hidden allocators
  • Compile-time execution: comptime enables powerful metaprogramming
  • C interop: Drop-in replacement for C, uses C libraries directly
  • Optional safety: Debug builds include bounds checking, use-after-free detection

Hello, Zig

1
2
3
4
5
const std = @import("std");

pub fn main() void {
    std.debug.print("Hello, {s}!\n", .{"World"});
}

Build and run:

1
2
zig build-exe hello.zig
./hello

Manual Memory Management Done Right

Zig requires explicit allocators, making memory behavior transparent:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
const std = @import("std");

pub fn main() !void {
    // Use the general purpose allocator
    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
    defer _ = gpa.deinit();
    const allocator = gpa.allocator();

    // Allocate an array
    var list = std.ArrayList(i32).init(allocator);
    defer list.deinit();

    try list.append(1);
    try list.append(2);
    try list.append(3);

    for (list.items) |item| {
        std.debug.print("{d} ", .{item});
    }
    std.debug.print("\n", .{});
}

The defer keyword ensures cleanup happens—similar to Go’s defer or RAII in C++.

Compile-Time Execution with comptime

Zig’s comptime enables code execution at compile time:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
const std = @import("std");

fn fibonacci(n: u64) u64 {
    if (n <= 1) return n;
    return fibonacci(n - 1) + fibonacci(n - 2);
}

pub fn main() void {
    // Computed at compile time!
    const fib_10 = comptime fibonacci(10);
    std.debug.print("Fibonacci(10) = {d}\n", .{fib_10});
}

This enables:

  • Zero-cost abstractions: Generic code with no runtime overhead
  • Compile-time validation: Check invariants before the program runs
  • Code generation: Generate specialized code based on types

Generic Programming

Generics in Zig use comptime types:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
fn LinkedList(comptime T: type) type {
    return struct {
        const Self = @This();
        
        pub const Node = struct {
            data: T,
            next: ?*Node,
        };

        head: ?*Node,
        allocator: std.mem.Allocator,

        pub fn init(allocator: std.mem.Allocator) Self {
            return .{
                .head = null,
                .allocator = allocator,
            };
        }

        pub fn prepend(self: *Self, data: T) !void {
            const new_node = try self.allocator.create(Node);
            new_node.* = .{
                .data = data,
                .next = self.head,
            };
            self.head = new_node;
        }

        pub fn deinit(self: *Self) void {
            var current = self.head;
            while (current) |node| {
                const next = node.next;
                self.allocator.destroy(node);
                current = next;
            }
        }
    };
}

pub fn main() !void {
    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
    defer _ = gpa.deinit();
    
    var list = LinkedList(i32).init(gpa.allocator());
    defer list.deinit();

    try list.prepend(3);
    try list.prepend(2);
    try list.prepend(1);
}

Error Handling

Zig has explicit error handling without exceptions:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
const std = @import("std");

const FileError = error{
    NotFound,
    PermissionDenied,
    Unknown,
};

fn readConfig(path: []const u8) FileError![]const u8 {
    // Simulate file reading
    if (std.mem.eql(u8, path, "missing.txt")) {
        return FileError.NotFound;
    }
    return "config data";
}

pub fn main() void {
    // Handle with catch
    const config = readConfig("config.txt") catch |err| {
        std.debug.print("Error: {}\n", .{err});
        return;
    };
    
    std.debug.print("Config: {s}\n", .{config});
}

// Or propagate with try
fn loadApp() !void {
    const config = try readConfig("config.txt");
    _ = config;
}

C Interoperability

Zig can directly use C headers:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
const c = @cImport({
    @cInclude("stdio.h");
    @cInclude("stdlib.h");
});

pub fn main() void {
    _ = c.printf("Hello from C!\n");
    
    const ptr = c.malloc(100) orelse {
        c.printf("Allocation failed\n");
        return;
    };
    defer c.free(ptr);
    
    // Use the memory...
}

Build with C libraries:

1
zig build-exe main.zig -lc

Building a Simple HTTP Server

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
const std = @import("std");
const net = std.net;

pub fn main() !void {
    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
    defer _ = gpa.deinit();
    const allocator = gpa.allocator();

    const address = try net.Address.parseIp("127.0.0.1", 8080);
    var server = try address.listen(.{
        .reuse_address = true,
    });
    defer server.deinit();

    std.debug.print("Listening on http://127.0.0.1:8080\n", .{});

    while (true) {
        var conn = try server.accept();
        defer conn.stream.close();

        var buf: [1024]u8 = undefined;
        const n = try conn.stream.read(&buf);
        
        if (n > 0) {
            const response = 
                "HTTP/1.1 200 OK\r\n" ++
                "Content-Type: text/plain\r\n" ++
                "Content-Length: 13\r\n" ++
                "\r\n" ++
                "Hello, Zig!\n";
            
            _ = try conn.stream.write(response);
        }
    }
}

Cross-Compilation

Zig excels at cross-compilation:

1
2
3
4
5
6
7
8
# Compile for Windows from Linux
zig build-exe main.zig -target x86_64-windows

# Compile for ARM Linux (Raspberry Pi)
zig build-exe main.zig -target arm-linux-gnueabihf

# Compile for macOS
zig build-exe main.zig -target x86_64-macos

Zig as a C/C++ Build System

Zig can compile C/C++ code with its built-in toolchain:

1
2
3
4
5
# Compile C code
zig cc -o hello hello.c

# With optimizations and cross-compilation
zig cc -O3 -target aarch64-linux -o hello hello.c

Conclusion

Zig offers a pragmatic approach to systems programming:

  • Simple, readable syntax without hidden complexity
  • Powerful compile-time execution for zero-cost abstractions
  • Seamless C interoperability
  • Excellent cross-compilation support

At Sajima Solutions, we’re exploring Zig for performance-critical components where C’s simplicity is desired but its safety footguns are not. Contact us to discuss your systems programming needs.