Zig Common Tasks

This website is dedicated to helping programmers perform common tasks in Zig.

It is not a tutorial and should be used mostly as a reference.

Known Hosts

Reading the samples

Notice that it’s implicit in many code samples:

All samples are tested on MacOS, Windows and Linux/Ubuntu using the Zig version selected in the selector in the top-right corner of this page.

Contributing

Read instructions on this project repository’s README on GitHub or OpenCode.

Index

Code samples

The Zig Standard Library is currently in flux. Effort is taken to keep all samples up-to-date, but changes are to be expected until it stabilizes after Zig 1.0.

Boolean logic (if expression, equality)

test "Basic boolean operators" {
    // Zig uses `and` and `or`, unlike many languages that use `&&` and `||`
    try std.testing.expect(true and true);
    try std.testing.expect(false or true);
    // but negation is still done using `!`
    try std.testing.expect(!true or true);
}

test "If expressions" {
    const x = true;
    // Zig does not have the ternary operator (`x ? 1 : 0`), but it has `if` expressions!
    try std.testing.expectEqual(42, if (x) 42 else 0);
}

test "Equality" {
    // testing equality with `==` or `!=` works for numeric types, booleans and types
    try std.testing.expect(2 == 1 + 1);
    try std.testing.expect(false == (2 == 3 + 1));
    try std.testing.expect(@TypeOf(true) == bool);

    // but it doesn't work for other types, such as strings (or slices)
    const array1 = [_]u8{ 1, 2, 3 };
    var array2 = [_]u8{ 3, 2, 1 };
    std.mem.sort(u8, &array2, {}, std.sort.asc(u8));

    // the pointers are NOT the same (comparing arrays would not even compile)
    try std.testing.expect(&array1 != &array2);

    // even if the contents are the same
    try std.testing.expect(std.mem.eql(u8, &array1, &array2));
}


C Strings

// WARNING: @cImport will eventually go away.
//          https://github.com/ziglang/zig/issues/20630
const c = @cImport({
    // normally you would include headers or even c files, like this:
    //@cInclude("test.c");
    // but for this sample, C code is directly defined here
    @cDefine("MY_C_CODE", "()\\const char* give_str() { return \\Hello C\\; }");
});

// use mem.span to convert strings to slices
const span = std.mem.span;

test "Handling C strings" {
    // cstr has type '[*c]const u8' here, but can be coerced to a Zig pointer
    const cstr = c.give_str();
    // notice how Zig String literals are also 0-terminated
    const hz: [*:0]const u8 = "Hello Zig";
    const hc: [*:0]const u8 = "Hello C";

    // calling "span" turns sentinel-terminated values into slices
    const hzSlice: []const u8 = span(hz);

    try expect(!std.mem.eql(u8, span(cstr), hzSlice));
    try expect(std.mem.eql(u8, span(cstr), span(hc)));
}



Check for null (using nullable value)

test "Use value orelse a default" {
    const byte: ?u8 = null;
    const n: u8 = byte orelse @as(u8, 0);
    try std.testing.expectEqual(n, @as(u8, 0));
}

test "Check for null using if statement" {
    const byte: ?u8 = 10;
    var n: u8 = 2;
    if (byte) |b| {
        // b is non-null here
        n += b;
    }
    try std.testing.expectEqual(n, @as(u8, 12));
}

test "Check for null using if expression" {
    const byte: ?u8 = null;
    const n: u8 = if (byte) |b| b + 1 else 2;
    try std.testing.expectEqual(n, @as(u8, 2));
} // 

Notes:


Create allocators

test "Get the test allocator" {
    const alloc = std.testing.allocator;
    _ = alloc; // use allocator
}

test "Create a general-purpose allocator" {
    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
    // call deinit to free it if necessary
    defer _ = gpa.deinit();
    _ = gpa.allocator(); // use allocator
}

test "Get system-native page allocator" {
    const alloc: std.mem.Allocator = std.heap.page_allocator;
    _ = alloc; // use allocator
}

