Zig Cookbook

Zig cookbook is a collection of simple Zig programs that demonstrate good practices to accomplish common programming tasks.

  • Main branch tracks Zig 0.13.0 and master, and ensured on Linux and macOS via GitHub actions.
  • Zig 0.11.0 support can be found in here.

How to use

The website is generated by mdbook, mdbook serve will start a server at http://localhost:3000 for preview.

Each recipe is accompanied by an illustrative example named after its corresponding sequence number. These examples can be executed using the command zig build run-{chapter-num}-{sequence-num}, or zig build run-all to execute all.

Note

Some recipes may depend on system libraries

  • Use make install-deps to install client libraries, and
  • docker-compose up -d to start required databases.

Contributing

This cookbook is a work in progress, and we welcome contributions from the community. If you have a favorite recipe that you'd like to share, please submit a pull request.

Acknowledgment

When working on zig-cookbook, we benefit a lot from several similar projects, thanks for their awesome work.

Star History

Star History Chart

License

The markdown files are licensed under CC BY-NC-ND 4.0 DEED, and zig files are under MIT.

Last change: 2024-07-07, commit: f0ebde2

Read file line by line

There is a Reader type in Zig, which provides various methods to read file, such as readAll, readInt. Here we will use streamUntilDelimiter to split lines.

const std = @import("std");
const fs = std.fs;
const print = std.debug.print;

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

    const file = try fs.cwd().openFile("tests/zig-zen.txt", .{});
    defer file.close();

    // Wrap the file reader in a buffered reader.
    // Since it's usually faster to read a bunch of bytes at once.
    var buf_reader = std.io.bufferedReader(file.reader());
    const reader = buf_reader.reader();

    var line = std.ArrayList(u8).init(allocator);
    defer line.deinit();

    const writer = line.writer();
    var line_no: usize = 0;
    while (reader.streamUntilDelimiter(writer, '\n', null)) {
        // Clear the line so we can reuse it.
        defer line.clearRetainingCapacity();
        line_no += 1;

        print("{d}--{s}\n", .{ line_no, line.items });
    } else |err| switch (err) {
        error.EndOfStream => { // end of file
            if (line.items.len > 0) {
                line_no += 1;
                print("{d}--{s}\n", .{ line_no, line.items });
            }
        },
        else => return err, // Propagate error
    }

    print("Total lines: {d}\n", .{line_no});
}
Last change: 2024-07-04, commit: e577a80

Mmap file

Creates a memory map of a file using mmap and simulates some non-sequential reads from the file. Using a memory map means you just index into a slice rather than having to deal with seek to navigate a file.

const std = @import("std");
const fs = std.fs;
const print = std.debug.print;

const filename = "/tmp/zig-cookbook-01-02.txt";

pub fn main() !void {
    if (.windows == @import("builtin").os.tag) {
        std.debug.print("MMap is not supported in Windows\n", .{});
        return;
    }

    const file = try fs.cwd().createFile(filename, .{
        .read = true,
        .truncate = true,
        .exclusive = false, // Set to true will ensure this file is created by us
    });
    defer file.close();
    const content_to_write = "hello zig cookbook";

    // Before mmap, we need to ensure file isn't empty
    try file.setEndPos(content_to_write.len);

    const md = try file.metadata();
    try std.testing.expectEqual(md.size(), content_to_write.len);

    const ptr = try std.posix.mmap(
        null,
        content_to_write.len,
        std.posix.PROT.READ | std.posix.PROT.WRITE,
        .{ .TYPE = .SHARED },
        file.handle,
        0,
    );
    defer std.posix.munmap(ptr);

    // Write file via mmap
    std.mem.copyForwards(u8, ptr, content_to_write);

    // Read file via mmap
    try std.testing.expectEqualStrings(content_to_write, ptr);
}
Last change: 2024-07-04, commit: e577a80

Find files that have been modified in the last 24 hours

Gets the current working directory by calling fs.cwd(), and then iterates files using walk(), which will recursively iterate entries in the directory.

For each entry, we check if it's a file, and use statFile() to retrieve the file's metadata.

//! Find files that have been modified in the last 24 hours

const std = @import("std");
const builtin = @import("builtin");
const fs = std.fs;
const print = std.debug.print;

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

    var iter_dir = try fs.cwd().openDir("src", .{ .iterate = true });
    defer iter_dir.close();

    var walker = try iter_dir.walk(allocator);
    defer walker.deinit();

    const now = std.time.nanoTimestamp();
    while (try walker.next()) |entry| {
        if (entry.kind != .file) {
            continue;
        }

        const stat = try iter_dir.statFile(entry.path);
        const last_modified = stat.mtime;
        const duration = now - last_modified;
        if (duration < std.time.ns_per_hour * 24) {
            print("Last modified: {d} seconds ago, size:{d} bytes, filename: {s}\n", .{
                @divTrunc(duration, std.time.ns_per_s),
                stat.size,
                entry.path,
            });
        }
    }
}
Last change: 2024-07-04, commit: e577a80

Calculate SHA-256 digest of a file

There are many crypto algorithm implementations in std, sha256, md5 are supported out of box.

const std = @import("std");
const fs = std.fs;
const Sha256 = std.crypto.hash.sha2.Sha256;

// In real world, this may set to page_size, usually it's 4096.
const BUF_SIZE = 16;

fn sha256_digest(
    file: fs.File,
) ![Sha256.digest_length]u8 {
    var sha256 = Sha256.init(.{});
    const rdr = file.reader();

    var buf: [BUF_SIZE]u8 = undefined;
    var n = try rdr.read(&buf);
    while (n != 0) {
        sha256.update(buf[0..n]);
        n = try rdr.read(&buf);
    }

    return sha256.finalResult();
}

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

    const file = try fs.cwd().openFile("tests/zig-zen.txt", .{});
    defer file.close();

    const digest = try sha256_digest(file);
    const hex_digest = try std.fmt.allocPrint(
        allocator,
        "{s}",
        .{std.fmt.fmtSliceHexLower(&digest)},
    );
    defer allocator.free(hex_digest);

    try std.testing.expectEqualStrings(
        "2210e9263ece534df0beff39ec06850d127dc60aa17bbc7769c5dc2ea5f3e342",
        hex_digest,
    );
}
Last change: 2024-01-01, commit: 53562ec

Salt and hash a password with PBKDF2

Uses std.crypto.pwhash.pbkdf2 to hash a salted password. The salt is generated using std.rand.DefaultPrng, which fills the salt byte array with generated random numbers.

const std = @import("std");
const print = std.debug.print;
const crypto = std.crypto;
const HmacSha256 = crypto.auth.hmac.sha2.HmacSha256;

pub fn main() !void {
    const salt = [_]u8{ 'a', 'b', 'c' };
    const password = "Guess Me If You Can!";
    const rounds = 1_000;

    // Usually 16 or 32 bytes
    var derived_key: [16]u8 = undefined;
    try crypto.pwhash.pbkdf2(&derived_key, password, &salt, rounds, HmacSha256);

    try std.testing.expectEqualSlices(
        u8,
        &[_]u8{
            44,
            184,
            223,
            181,
            238,
            128,
            211,
            50,
            149,
            114,
            26,
            86,
            225,
            172,
            116,
            81,
        },
        &derived_key,
    );
}
Last change: 2024-01-01, commit: 53562ec

Measure the elapsed time between two code sections

Instant represents a timestamp with respect to the currently executing program that ticks while the program is suspended and can be used to record elapsed time.

Calling std.time.Instant.since returns a u64 representing nanoseconds elapsed.

This is such a common task, that there is a Timer for convenience.

const std = @import("std");
const time = std.time;
const Instant = time.Instant;
const Timer = time.Timer;
const print = std.debug.print;