test "Create a fixed buffer allocator" {
    const alloc: std.mem.Allocator = init: {
        // use an array as the "heap"
        var buffer: [1024]u8 = undefined;
        var fba = std.heap.FixedBufferAllocator.init(&buffer);
        break :init fba.allocator();
    };
    _ = alloc; // use allocator
}

test "Get an Arena allocator" {
    const alloc = std.heap.ArenaAllocator.init(std.heap.page_allocator);
    // the arena allocator will free everything allocated through it
    // when it is de-initialized.
    defer alloc.deinit();
}



Get command line arguments

test "argsWithAllocator - get an iterator, use an allocator" {
    var args = try std.process.argsWithAllocator(alloc);
    defer args.deinit();
    while (args.next()) |arg| {
        _ = arg; // use arg
    }
}

test "argsAlloc - get a slice, use an allocator" {
    const args = try std.process.argsAlloc(alloc);
    defer std.process.argsFree(alloc, args);
    for (args) |arg| {
        _ = arg; // use arg
    }
}

test "args - get an iterator, no allocation but not fully portable" {
    const builtin = @import("builtin");
    var args =
        if (builtin.os.tag == .windows or builtin.os.tag == .wasi)
        // this sample does not work on Windows and WASI
        return
    else
        // Linux, MacOS etc. can use the simpler args() method:
        std.process.args();

    while (args.next()) |arg| {
        _ = arg; // use arg
    }
} //

Notes:


Get environment variables

test "Get a single environment variable" {
    const path = try std.process.getEnvVarOwned(alloc, "PATH");
    defer alloc.free(path);
    try std.testing.expect(path.len > 0);
}

test "Get all environment variables" {
    var env = try std.process.getEnvMap(alloc);
    defer env.deinit();
    try std.testing.expect(env.count() > 0);
} //

HTTP Server and Client

const http = std.http;
const alloc = std.testing.allocator;

fn server_starter() !void {
    const server_address = try std.net.Address.resolveIp("127.0.0.1", 8080);
    var server = try server_address.listen(.{ .reuse_address = true });

    // This would normally be an infinite loop!
    // But for testing, we only handle a single request and stop.
    const client_con = server.accept() catch |err| {
        std.log.err("Unable to accept client connection: {s}", .{@errorName(err)});
        return;
    };
    handle_connection(client_con) catch |err| {
        std.log.err("Failed to handle request: {s}", .{@errorName(err)});
        return;
    };
}

/// Handles connection by always responding with a hardcoded response (ok for GET, method_not_allowed otherwise).
/// Normally you would inspect the request and spawn a new Thread (or use async, coming soon to Zig!).
fn handle_connection(client_con: std.net.Server.Connection) !void {
    defer client_con.stream.close();
    const read_buffer = try alloc.alloc(u8, 8 * 1024);
    defer alloc.free(read_buffer);
    const write_buffer = try alloc.alloc(u8, 8 * 1024);
    defer alloc.free(write_buffer);
    var con_reader = client_con.stream.reader(read_buffer);
    var con_writer = client_con.stream.writer(write_buffer);
    var server = http.Server.init(con_reader.interface(), &con_writer.interface);
    var req = try server.receiveHead();
    if (req.head.method == .GET) {
        try req.respond("Hello\\", .{ .extra_headers = &.{.{ .name = "Content-Type", .value = "text/plain" }} });
    } else {
        try req.respond("", .{ .status = .method_not_allowed });
    }
}