fn expensive_function() void {
    // sleep 500ms
    time.sleep(time.ns_per_ms * 500);
}

pub fn main() !void {
    // Method 1: Instant
    const start = try Instant.now();
    expensive_function();
    const end = try Instant.now();
    const elapsed1: f64 = @floatFromInt(end.since(start));
    print("Time elapsed is: {d:.3}ms\n", .{
        elapsed1 / time.ns_per_ms,
    });

    // Method 2: Timer
    var timer = try Timer.start();
    expensive_function();
    const elapsed2: f64 = @floatFromInt(timer.read());
    print("Time elapsed is: {d:.3}ms\n", .{
        elapsed2 / time.ns_per_ms,
    });
}
Last change: 2024-07-04, commit: e577a80

Listen on unused port TCP/IP

In this example, the port is displayed on the console, and the program will listen until a request is made. Ip4Address assigns a random port when setting port to 0.

//! Start a TCP server at an unused port.
//!
//! Test with
//! echo "hello zig" | nc localhost <port>

const std = @import("std");
const net = std.net;
const print = std.debug.print;

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

    const loopback = try net.Ip4Address.parse("127.0.0.1", 0);
    const localhost = net.Address{ .in = loopback };
    var server = try localhost.listen(.{
        .reuse_port = true,
    });
    defer server.deinit();

    const addr = server.listen_address;
    print("Listening on {}, access this port to end the program\n", .{addr.getPort()});

    var client = try server.accept();
    defer client.stream.close();

    print("Connection received! {} is sending data.\n", .{client.address});

    const message = try client.stream.reader().readAllAlloc(allocator, 1024);
    defer allocator.free(message);

    print("{} says {s}\n", .{ client.address, message });
}

When start starts up, try test like this:

echo "hello zig" | nc localhost <port>

By default, the program listens with IPv4. If you want IPv6, use ::1 instead of 127.0.0.1, replace net.Ip4Address.parse by net.Ip6Address.parse and the field .in in the creation of the net.Address with .in6.

(And connect to something like ip6-localhost, depending on the way your machine is set up.)

The next section will show how to connect to this server using Zig code.

Last change: 2024-07-04, commit: e577a80

TCP Client

In this example, we demonstrate creation of a TCP client to connect to the server from the previous section. You can run it using zig build run-04-02 -- <port>.

const std = @import("std");
const net = std.net;
const print = std.debug.print;

pub fn main() !void {
    var args = std.process.args();
    // The first (0 index) Argument is the path to the program.
    _ = args.skip();
    const port_value = args.next() orelse {
        print("expect port as command line argument\n", .{});
        return error.NoPort;
    };
    const port = try std.fmt.parseInt(u16, port_value, 10);

    const peer = try net.Address.parseIp4("127.0.0.1", port);
    // Connect to peer
    const stream = try net.tcpConnectToAddress(peer);
    defer stream.close();
    print("Connecting to {}\n", .{peer});

    // Sending data to peer
    const data = "hello zig";
    var writer = stream.writer();
    const size = try writer.write(data);
    print("Sending '{s}' to peer, total written: {d} bytes\n", .{ data, size });
    // Or just using `writer.writeAll`
    // try writer.writeAll("hello zig");
}

By default, the program connects with IPv4. If you want IPv6, use ::1 instead of 127.0.0.1, replace net.Address.parseIp4 by net.Address.parseIp6.

Last change: 2024-07-04, commit: e577a80

UDP Echo

Similar to the TCP server example, this program will listen on the specified IP address and port, but for UDP datagrams this time. If data is received, it will be echoed back to the sender's address.

Although std.net is mostly focused on abstractions for TCP (so far), we can still make use of socket programming to communicate via UDP.

//! Start a UDP echo on an unused port.
//!
//! Test with
//! echo "hello zig" | nc -u localhost <port>

const std = @import("std");
const net = std.net;
const posix = std.posix;
const print = std.debug.print;

pub fn main() !void {
    // adjust the ip/port here as needed
    const addr = try net.Address.parseIp("127.0.0.1", 32100);

    // get a socket and set domain, type and protocol flags
    const sock = try posix.socket(
        posix.AF.INET,
        posix.SOCK.DGRAM,
        posix.IPPROTO.UDP,
    );

    // for completeness, we defer closing the socket. In practice, if this is
    // a one-shot program, we could omit this and let the OS do the cleanup
    defer posix.close(sock);

    try posix.bind(sock, &addr.any, addr.getOsSockLen());

    var other_addr: posix.sockaddr = undefined;
    var other_addrlen: posix.socklen_t = @sizeOf(posix.sockaddr);

    var buf: [1024]u8 = undefined;

    print("Listen on {any}...\n", .{addr});

    // we did not set the NONBLOCK flag (socket type flag),
    // so the program will wait until data is received
    const n_recv = try posix.recvfrom(
        sock,
        buf[0..],
        0,
        &other_addr,
        &other_addrlen,
    );
    print(
        "received {d} byte(s) from {any};\n    string: {s}\n",
        .{ n_recv, other_addr, buf[0..n_recv] },
    );

    // we could extract the source address of the received data by
    // parsing the other_addr.data field

    const n_sent = try posix.sendto(
        sock,
        buf[0..n_recv],
        0,
        &other_addr,
        other_addrlen,
    );
    print("echoed {d} byte(s) back\n", .{n_sent});
}

After starting the program, test as follows with nc, using the -u flag for UDP:

echo "hello zig" | nc -u localhost <port>
Last change: 2024-05-31, commit: b0237f2

GET

Parses the supplied URL and makes a synchronous HTTP GET request with request. Prints obtained Response status and headers.

Note: Since HTTP support is in early stage, it's recommended to use libcurl for any complex task.

const std = @import("std");
const print = std.debug.print;
const http = std.http;

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

    var client = http.Client{ .allocator = allocator };
    defer client.deinit();

    const uri = try std.Uri.parse("http://httpbin.org/headers");
    const buf = try allocator.alloc(u8, 1024 * 1024 * 4);
    defer allocator.free(buf);
    var req = try client.open(.GET, uri, .{
        .server_header_buffer = buf,
    });
    defer req.deinit();

    try req.send();
    try req.finish();
    try req.wait();

    var iter = req.response.iterateHeaders();
    while (iter.next()) |header| {
        std.debug.print("Name:{s}, Value:{s}\n", .{ header.name, header.value });
    }

    try std.testing.expectEqual(req.response.status, .ok);

    var rdr = req.reader();
    const body = try rdr.readAllAlloc(allocator, 1024 * 1024 * 4);
    defer allocator.free(body);

    print("Body:\n{s}\n", .{body});
}
Last change: 2024-03-22, commit: f2bb91c

POST

Parses the supplied URL and makes a synchronous HTTP POST request with request. Prints obtained Response status, and data received from server.

Note: Since HTTP support is in early stage, it's recommended to use libcurl for any complex task.

const std = @import("std");
const print = std.debug.print;
const http = std.http;

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

    var client = http.Client{ .allocator = allocator };
    defer client.deinit();

    const uri = try std.Uri.parse("http://httpbin.org/anything");

    const payload =
        \\ {
        \\  "name": "zig-cookbook",
        \\  "author": "John"
        \\ }
    ;

    var buf: [1024]u8 = undefined;
    var req = try client.open(.POST, uri, .{ .server_header_buffer = &buf });
    defer req.deinit();

    req.transfer_encoding = .{ .content_length = payload.len };
    try req.send();
    var wtr = req.writer();
    try wtr.writeAll(payload);
    try req.finish();
    try req.wait();

    try std.testing.expectEqual(req.response.status, .ok);

    var rdr = req.reader();
    const body = try rdr.readAllAlloc(allocator, 1024 * 1024 * 4);
    defer allocator.free(body);

    print("Body:\n{s}\n", .{body});
}
Last change: 2024-03-22, commit: f2bb91c