test "HTTP Server and HTTP Client" {
    // The Server needs to run on a separate Thread.
    const server_thread = try std.Thread.spawn(.{ .allocator = alloc }, server_starter, .{});
    defer server_thread.join();

    // Create a HTTP client that will send a single request to the Server.
    var client = http.Client{ .allocator = alloc };
    defer client.deinit();

    const uri = try std.Uri.parse("http://localhost:8080/hello");

    // create a GET request with some headers
    var req = try client.request(http.Method.GET, uri, .{ .headers = .{
        .user_agent = .{ .override = "zig-example-http-client" },
    }, .extra_headers = &.{.{ .name = "Accept", .value = "text/plain" }} });
    defer req.deinit();

    // send it!
    try req.sendBodiless();

    // receive the request header
    var response = try req.receiveHead(&.{});

    // first, make sure the status code was 200 OK
    try std.testing.expectEqual(.ok, response.head.status);

    // we can iterate over the response headers
    var resp_headers = response.head.iterateHeaders();
    var content_type_found = false;
    var content_length: usize = 0;
    while (resp_headers.next()) |header| {
        if (std.ascii.eqlIgnoreCase(header.name, "Content-Length")) {
            content_length = try std.fmt.parseUnsigned(usize, header.value, 10);
        } else if (std.ascii.eqlIgnoreCase(header.name, "Content-Type")) {
            content_type_found = true;
            try std.testing.expectEqualSlices(u8, header.value, "text/plain");
        }
    }
    try std.testing.expectEqual(@as(usize, 6), content_length);
    try std.testing.expect(content_type_found);

    // read the full response body
    const body = try alloc.alloc(u8, content_length);
    defer alloc.free(body);
    var body_reader = response.reader(body);
    try body_reader.fill(content_length);

    // assert the response body is as expected
    try std.testing.expectEqual(6, body.len);
    const expected_body = "Hello\\";
    try std.testing.expectEqualSlices(u8, expected_body, body);
}


JSON - Parse object

test "Parse JSON object" {
    const example_json: []const u8 =
        \\{"a_number": 10, "a_str": "hello"}
    ;
    const JsonStruct = struct {
        a_number: u32,
        a_str: []const u8
    };

    const parsed = try std.json.parseFromSlice(JsonStruct, alloc, example_json, .{});
    defer parsed.deinit();

    const result = parsed.value;
    try std.testing.expectEqual(@as(u32, 10), result.a_number);
    try std.testing.expectEqualSlices(u8, "hello", result.a_str);
}


JSON - Write object

test "Write JSON object" {
    const example_json: []const u8 =
        \\{"a_number":10,"a_str":"hello"}
    ;

    var buffer: [256]u8 = undefined;
    var writer = std.Io.Writer.fixed(&buffer);

    try std.json.Stringify.value(.{
        .a_number = @as(u32, 10),
        .a_str = "hello",
    }, .{}, &writer);

    try std.testing.expectEqualSlices(u8, example_json, buffer[0..writer.end]);
}


List directory contents

test "List contents of directory" {
    var children = std.fs.cwd().openDir("source", .{ .iterate = true }) catch {
        // couldn't open dir
        return;
    };
    defer children.close();
    // use the returned iterator to iterate over dir contents
    _ = children.iterate();
} // 

Loops

test "Simple for-each loop" {
    const items = [_]i32{ 1, 2, 3, 4 };
    var sum: i32 = 0;
    for (items) |item| {
        sum += item;
    }
    try std.testing.expectEqual(sum, 1 + 2 + 3 + 4);
}

test "Looping with indexes" {
    var items: [4]i32 = undefined;

    // the first item is the range being iterated over, the second gives the index
    for (4..8, 0..) |value, index| {
        items[index] = @as(i32, @intCast(value));
    }
    // notice that the range end is excluded
    const expected = [_]i32{ 4, 5, 6, 7 };
    try std.testing.expectEqualSlices(i32, &expected, &items);
}

test "For expression" {
    const x = for (1..10) |i| {
        if (i == 10) {
            break "too big"; // if break is called within the for loop, this is the result
        }
    } else els: { // if break is not called, the else block gives the result
        break :els "ok";
    };
    // if the range was 1..11, the x would be "too big"!
    try std.testing.expectEqualSlices(u8, "ok", x);
}

test "Simple while loop" {
    var done = false;
    // as in most other languages, "while" can just take a boolean variable
    while (!done) {
        done = true;
    }
    try std.testing.expect(done);
}

test "While not null" {
    // creating an iterable-like struct for this example
    const Items = struct {
        items: []i32,
        index: usize = 0,
        pub fn nextItem(self: *@This()) ?i32 {
            if (self.index >= self.items.len) {
                return null;
            }
            defer self.index += 1;
            return self.items[self.index];
        }
    };

    var items_array = [_]i32{ 1, 2, 3 };
    var items: Items = .{ .items = &items_array };

    var sum_before_null: i32 = 0;
    var has_null = false;

    // loop while the iterable does not return null
    while (items.nextItem()) |item| {
        sum_before_null += item;
    } else { // when a null is found, the else branch runs and the loop exits
        has_null = true;
    }
    try std.testing.expect(has_null);
    try std.testing.expectEqual(1 + 2 + 3, sum_before_null);
}

test "While not error" {
    // creating a struct for this example
    const Positive = struct {
        value: u32,

        pub fn decrement(self: *@This()) void {
            if (self.value > 0) self.value -= 1;
        }

        pub fn get(self: *@This()) anyerror!u32 {
            if (self.value == 0) return error.ZeroValue;
            return self.value;
        }
    };

    var positive: Positive = .{ .value = 2 };
    var sum: u32 = 0;

    // while no error, keep looping... decrement the value after each iteration.
    while (positive.get()) |value| : (positive.decrement()) {
        sum += value;
    } else |err| { // the optional else branch can capture the final error
        std.debug.assert(err == error.ZeroValue);
    }

    try std.testing.expectEqual(3, sum);
}


Memory Safety

test "Stack allocated data" {
    const Example = struct {
        fn createDataOnTheStack() anyerror![]u8 {
            // unless using allocators, data is allocated on the stack, not heap
            var data = [_]u8{ 1, 2 };

            // data on the stack can only be used while in the scope of the function
            try std.testing.expectEqual(1, data[0]); // OK

            // very, very bad
            return &data;
        }
    };

    // using the returned value here would be unchecked Illegal Behavior,
    // see: https://ziglang.org/documentation/master/#toc-Illegal-Behavior
    _ = try Example.createDataOnTheStack();
}

test "Allocator-owned data" {
    const Example = struct {
        // in Zig, it is good practice to always take an allocator as an argument
        // in any function that needs to allocate memory outside the stack.
        fn createDataWithAllocator(allocator: std.mem.Allocator) anyerror![]u8 {
            const local_data = [_]u8{ 1, 2 };
            const data = try allocator.alloc(u8, 2);
            @memcpy(data, &local_data);
            return data; // OK, data is not allocated on the stack!
        }
    };

    const alloc = std.testing.allocator;

    const data = try Example.createDataWithAllocator(alloc);

    // Allocator-allocated data must be freed using the same allocator!
    // In tests, use `std.testing.allocator` and Zig will detect memory leaks and other errors.
    // In Debug mode, you will get warnings or errors when using `std.heapGeneralPurposeAllocator`
    // in cases like double-free, use-after-free and memory leaks.
    defer alloc.free(data);

    try std.testing.expectEqual(1, data[0]); // OK!

    // Zig does bounds-checking in safe mode (it panics on error).
    // To opt-out, use `-O ReleaseFast`
    try std.testing.expectEqual(2, data[1]); // OK!
}


Print something to stdout/stderr

pub fn hello_world() !void {
    // to print something for debugging purposes, there's an easy way!
    std.debug.print("Hello world!\\", .{});

    // but on real apps, you will want to use the "real" stdout/stderr

    var buffer: [1024]u8 = undefined;

    // Choose File.stdout() or File.stderr()
    // Note: don't use stdout() in tests, it will block your test on "zig build test"!
    var writer = std.fs.File.stderr().writer(&buffer);
    const stderr = &writer.interface;

    try stderr.print("Hello world!\\", .{});

    // you can provide arguments, similar to C's printf,
    // though in Zig the format is checked at compile-time!
    try stderr.print("number: {d}, string: {s}\\", .{ 42, "fourty-two" });

    try stderr.flush(); // Don't forget to flush!
} // 

Process - execute