Generate random numbers

Generates random numbers with the help of a thread-local, cryptographically secure pseudo random number generator std.crypto.random.

const std = @import("std");
const print = std.debug.print;

pub fn main() !void {
    const rand = std.crypto.random;

    print("Random u8: {}\n", .{rand.int(u8)});
    print("Random u8 less than 10: {}\n", .{rand.uintLessThan(u8, 10)});
    print("Random u16: {}\n", .{rand.int(u16)});
    print("Random u32: {}\n", .{rand.int(u32)});
    print("Random i32: {}\n", .{rand.int(i32)});
    print("Random float: {d}\n", .{rand.float(f64)});

    var i: usize = 0;
    while (i < 9) {
        print("Random enum: {}\n", .{rand.enumValue(enum { red, green, blue })});
        i += 1;
    }
}
Last change: 2024-07-04, commit: e577a80

Spawn a short-lived thread

The example uses std.Thread for concurrent and parallel programming. std.Thread.spawn spawns a new thread to calculate the result.

This example splits the array in half and performs the work in separate threads.

Note: In order to ensure t1 thread is completed when spawn t2 fails, we defer t1.join() immediately after spawn t1.

const std = @import("std");

pub fn main() !void {
    var arr = [_]i32{ 1, 25, -4, 10, 100, 200, -100, -200 };
    var max_value: i32 = undefined;
    try findMax(&max_value, &arr);
    try std.testing.expectEqual(max_value, 200);
}

fn findMax(max_value: *i32, values: []i32) !void {
    const THRESHOLD: usize = 2;

    if (values.len <= THRESHOLD) {
        var res = values[0];
        for (values) |it| {
            res = @max(res, it);
        }
        max_value.* = res;
        return;
    }

    const mid = values.len / 2;
    const left = values[0..mid];
    const right = values[mid..];

    var left_max: i32 = undefined;
    var right_max: i32 = undefined;
    // This block is necessary to ensure that all threads are joined before proceeding.
    {
        const t1 = try std.Thread.spawn(.{}, findMax, .{ &left_max, left });
        defer t1.join();
        const t2 = try std.Thread.spawn(.{}, findMax, .{ &right_max, right });
        defer t2.join();
    }

    max_value.* = @max(left_max, right_max);
}
Last change: 2024-02-02, commit: 1455a03

Share data between two threads

When we want to mutate data shared between threads, Mutex(Mutually exclusive flag) must be used to synchronize threads, otherwise the result maybe unexpected.

const std = @import("std");
const Thread = std.Thread;
const Mutex = Thread.Mutex;

const SharedData = struct {
    mutex: Mutex,
    value: i32,

    pub fn updateValue(self: *SharedData, increment: i32) void {
        // Use `tryLock` if you don't want to block
        self.mutex.lock();
        defer self.mutex.unlock();

        for (0..100) |_| {
            self.value += increment;
        }
    }
};

pub fn main() !void {
    var shared_data = SharedData{ .mutex = Mutex{}, .value = 0 };
    // This block is necessary to ensure that all threads are joined before proceeding.
    {
        const t1 = try Thread.spawn(.{}, SharedData.updateValue, .{ &shared_data, 1 });
        defer t1.join();
        const t2 = try Thread.spawn(.{}, SharedData.updateValue, .{ &shared_data, 2 });
        defer t2.join();
    }
    try std.testing.expectEqual(shared_data.value, 300);
}

If we remove Mutex protection, the result will most like be less than 300.

Last change: 2024-07-04, commit: e577a80

Check number of logical cpu cores

Shows the number of logical CPU cores in the current machine using std.Thread.getCpuCount.

const std = @import("std");
const print = std.debug.print;

pub fn main() !void {
    print("Number of logical cores is {}\n", .{try std.Thread.getCpuCount()});
}
Last change: 2024-07-04, commit: e577a80

External Command

Run an external command via std.process.Child, and collect output into ArrayList via pipe.

const std = @import("std");
const print = std.debug.print;
const Child = std.process.Child;
const ArrayList = std.ArrayList;

pub fn main() !void {
    var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);
    defer arena.deinit();
    const allocator = arena.allocator();

    const argv = [_][]const u8{
        "echo",
        "-n",
        "hello",
        "world",
    };

    // By default, child will inherit stdout & stderr from its parents,
    // this usually means that child's output will be printed to terminal.
    // Here we change them to pipe and collect into `ArrayList`.
    var child = Child.init(&argv, allocator);
    child.stdout_behavior = .Pipe;
    child.stderr_behavior = .Pipe;
    var stdout = ArrayList(u8).init(allocator);
    var stderr = ArrayList(u8).init(allocator);
    defer {
        stdout.deinit();
        stderr.deinit();
    }

    try child.spawn();
    try child.collectOutput(&stdout, &stderr, 1024);
    const term = try child.wait();

    try std.testing.expectEqual(term.Exited, 0);
    try std.testing.expectEqualStrings("hello world", stdout.items);
    try std.testing.expectEqualStrings("", stderr.items);
}
Last change: 2024-07-04, commit: e577a80

Parse a version string.

Constructs a std.SemanticVersion from a string literal using SemanticVersion.parse.

const std = @import("std");
const assert = std.debug.assert;

pub fn main() !void {
    const version = try std.SemanticVersion.parse("0.2.6");

    assert(version.order(.{
        .major = 0,
        .minor = 2,
        .patch = 6,
        .pre = null,
        .build = null,
    }) == .eq);
}
Last change: 2024-01-01, commit: 53562ec

Serialize and deserialize JSON

The std.json provides a set of functions such as stringify and stringifyAlloc for serializing JSON. Additionally, we can use parseFromSlice function to parse a []u8 of JSON.

The example below shows a []u8 of JSON being parsed. Compare each member one by one. Then, we modify the verified field to false and serialize it back into a JSON string.

const std = @import("std");
const json = std.json;
const testing = std.testing;

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

    // Deserialize JSON
    const json_str =
        \\{
        \\  "userid": 103609,
        \\  "verified": true,
        \\  "access_privileges": [
        \\    "user",
        \\    "admin"
        \\  ]
        \\}
    ;
    const T = struct { userid: i32, verified: bool, access_privileges: [][]u8 };
    const parsed = try json.parseFromSlice(T, allocator, json_str, .{});
    defer parsed.deinit();

    var value = parsed.value;

    try testing.expect(value.userid == 103609);
    try testing.expect(value.verified);
    try testing.expectEqualStrings("user", value.access_privileges[0]);
    try testing.expectEqualStrings("admin", value.access_privileges[1]);

    // Serialize JSON
    value.verified = false;
    const new_json_str = try json.stringifyAlloc(allocator, value, .{ .whitespace = .indent_2 });
    defer allocator.free(new_json_str);

    try testing.expectEqualStrings(
        \\{
        \\  "userid": 103609,
        \\  "verified": false,
        \\  "access_privileges": [
        \\    "user",
        \\    "admin"
        \\  ]
        \\}
    ,
        new_json_str,
    );
}
Last change: 2024-01-01, commit: 53562ec

Encode and decode base64

Encodes a byte slice into base64 string using std.base64.standard encode and decodes it with standard decode.

const std = @import("std");
const print = std.debug.print;
const Encoder = std.base64.standard.Encoder;
const Decoder = std.base64.standard.Decoder;

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

    const src = "hello zig";

    // Encode
    const encoded_length = Encoder.calcSize(src.len);
    const encoded_buffer = try allocator.alloc(u8, encoded_length);
    defer allocator.free(encoded_buffer);

    _ = Encoder.encode(encoded_buffer, src);
    try std.testing.expectEqualStrings("aGVsbG8gemln", encoded_buffer);

    // Decode
    const decoded_length = try Decoder.calcSizeForSlice(encoded_buffer);
    const decoded_buffer = try allocator.alloc(u8, decoded_length);
    defer allocator.free(decoded_buffer);

    try Decoder.decode(decoded_buffer, encoded_buffer);
    try std.testing.expectEqualStrings(src, decoded_buffer);
}
Last change: 2024-07-04, commit: e577a80

Creating and adding complex numbers

Creates complex numbers of type std.math.Complex. Both the real and imaginary part of the complex number must be of the same type.

Performing mathematical operations on complex numbers is the same as on built in types: the numbers in question must be of the same type (i.e. floats or integers).

const std = @import("std");
const print = std.debug.print;
const expectEqual = std.testing.expectEqual;
const Complex = std.math.Complex;

pub fn main() !void {
    const complex_integer = Complex(i32).init(10, 20);
    const complex_integer2 = Complex(i32).init(5, 17);
    const complex_float = Complex(f32).init(10.1, 20.1);

    print("Complex integer: {}\n", .{complex_integer});
    print("Complex float: {}\n", .{complex_float});

    const sum = complex_integer.add(complex_integer2);

    try expectEqual(sum, Complex(i32).init(15, 37));
}
Last change: 2024-01-01, commit: 53562ec

Bitfield

The fields of a packed struct are always laid out in memory in the order they are written, with no padding, so they are very nice to represent a bitfield.

Boolean values are represented as 1 bit in a packed struct, Zig also has arbitrary bit-width integers, like u28, u1 and so on.

const std = @import("std");
const print = std.debug.print;

const ColorFlags = packed struct(u32) {
    red: bool = false,
    green: bool = false,
    blue: bool = false,

    _padding: u29 = 0,
};

pub fn main() !void {
    const tom: ColorFlags = @bitCast(@as(u32, 0xFF));

    if (tom.red) {
        print("Tom likes red.\n", .{});
    }

    if (tom.red and tom.green) {
        print("Tom likes red and green.\n", .{});
    }

    const jerry: ColorFlags = @bitCast(@as(u32, 0x01));

    if (jerry.red) {
        print("Jerry likes red.\n", .{});
    }

    if (jerry.red and !jerry.green) {
        print("Jerry likes red, not green.\n", .{});
    }
}

Reference

Last change: 2024-07-04, commit: e577a80

Singly Linked List

Singly linked list contains nodes linked together. The list contains:

  • head, head of the list, used for traversal.
  • tail, tail of the list, used for fast add.
  • len, length of the list

Operations that can be performed on singly linked lists include:

  • Insertion O(1)
  • Deletion O(n), which is the most complex since it needs to maintain head/tail pointer.
  • Traversal O(n)
const std = @import("std");
const Allocator = std.mem.Allocator;

fn LinkedList(comptime T: type) type {
    return struct {
        const Node = struct {
            data: T,
            next: ?*Node = null,
        };
        const Self = @This();

        head: ?*Node = null,
        tail: ?*Node = null,
        len: usize = 0,
        allocator: Allocator,

        fn init(allocator: Allocator) Self {
            return .{ .allocator = allocator };
        }

        fn add(self: *Self, value: T) !void {
            const node = try self.allocator.create(Node);
            node.* = .{ .data = value };

            if (self.tail) |*tail| {
                tail.*.next = node;
                tail.* = node;
            } else {
                self.head = node;
                self.tail = node;
            }

            self.len += 1;
        }

        fn remove(self: *Self, value: T) bool {
            if (self.head == null) {
                return false;
            }

            // In this loop, we are trying to find the node that contains the value.
            // We need to keep track of the previous node to update the tail pointer if necessary.
            var current = self.head;
            var previous: ?*Node = null;
            while (current) |cur| : (current = cur.next) {
                if (cur.data == value) {
                    // If the current node is the head, point head to the next node.
                    if (self.head.? == cur) {
                        self.head = cur.next;
                    }
                    // If the current node is the tail, point tail to its previous node.
                    if (self.tail.? == cur) {
                        self.tail = previous;
                    }
                    // Skip the current node and update the previous node to point to the next node.
                    if (previous) |*prev| {
                        prev.*.next = cur.next;
                    }
                    self.allocator.destroy(cur);
                    self.len -= 1;
                    return true;
                }

                previous = cur;
            }

            return false;
        }

        fn search(self: *Self, value: T) bool {
            var head: ?*Node = self.head;
            while (head) |h| : (head = h.next) {
                if (h.data == value) {
                    return true;
                }
            }
            return false;
        }

        fn visit(self: *Self, visitor: *const fn (i: usize, v: T) anyerror!void) !usize {
            var head = self.head;
            var i: usize = 0;
            while (head) |n| : (i += 1) {
                try visitor(i, n.data);
                head = n.next;
            }

            return i;
        }

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

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

    var lst = LinkedList(u32).init(allocator);
    defer lst.deinit();

    const values = [_]u32{ 32, 20, 21 };
    for (values) |v| {
        try lst.add(v);
    }

    try std.testing.expectEqual(lst.len, values.len);
    try std.testing.expectEqual(
        try lst.visit(struct {
            fn visitor(i: usize, v: u32) !void {
                try std.testing.expectEqual(values[i], v);
            }
        }.visitor),
        3,
    );

    try std.testing.expect(lst.search(20));

    // Test delete head
    try std.testing.expect(lst.remove(32));
    try std.testing.expectEqual(
        try lst.visit(struct {
            fn visitor(i: usize, v: u32) !void {
                try std.testing.expectEqual(([_]u32{ 20, 21 })[i], v);
            }
        }.visitor),
        2,
    );

    // Test delete tail
    try std.testing.expect(lst.remove(21));
    try std.testing.expectEqual(
        try lst.visit(struct {
            fn visitor(i: usize, v: u32) !void {
                try std.testing.expectEqual(([_]u32{20})[i], v);
            }
        }.visitor),
        1,
    );

    // Test delete head and tail at the same time
    try std.testing.expect(lst.remove(20));
    try std.testing.expectEqual(
        try lst.visit(struct {
            fn visitor(_: usize, _: u32) !void {
                unreachable;
            }
        }.visitor),
        0,
    );

    try std.testing.expectEqual(lst.len, 0);
}
Last change: 2024-07-04, commit: e577a80

Doubly Linked List

A doubly linked list contains nodes that are linked together in both directions, allowing for more efficient operations in some scenarios. Each node in a doubly linked list contains:

  • data, the value stored in the node.
  • next, a pointer to the next node in the list.
  • prev, a pointer to the previous node in the list.

The list itself maintains:

  • head, the head of the list, used for traversal from the start.
  • tail, the tail of the list, used for traversal from the end and fast additions.
  • len, the length of the list, tracking the number of nodes.

Operations that can be performed on doubly linked lists include:

  • Insertion at the end O(1), based on the tail pointer.
  • Insertion at arbitrary positions O(n), due to traversal requirements.
  • Deletion O(n), with improved efficiency compared to singly linked lists it could easily find and remove nodes in this list without a full traversal.
const std = @import("std");
const Allocator = std.mem.Allocator;

fn DoublyLinkedList(comptime T: type) type {
    return struct {
        const Node = struct {
            data: T,
            next: ?*Node = null,
            prev: ?*Node = null,
        };

        const Self = @This();

        head: ?*Node = null,
        tail: ?*Node = null,
        len: usize = 0,
        allocator: Allocator,

        fn init(allocator: Allocator) Self {
            return .{ .allocator = allocator };
        }

        // This function is equals to self.insertAt(self.len, value)
        // But this cost O(1), while insertAt cost O(n) .
        fn insertLast(self: *Self, value: T) !void {
            const node = try self.allocator.create(Node);
            node.* = Node{ .data = value, .prev = self.tail };
            if (self.tail) |tail| {
                tail.*.next = node;
            } else {
                self.head = node;
            }

            self.tail = node;
            self.len += 1;
        }

        fn insertAt(self: *Self, position: usize, value: T) !void {
            if (position > self.len) {
                return error.OutOfRange;
            }
            defer self.len += 1;

            const node = try self.allocator.create(Node);
            node.* = Node{ .data = value };

            var current = self.head;
            // Find the node which is specified by the position
            for (0..position) |_| {
                if (current) |cur| {
                    current = cur.next;
                }
            }

            // Put node in front of current
            node.next = current;
            if (current) |cur| {
                node.*.prev = cur.prev;

                if (cur.prev) |*prev| {
                    prev.*.next = node;
                } else {
                    // When current has no prev, we are insert at head.
                    self.head = node;
                }

                cur.*.prev = node;
            } else { // We are insert at tail, update node to new tail.
                if (self.tail) |tail| {
                    node.*.prev = tail;
                    tail.*.next = node;
                }
                self.tail = node;
                // Head may also be null for an empty list
                if (null == self.head) {
                    self.head = node;
                }
            }
        }

        fn remove(self: *Self, value: T) bool {
            var current = self.head;

            while (current) |cur| : (current = cur.next) {
                if (cur.data == value) {
                    // if the current node has a previous node
                    // then set the previous node's next to the current node's next
                    if (cur.prev) |prev| {
                        prev.next = cur.next;
                    } else {
                        // if the current node has no previous node
                        self.head = cur.next;
                    }

                    // if the current node has a next node
                    // then set the next node's previous to the current node's previous
                    if (cur.next) |next| {
                        next.prev = cur.prev;
                    } else {
                        //  if the current node has no next node
                        // then set the tail to the current node's previous
                        self.tail = cur.prev;
                    }

                    self.allocator.destroy(cur);
                    self.len -= 1;

                    return true;
                }
            }

            return false;
        }

        fn search(self: *Self, value: T) bool {
            var current: ?*Node = self.head;

            while (current) |cur| : (current = cur.next) {
                if (cur.data == value) {
                    return true;
                }
            }

            return false;
        }

        fn visit(self: Self, visitor: *const fn (i: usize, v: T) anyerror!void) !usize {
            var current: ?*Node = self.head;
            var i: usize = 0;

            while (current) |cur| : (i += 1) {
                try visitor(i, cur.data);
                current = cur.next;
            }

            return i;
        }

        fn visitBackwards(self: Self, visitor: *const fn (i: usize, v: T) anyerror!void) !usize {
            var current = self.tail;
            var i: usize = 0;

            while (current) |cur| : (i += 1) {
                try visitor(i, cur.data);
                current = cur.prev;
            }
            return i;
        }

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

                current = next;
            }

            self.head = null;
            self.tail = null;
            self.len = 0;
        }
    };
}