test "Execute a process (inherit stdout and stderr)" {
    // the command to run
    const argv = [_][]const u8{ "ls", "./" };

    // init a ChildProcess... cleanup is done by calling wait().
    var proc = std.process.Child.init(&argv, alloc);

    // ignore the streams to avoid the zig build blocking...
    // REMOVE THESE IF YOU ACTUALLY WANT TO INHERIT THE STREAMS.
    proc.stdin_behavior = .Ignore;
    proc.stdout_behavior = .Ignore;
    proc.stderr_behavior = .Ignore;

    try proc.spawn();

    //std.debug.print("Spawned process PID={}\\", .{proc.id});

    // the process only ends after this call returns.
    const term = try proc.wait();

    // term can be .Exited, .Signal, .Stopped, .Unknown
    try std.testing.expectEqual(term, std.process.Child.Term{ .Exited = 0 });
}

test "Execute a process (consume stdout and stderr into allocated memory)" {
    // the command to run
    const argv = [_][]const u8{ "ls", "./" };

    const proc = try std.process.Child.run(.{
        .argv = &argv,
        .allocator = alloc,
    });

    // on success, we own the output streams
    defer alloc.free(proc.stdout);
    defer alloc.free(proc.stderr);

    const term = proc.term;

    try std.testing.expectEqual(std.process.Child.Term{ .Exited = 0 }, term);
    try std.testing.expect(proc.stdout.len != 0);
    try std.testing.expectEqual(proc.stderr.len, 0);

}


Read file bytes

test "Read bytes from a file" {
    // current working directory
    const cwd = std.fs.cwd();

    const handle = cwd.openFile("hello.txt",
    // this is the default, so could be just '.{}'
    .{ .mode = .read_only }) catch {
        // file not found
        return;
    };
    defer handle.close();

    // read into this buffer
    var buffer: [64]u8 = undefined;
    const bytes_read = handle.readAll(&buffer) catch unreachable;

    // if bytes_read is smaller than buffer.len, then EOF was reached
    try std.testing.expectEqual(@as(usize, 6), bytes_read);

    const expected_bytes = [_]u8{ 'h', 'e', 'l', 'l', 'o', '\\' };
    try std.testing.expectEqualSlices(u8, &expected_bytes, buffer[0..bytes_read]);
}


Read file line by line

test "Read file one line at a time" {
    const max_bytes_per_line = 4096;
    var file = std.fs.cwd().openFile("my-file.txt", .{}) catch {
        // couldn't open file
        return;
    };
    defer file.close();

    var read_buffer: [max_bytes_per_line]u8 = undefined;
    var reader = file.readerStreaming(&read_buffer).interface;

    while (try reader.takeDelimiter('\\')) |line| {
        // use line
        _ = line;
    }
} // 

Read user input from command line

pub fn main() !void {
    const stdin = std.io.getStdIn().reader();
    const maybe_input = try stdin.readUntilDelimiterOrEofAlloc(alloc, '\\', max_line_size);
    if (maybe_input) |input| {
        defer alloc.free(input);
        // use input
    }
} // 

Socket - accept TCP connections

fn localhostListener(port: u16) !std.net.Server {
    const localhost = try std.net.Address.resolveIp("0.0.0.0", port);
    return localhost.listen(.{});
}

pub fn main() !void {
    var listener = try localhostListener(8081);
    defer listener.deinit();

    const connection = try listener.accept();
    defer connection.stream.close();

    std.log.info("Accepted connection from {}", .{connection.address});
    try printMessage(&connection.stream.reader());
} // 

Notes:


Socket - send data

pub fn main() !void {
    const remote = try std.net.Address.resolveIp("0.0.0.0", 8081);
    var remote_stream = try std.net.tcpConnectToAddress(remote);
    defer remote_stream.close();
    try remote_stream.writer().writeAll("hello from Zig\\");
} // 

Notes:


String - check if it contains another

test "Check if a string contains another" {
    const to_find = "string";
    const contains = std.mem.containsAtLeast(u8, "big string", 1, to_find);
    try std.testing.expect(contains);
} // 

String - compare two strings

test "Check if two strings are equal ignoring case (ASCII only)" {
    const are_equal = std.ascii.eqlIgnoreCase("hEllO", "Hello");
    try std.testing.expect(are_equal);
}

test "Check if two strings are exactly equal" {
    const are_equal = std.mem.eql(u8, "hello", "hello");
    try std.testing.expect(are_equal);
} // 

String - convert to number

test "Convert string to number" {
    // base-10 signed integer
    const decimal = try std.fmt.parseInt(i32, "42", 10);
    try std.testing.expectEqual(@as(i32, 42), decimal);

    // floating-point number
    const float = try std.fmt.parseFloat(f64, "3.1415");
    try std.testing.expectEqual(@as(f64, 3.1415), float);

    // base-2 unsigned integer
    const byte = try std.fmt.parseUnsigned(u8, "1_0101", 2);
    try std.testing.expectEqual(@as(u8, 21), byte);
} // 

String - iterate over UTF-8 code points

test "Iterate over utf-8 string code points" {
    // reference: https://emojipedia.org/woman-astronaut/
    var utf8_str = (try std.unicode.Utf8View.init("👩‍🚀")).iterator();
    try std.testing.expectEqual(@as(?u21, 0x1F469), utf8_str.nextCodepoint());
    try std.testing.expectEqual(@as(?u21, 0x200D), utf8_str.nextCodepoint());
    try std.testing.expectEqual(@as(?u21, 0x1F680), utf8_str.nextCodepoint());
    try std.testing.expectEqual(@as(?u21, null), utf8_str.nextCodepoint());
} // 

Threads and channels

const Val = std.atomic.Value(u32);
const Order = std.builtin.AtomicOrder;

fn toRunInThread(v: *Val) void {
    const value = v.load(Order.acquire);
    v.store(value + 1, Order.release);
}

test "Pass an atomic value to a Thread and wait for it to be modified" {
    const allocator = std.heap.page_allocator;

    // an atomic value to be updated by another Thread
    var value = Val.init(42);

    const thread = try std.Thread.spawn(.{
        // optional config
        .allocator = allocator,
        .stack_size = 1024,
    }, toRunInThread, .{&value});

    // must either join() or detach()
    thread.join();

    try expectEqual(@as(u32, 43), value.load(Order.unordered));
} // 

Write to file

test "Write bytes to a file (create if necessary, append or replace contents)" {
    // current working directory
    const cwd = std.fs.cwd();

    const file_path = "hello.txt";

    const handle = try cwd.createFile(file_path, .{
        // set to true to fully replace the contents of the file
        .truncate = false,
    });
    defer handle.close();
    defer cwd.deleteFile(file_path) catch unreachable;

    // go to the end of the file if you want to append to the file
    // and `truncate` was set to `false` (otherwise this is not needed)
    try handle.seekFromEnd(0);

    // write bytes to the file
    const bytes_written = try handle.write("hello Zig\\");

    try std.testing.expectEqual(@as(usize, 10), bytes_written);
}


void pointers

const c_code =
    \\
    \\static const void* _value;
    \\void store_void(const void* v) { _value = v; }
    \\const void* get_void() { return _value; }
;

// WARNING: @cImport will eventually go away.
//          https://github.com/ziglang/zig/issues/20630
const c = @cImport({
    // normally you would include headers or even c files, like this:
    //@cInclude("test.c");
    // but for this sample, C code is directly defined here
    @cDefine("MY_C_CODE", c_code);
});

test "deal with void pointer from C" {
    // send some value to be stored in C as a void* pointer
    c.store_void(@as(?*const anyopaque, &@as(u32, 10)));

    // retrieve the value which now is "untyped", or opaque
    const untyped_value: ?*const anyopaque = c.get_void();

    // to restore the type, we must cast it to the known type explicitly
    const value: *const u32 = @ptrCast(@alignCast( untyped_value));
    
    try std.testing.expectEqual(@as(u32, 10), value.*);
}