fn ensureList(lst: DoublyLinkedList(u32), comptime expected: []const u32) !void {
    const visited_times = try lst.visit(struct {
        fn visitor(i: usize, v: u32) !void {
            if (expected.len == 0) {
                unreachable;
            } else {
                try std.testing.expectEqual(v, expected[i]);
            }
        }
    }.visitor);
    try std.testing.expectEqual(visited_times, expected.len);

    const visited_times2 = try lst.visitBackwards(struct {
        fn visitor(i: usize, v: u32) !void {
            if (expected.len == 0) {
                unreachable;
            } else {
                try std.testing.expectEqual(v, expected[expected.len - i - 1]);
            }
        }
    }.visitor);
    try std.testing.expectEqual(visited_times2, expected.len);
}

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

    const allocator = gpa.allocator();
    var list = DoublyLinkedList(u32).init(allocator);
    defer list.deinit();

    const values = [_]u32{ 1, 2, 3, 4, 5 };

    for (values) |value| {
        try list.insertLast(value);
    }
    try ensureList(list, &values);

    try list.insertAt(1, 100);
    try ensureList(list, &[_]u32{ 1, 100, 2, 3, 4, 5 });

    try list.insertAt(0, 200);
    try ensureList(list, &[_]u32{ 200, 1, 100, 2, 3, 4, 5 });

    try std.testing.expect(list.remove(100));
    try ensureList(list, &[_]u32{ 200, 1, 2, 3, 4, 5 });

    // delete all
    for (values) |value| {
        try std.testing.expect(list.remove(value));
    }
    try std.testing.expect(list.remove(200));
    try ensureList(list, &[_]u32{});
}
Last change: 2024-09-02, commit: 4ac1d99

Argument Parsing

Parsing arguments is common in command line programs and there are some packages in Zig to make this task easier. To name a few:

Here we will give an example using simargs.

const std = @import("std");
const print = std.debug.print;
const simargs = @import("simargs");

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

    var opt = try simargs.parse(allocator, struct {
        // Those fields declare arguments options
        // only `output` is required, others are all optional
        verbose: ?bool,
        @"user-agent": enum { Chrome, Firefox, Safari } = .Firefox,
        timeout: ?u16 = 30, // default value
        output: []const u8 = "/tmp",
        help: bool = false,

        // This declares option's short name
        pub const __shorts__ = .{
            .verbose = .v,
            .output = .o,
            .@"user-agent" = .A,
            .help = .h,
        };

        // This declares option's help message
        pub const __messages__ = .{
            .verbose = "Make the operation more talkative",
            .output = "Write to file instead of stdout",
            .timeout = "Max time this request can cost",
        };
    }, "[file]", null);
    defer opt.deinit();

    const sep = "-" ** 30;
    print("{s}Program{s}\n{s}\n\n", .{ sep, sep, opt.program });
    print("{s}Arguments{s}\n", .{ sep, sep });
    inline for (std.meta.fields(@TypeOf(opt.args))) |fld| {
        const format = "{s:>10}: " ++ switch (fld.type) {
            []const u8 => "{s}",
            ?[]const u8 => "{?s}",
            else => "{any}",
        } ++ "\n";
        print(format, .{ fld.name, @field(opt.args, fld.name) });
    }

    print("\n{s}Positionals{s}\n", .{ sep, sep });
    for (opt.positional_args, 0..) |arg, idx| {
        print("{d}: {s}\n", .{ idx + 1, arg });
    }

    // Provide a print_help util method
    print("\n{s}print_help{s}\n", .{ sep, sep });
    const stdout = std.io.getStdOut();
    try opt.printHelp(stdout.writer());
}
Last change: 2024-10-09, commit: 08cc2a5

Database

This section will demonstrate how to connect to popular databases from Zig.

Data model used is as follows:

  1. Create two tables: cat_colors, cats, cats has a reference in cat_colors.
  2. Create a prepare statement for each table, and insert data.
  3. Execute a join query to get cat_name and color_name at the same time.

cat_colors

idname
1Blue
2Black

cats

idnamecolor_id
1Tigger1
2Sammy1
3Oreo2
4Biscuit2
Last change: 2024-07-04, commit: e577a80

SQLite

Although there are some wrapper package options for SQLite in Zig, they are unstable. So here we will introduce the C API interface.

Data models are introduced here.

/// SQLite API demo
/// https://www.sqlite.org/cintro.html
///
const std = @import("std");
const c = @cImport({
    @cInclude("sqlite3.h");
});
const print = std.debug.print;

const DB = struct {
    db: *c.sqlite3,

    fn init(db: *c.sqlite3) DB {
        return .{ .db = db };
    }

    fn deinit(self: DB) void {
        _ = c.sqlite3_close(self.db);
    }

    fn execute(self: DB, query: [:0]const u8) !void {
        var errmsg: [*c]u8 = undefined;
        if (c.SQLITE_OK != c.sqlite3_exec(self.db, query, null, null, &errmsg)) {
            defer c.sqlite3_free(errmsg);
            print("Exec query failed: {s}\n", .{errmsg});
            return error.execError;
        }
        return;
    }

    fn queryTable(self: DB) !void {
        const stmt = blk: {
            var stmt: ?*c.sqlite3_stmt = undefined;
            const query =
                \\ SELECT
                \\     c.name,
                \\     cc.name
                \\ FROM
                \\     cats c
                \\     INNER JOIN cat_colors cc ON cc.id = c.color_id;
                \\
            ;
            if (c.SQLITE_OK != c.sqlite3_prepare_v2(self.db, query, query.len + 1, &stmt, null)) {
                print("Can't create prepare statement: {s}\n", .{c.sqlite3_errmsg(self.db)});
                return error.prepareStmt;
            }
            break :blk stmt.?;
        };
        defer _ = c.sqlite3_finalize(stmt);

        var rc = c.sqlite3_step(stmt);
        while (rc == c.SQLITE_ROW) {
            // iCol is 0-based.
            // Those return text are invalidated when call step, reset, finalize are called
            // https://www.sqlite.org/c3ref/column_blob.html
            const cat_name = c.sqlite3_column_text(stmt, 0);
            const color_name = c.sqlite3_column_text(stmt, 1);

            print("Cat {s} is in {s} color\n", .{ cat_name, color_name });
            rc = c.sqlite3_step(stmt);
        }

        if (rc != c.SQLITE_DONE) {
            print("Step query failed: {s}\n", .{c.sqlite3_errmsg(self.db)});
            return error.stepQuery;
        }
    }

    fn insertTable(self: DB) !void {
        const insert_color_stmt = blk: {
            var stmt: ?*c.sqlite3_stmt = undefined;
            const query = "INSERT INTO cat_colors (name) values (?1)";
            if (c.SQLITE_OK != c.sqlite3_prepare_v2(self.db, query, query.len + 1, &stmt, null)) {
                print("Can't create prepare statement: {s}\n", .{c.sqlite3_errmsg(self.db)});
                return error.prepareStmt;
            }
            break :blk stmt.?;
        };
        defer _ = c.sqlite3_finalize(insert_color_stmt);

        const insert_cat_stmt = blk: {
            var stmt: ?*c.sqlite3_stmt = undefined;
            const query = "INSERT INTO cats (name, color_id) values (?1, ?2)";
            if (c.SQLITE_OK != c.sqlite3_prepare_v2(self.db, query, query.len + 1, &stmt, null)) {
                print("Can't create prepare statement: {s}\n", .{c.sqlite3_errmsg(self.db)});
                return error.prepareStmt;
            }
            break :blk stmt.?;
        };
        defer _ = c.sqlite3_finalize(insert_cat_stmt);

        const cat_colors = .{
            .{
                "Blue", .{
                    "Tigger",
                    "Sammy",
                },
            },
            .{
                "Black", .{
                    "Oreo",
                    "Biscuit",
                },
            },
        };

        inline for (cat_colors) |row| {
            const color = row.@"0";
            const cat_names = row.@"1";

            // bind index is 1-based.
            if (c.SQLITE_OK != c.sqlite3_bind_text(insert_color_stmt, 1, color, color.len, c.SQLITE_STATIC)) {
                print("Can't bind text: {s}\n", .{c.sqlite3_errmsg(self.db)});
                return error.bindText;
            }
            if (c.SQLITE_DONE != c.sqlite3_step(insert_color_stmt)) {
                print("Can't step color stmt: {s}\n", .{c.sqlite3_errmsg(self.db)});
                return error.step;
            }

            _ = c.sqlite3_reset(insert_color_stmt);

            const last_id = c.sqlite3_last_insert_rowid(self.db);
            inline for (cat_names) |cat_name| {
                if (c.SQLITE_OK != c.sqlite3_bind_text(insert_cat_stmt, 1, cat_name, cat_name.len, c.SQLITE_STATIC)) {
                    print("Can't bind cat name: {s}\n", .{c.sqlite3_errmsg(self.db)});
                    return error.bindText;
                }
                if (c.SQLITE_OK != c.sqlite3_bind_int64(insert_cat_stmt, 2, last_id)) {
                    print("Can't bind cat color_id: {s}\n", .{c.sqlite3_errmsg(self.db)});
                    return error.bindText;
                }
                if (c.SQLITE_DONE != c.sqlite3_step(insert_cat_stmt)) {
                    print("Can't step cat stmt: {s}\n", .{c.sqlite3_errmsg(self.db)});
                    return error.step;
                }

                _ = c.sqlite3_reset(insert_cat_stmt);
            }
        }

        return;
    }
};

pub fn main() !void {
    const version = c.sqlite3_libversion();
    print("libsqlite3 version is {s}\n", .{version});

    var c_db: ?*c.sqlite3 = undefined;
    if (c.SQLITE_OK != c.sqlite3_open(":memory:", &c_db)) {
        print("Can't open database: {s}\n", .{c.sqlite3_errmsg(c_db)});
        return;
    }
    const db = DB.init(c_db.?);
    defer db.deinit();

    try db.execute(
        \\ create table if not exists cat_colors (
        \\   id integer primary key,
        \\   name text not null unique
        \\ );
        \\ create table if not exists cats (
        \\   id integer primary key,
        \\   name text not null,
        \\   color_id integer not null references cat_colors(id)
        \\ );
    );

    try db.insertTable();
    try db.queryTable();
}
Last change: 2024-07-04, commit: e577a80

Postgres

As with previous section, here we introduce libpq interface directly, other than wrapper package.

Data models used in this demo are the same with the one used in SQLite section.

Note: After executing a query with PQexec like functions, if there are returning results, check the result with PGRES_TUPLES_OK, otherwise PGRES_COMMAND_OK should be used.

//! Libpq API example
//! https://gist.github.com/jiacai2050/00709b98ee69d73d022d2f293555f08f
//! https://www.postgresql.org/docs/16/libpq-example.html
//!
const std = @import("std");
const print = std.debug.print;
const c = @cImport({
    @cInclude("libpq-fe.h");
});

const DB = struct {
    conn: *c.PGconn,

    fn init(conn_info: [:0]const u8) !DB {
        const conn = c.PQconnectdb(conn_info);
        if (c.PQstatus(conn) != c.CONNECTION_OK) {
            print("Connect failed, err: {s}\n", .{c.PQerrorMessage(conn)});
            return error.connect;
        }
        return DB{ .conn = conn.? };
    }

    fn deinit(self: DB) void {
        c.PQfinish(self.conn);
    }

    // Execute a query without returning any data.
    fn exec(self: DB, query: [:0]const u8) !void {
        const result = c.PQexec(self.conn, query);
        defer c.PQclear(result);

        if (c.PQresultStatus(result) != c.PGRES_COMMAND_OK) {
            print("exec query failed, query:{s}, err: {s}\n", .{ query, c.PQerrorMessage(self.conn) });
            return error.Exec;
        }
    }

    fn insertTable(self: DB) !void {
        // 1. create two prepared statements.
        {
            // There is no `get_last_insert_rowid` in libpq, so we use RETURNING id to get the last insert id.
            const res = c.PQprepare(
                self.conn,
                "insert_cat_colors",
                "INSERT INTO cat_colors (name) VALUES ($1) returning id",
                1, // nParams, number of parameters supplied
                // Specifies, by OID, the data types to be assigned to the parameter symbols.
                // When null, the server infers a data type for the parameter symbol in the same way it would do for an untyped literal string.
                null, // paramTypes.
            );
            defer c.PQclear(res);
            if (c.PQresultStatus(res) != c.PGRES_COMMAND_OK) {
                print("prepare insert cat_colors failed, err: {s}\n", .{c.PQerrorMessage(self.conn)});
                return error.prepare;
            }
        }
        {
            const res = c.PQprepare(self.conn, "insert_cats", "INSERT INTO cats (name, color_id) VALUES ($1, $2)", 2, null);
            defer c.PQclear(res);
            if (c.PQresultStatus(res) != c.PGRES_COMMAND_OK) {
                print("prepare insert cats failed, err: {s}\n", .{c.PQerrorMessage(self.conn)});
                return error.prepare;
            }
        }
        const cat_colors = .{
            .{
                "Blue", .{
                    "Tigger",
                    "Sammy",
                },
            },
            .{
                "Black", .{
                    "Oreo",
                    "Biscuit",
                },
            },
        };

        // 2. Use prepared statements to insert data.
        inline for (cat_colors) |row| {
            const color = row.@"0";
            const cat_names = row.@"1";
            const color_id = blk: {
                const res = c.PQexecPrepared(
                    self.conn,
                    "insert_cat_colors",
                    1, // nParams
                    &[_][*c]const u8{color}, // paramValues
                    &[_]c_int{color.len}, // paramLengths
                    &[_]c_int{0}, // paramFormats
                    0, // resultFormat
                );
                defer c.PQclear(res);

                // Since this insert has returns, so we check res with PGRES_TUPLES_OK
                if (c.PQresultStatus(res) != c.PGRES_TUPLES_OK) {
                    print("exec insert cat_colors failed, err: {s}\n", .{c.PQresultErrorMessage(res)});
                    return error.InsertCatColors;
                }
                break :blk std.mem.span(c.PQgetvalue(res, 0, 0));
            };
            inline for (cat_names) |name| {
                const res = c.PQexecPrepared(
                    self.conn,
                    "insert_cats",
                    2, // nParams
                    &[_][*c]const u8{ name, color_id }, // paramValues
                    &[_]c_int{ name.len, @intCast(color_id.len) }, // paramLengths
                    &[_]c_int{ 0, 0 }, // paramFormats
                    0, // resultFormat, 0 means text, 1 means binary.
                );
                defer c.PQclear(res);

                // This insert has no returns, so we check res with PGRES_COMMAND_OK
                if (c.PQresultStatus(res) != c.PGRES_COMMAND_OK) {
                    print("exec insert cats failed, err: {s}\n", .{c.PQresultErrorMessage(res)});
                    return error.InsertCats;
                }
            }
        }
    }

    fn queryTable(self: DB) !void {
        const query =
            \\ SELECT
            \\     c.name,
            \\     cc.name
            \\ FROM
            \\     cats c
            \\     INNER JOIN cat_colors cc ON cc.id = c.color_id;
            \\
        ;

        const result = c.PQexec(self.conn, query);
        defer c.PQclear(result);

        if (c.PQresultStatus(result) != c.PGRES_TUPLES_OK) {
            print("exec query failed, query:{s}, err: {s}\n", .{ query, c.PQerrorMessage(self.conn) });
            return error.queryTable;
        }

        const num_rows = c.PQntuples(result);
        for (0..@intCast(num_rows)) |row| {
            const cat_name = std.mem.span(c.PQgetvalue(result, @intCast(row), 0));
            const color_name = std.mem.span(c.PQgetvalue(result, @intCast(row), 1));
            print("Cat {s} is in {s} color\n", .{ cat_name, color_name });
        }
    }
};

pub fn main() !void {
    const conn_info = "host=127.0.0.1 user=postgres password=postgres dbname=postgres";

    const db = try DB.init(conn_info);
    defer db.deinit();

    try db.exec(
        \\ create table if not exists cat_colors (
        \\   id integer primary key generated always as identity,
        \\   name text not null unique
        \\ );
        \\ create table if not exists cats (
        \\   id integer primary key generated always as identity,
        \\   name text not null,
        \\   color_id integer not null references cat_colors(id)
        \\ );
    );

    try db.insertTable();
    try db.queryTable();
}
Last change: 2024-07-04, commit: e577a80

MySQL

As with sqlite section, here we introduce libmysqlclient interface directly.

Data models are introduced here.

Note: After executing a query with mysql_real_query like functions, if there are returning results, we must consume the result, otherwise we will get following error when we execute next query.

Commands out of sync; you can't run this command now
//! MySQL 8.0 API demo
//! https://dev.mysql.com/doc/c-api/8.0/en/c-api-basic-interface-usage.html
//!
const std = @import("std");
const Allocator = std.mem.Allocator;
const c = @cImport({
    @cInclude("mysql.h");
});
const print = std.debug.print;

pub const DBInfo = struct {
    host: [:0]const u8,
    user: [:0]const u8,
    password: [:0]const u8,
    database: [:0]const u8,
    port: u32 = 3306,
};

pub const DB = struct {
    conn: *c.MYSQL,
    allocator: Allocator,

    fn init(allocator: Allocator, db_info: DBInfo) !DB {
        const db = c.mysql_init(null);

        if (db == null) {
            return error.initError;
        }

        if (c.mysql_real_connect(
            db,
            db_info.host,
            db_info.user,
            db_info.password,
            db_info.database,
            db_info.port,
            null,
            c.CLIENT_MULTI_STATEMENTS,
        ) == null) {
            print("Connect to database failed: {s}\n", .{c.mysql_error(db)});
            return error.connectError;
        }

        return .{
            .conn = db,
            .allocator = allocator,
        };
    }

    fn deinit(self: DB) void {
        c.mysql_close(self.conn);
    }

    fn execute(self: DB, query: []const u8) !void {
        if (c.mysql_real_query(self.conn, query.ptr, query.len) != 0) {
            print("Exec query failed: {s}\n", .{c.mysql_error(self.conn)});
            return error.execError;
        }
    }

    fn queryTable(self: DB) !void {
        const query =
            \\ SELECT
            \\     c.name,
            \\     cc.name
            \\ FROM
            \\     cats c
            \\     INNER JOIN cat_colors cc ON cc.id = c.color_id;
            \\
        ;

        try self.execute(query);
        const result = c.mysql_store_result(self.conn);
        if (result == null) {
            print("Store result failed: {s}\n", .{c.mysql_error(self.conn)});
            return error.storeResultError;
        }
        defer c.mysql_free_result(result);

        while (c.mysql_fetch_row(result)) |row| {
            const cat_name = row[0];
            const color_name = row[1];
            print("Cat: {s}, Color: {s}\n", .{ cat_name, color_name });
        }
    }

    fn insertTable(self: DB) !void {
        const cat_colors = .{
            .{
                "Blue",
                .{ "Tigger", "Sammy" },
            },
            .{
                "Black",
                .{ "Oreo", "Biscuit" },
            },
        };

        const insert_color_stmt: *c.MYSQL_STMT = blk: {
            const stmt = c.mysql_stmt_init(self.conn);
            if (stmt == null) {
                return error.initStmt;
            }
            errdefer _ = c.mysql_stmt_close(stmt);

            const insert_color_query = "INSERT INTO cat_colors (name) values (?)";
            if (c.mysql_stmt_prepare(stmt, insert_color_query, insert_color_query.len) != 0) {
                print("Prepare color stmt failed, msg:{s}\n", .{c.mysql_error(self.conn)});
                return error.prepareStmt;
            }

            break :blk stmt.?;
        };
        defer _ = c.mysql_stmt_close(insert_color_stmt);

        const insert_cat_stmt = blk: {
            const stmt = c.mysql_stmt_init(self.conn);
            if (stmt == null) {
                return error.initStmt;
            }
            errdefer _ = c.mysql_stmt_close(stmt);

            const insert_cat_query = "INSERT INTO cats (name, color_id) values (?, ?)";
            if (c.mysql_stmt_prepare(stmt, insert_cat_query, insert_cat_query.len) != 0) {
                print("Prepare cat stmt failed: {s}\n", .{c.mysql_error(self.conn)});
                return error.prepareStmt;
            }

            break :blk stmt.?;
        };
        defer _ = c.mysql_stmt_close(insert_cat_stmt);

        inline for (cat_colors) |row| {
            const color = row.@"0";
            const cat_names = row.@"1";

            var color_binds = [_]c.MYSQL_BIND{std.mem.zeroes(c.MYSQL_BIND)};
            color_binds[0].buffer_type = c.MYSQL_TYPE_STRING;
            color_binds[0].buffer_length = color.len;
            color_binds[0].is_null = 0;
            color_binds[0].buffer = @constCast(@ptrCast(color.ptr));

            if (c.mysql_stmt_bind_param(insert_color_stmt, &color_binds)) {
                print("Bind color param failed: {s}\n", .{c.mysql_error(self.conn)});
                return error.bindParamError;
            }
            if (c.mysql_stmt_execute(insert_color_stmt) != 0) {
                print("Exec color stmt failed: {s}\n", .{c.mysql_error(self.conn)});
                return error.execStmtError;
            }
            const last_id = c.mysql_stmt_insert_id(insert_color_stmt);
            _ = c.mysql_stmt_reset(insert_color_stmt);

            inline for (cat_names) |cat_name| {
                var cat_binds = [_]c.MYSQL_BIND{ std.mem.zeroes(c.MYSQL_BIND), std.mem.zeroes(c.MYSQL_BIND) };
                cat_binds[0].buffer_type = c.MYSQL_TYPE_STRING;
                cat_binds[0].buffer_length = cat_name.len;
                cat_binds[0].buffer = @constCast(@ptrCast(cat_name.ptr));

                cat_binds[1].buffer_type = c.MYSQL_TYPE_LONG;
                cat_binds[1].length = (@as(c_ulong, 1));
                cat_binds[1].buffer = @constCast(@ptrCast(&last_id));

                if (c.mysql_stmt_bind_param(insert_cat_stmt, &cat_binds)) {
                    print("Bind cat param failed: {s}\n", .{c.mysql_error(self.conn)});
                    return error.bindParamError;
                }
                if (c.mysql_stmt_execute(insert_cat_stmt) != 0) {
                    print("Exec cat stmt failed: {s}\n", .{c.mysql_error(self.conn)});
                    return error.execStmtError;
                }

                _ = c.mysql_stmt_reset(insert_cat_stmt);
            }
        }
    }
};

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

    const version = c.mysql_get_client_version();
    print("MySQL client version is {}\n", .{version});

    const db = try DB.init(allocator, .{
        .database = "public",
        .host = "127.0.0.1",
        .user = "root",
        .password = "password",
    });
    defer db.deinit();

    try db.execute(
        \\ CREATE TABLE IF NOT EXISTS cat_colors (
        \\  id INT AUTO_INCREMENT PRIMARY KEY,
        \\  name VARCHAR(255) NOT NULL
        \\);
        \\
        \\CREATE TABLE IF NOT EXISTS cats (
        \\  id INT AUTO_INCREMENT PRIMARY KEY,
        \\  name VARCHAR(255) NOT NULL,
        \\  color_id INT NOT NULL
        \\)
    );
    // Since we use multi-statement, we need to consume all results.
    // Otherwise we will get following error when we execute next query.
    // Commands out of sync; you can't run this command now
    //
    // https://dev.mysql.com/doc/c-api/8.0/en/mysql-next-result.html
    while (c.mysql_next_result(db.conn) == 0) {
        const res = c.mysql_store_result(db.conn);
        c.mysql_free_result(res);
    }

    try db.insertTable();
    try db.queryTable();
}
Last change: 2024-07-04, commit: e577a80

Regular Expressions

Currently there is no regex support in Zig, so the best way to go is binding with Posix's regex.h.

Interop with C is easy in Zig, but since translate-c doesn't support bitfields, we can't use regex_t directly. So here we create a static C library first, providing two functions:

regex_t* alloc_regex_t(void);
void free_regex_t(regex_t* ptr);

regex_t* is a pointer, it has fixed size, so we can use it directly in Zig.

const std = @import("std");
const print = std.debug.print;
const c = @cImport({
    @cInclude("regex.h");
    // This is our static library.
    @cInclude("regex_slim.h");
});

const Regex = struct {
    inner: *c.regex_t,

    fn init(pattern: [:0]const u8) !Regex {
        const inner = c.alloc_regex_t().?;
        if (0 != c.regcomp(inner, pattern, c.REG_NEWLINE | c.REG_EXTENDED)) {
            return error.compile;
        }

        return .{
            .inner = inner,
        };
    }

    fn deinit(self: Regex) void {
        c.free_regex_t(self.inner);
    }

    fn matches(self: Regex, input: [:0]const u8) bool {
        const match_size = 1;
        var pmatch: [match_size]c.regmatch_t = undefined;
        return 0 == c.regexec(self.inner, input, match_size, &pmatch, 0);
    }

    fn exec(self: Regex, input: [:0]const u8) !void {
        const match_size = 1;
        var pmatch: [match_size]c.regmatch_t = undefined;

        var i: usize = 0;
        var string = input;
        const expected = [_][]const u8{ "John Do", "John Foo" };
        while (true) {
            if (0 != c.regexec(self.inner, string, match_size, &pmatch, 0)) {
                break;
            }

            const slice = string[@as(usize, @intCast(pmatch[0].rm_so))..@as(usize, @intCast(pmatch[0].rm_eo))];

            try std.testing.expectEqualStrings(expected[i], slice);

            string = string[@intCast(pmatch[0].rm_eo)..];
            i += 1;
        }

        try std.testing.expectEqual(i, 2);
    }
};

pub fn main() !void {
    {
        const regex = try Regex.init("[ab]c");
        defer regex.deinit();

        try std.testing.expect(regex.matches("bc"));
        try std.testing.expect(!regex.matches("cc"));
    }

    {
        const regex = try Regex.init("John.*o");
        defer regex.deinit();

        try regex.exec(
            \\ 1) John Driverhacker;
            \\ 2) John Doe;
            \\ 3) John Foo;
            \\
        );
    }
}

References

Last change: 2024-09-18, commit: 26822f5

String Parsing

String to number/enum

const std = @import("std");
const expectEqual = std.testing.expectEqual;
const expectError = std.testing.expectError;
const parseInt = std.fmt.parseInt;
const parseFloat = std.fmt.parseFloat;
const stringToEnum = std.meta.stringToEnum;

pub fn main() !void {
    try expectEqual(parseInt(i32, "123", 10), 123);
    try expectEqual(parseInt(i32, "-123", 10), -123);
    try expectError(error.Overflow, parseInt(u4, "123", 10));

    // 0 means auto detect the base.
    // base = 16
    try expectEqual(parseInt(i32, "0xF", 0), 15);
    // base = 2
    try expectEqual(parseInt(i32, "0b1111", 0), 15);
    // base = 8
    try expectEqual(parseInt(i32, "0o17", 0), 15);

    try expectEqual(parseFloat(f32, "1.23"), 1.23);
    try expectEqual(parseFloat(f32, "-1.23"), -1.23);

    const Color = enum {
        Red,
        Blue,
        Green,
    };
    try expectEqual(stringToEnum(Color, "Red").?, Color.Red);
    try expectEqual(stringToEnum(Color, "Yello"), null);
}
Last change: 2024-01-01, commit: 7c6cb83