Introduction to Zig: a project-based book

repository·main·Indexed 25 days ago

https://github.com/pedropark99/zig-book

An open-source technical introduction to the Zig programming language by Pedro Duarte Faria. This project-based resource covers Zig syntax, memory allocators, error handling, the Zig build system, C interoperability, and parallelism (threads and SIMD). It includes practical examples such as building a Base64 encoder/decoder, an HTTP server, and an image filter, as well as guidance on testing, fuzzing, and I/O operations.

Tokens
3K
Snippets
10
Records
14
Agent score
83%

What's inside zig-book

  1. Overview of Introduction to Zig

    main

    This project is the official repository for the book "Introduction to Zig: a project-based book" by Pedro Duarte Faria. The book is an open-source, technical introduction to the Zig programming language, designed for both beginners and experienced developers.

    It uses a project-based approach (e.g., building a Base64 encoder/decoder, an HTTP Server, and an image filter) to teach:

    • Zig syntax and comparisons to C, C++, and Rust.
    • Data structures, memory allocators, filesystem, and I/O.
    • Optionals and nullability.
    • Testing and debugging.
    • Error handling as values.
    • The Zig build system and C interoperability.
    • Parallelism (threads and SIMD).
  2. Build the book manually

    main

    To build the book manually, you must first install the following dependencies on your machine:

    1. Zig compiler
    2. R programming language (including knitr and rmarkdown)
    3. Quarto publishing system (which uses Pandoc)

    Once these are installed, follow these steps:

    1. Install R packages

    Run the provided R script to install the necessary R packages used throughout the book. Note that on Linux or macOS, this may take time as dependencies are often built from source.

    2. Render the book

    Use the Quarto CLI to compile the book content into HTML.

    # Install R dependencies
    Rscript dependencies.R
    
    # Render the book
    quarto render
  3. Build the book with Nix Flake

    main

    If you prefer a reproducible environment, you can use the Nix Flake declared in the repository. This automatically handles the installation of Zig, R, and Quarto.

    1. Run nix develop in the project root to enter a new bash session containing the required environment.
    2. Run quarto render within that session to build the book.
    nix develop
    # Inside the new shell:
    quarto render
  4. Write a simple test with memory leak detection

    main

    Zig's testing framework allows you to write tests using the test keyword. When using allocators within tests, use std.testing.allocator. This allocator is designed to detect memory leaks; if you fail to call deinit() on an allocated resource (like an ArrayList), the test will fail, notifying you of the leak.

    test "simple test" {
        var list = std.ArrayList(i32).init(std.testing.allocator);
        defer list.deinit(); // Ensures memory is freed and leaks are detected
        try list.append(42);
        try std.testing.expectEqual(@as(i32, 42), list.pop());
    }
  5. Use SIMD vectors for matrix multiplication

    main

    This example demonstrates how to perform matrix multiplication using Zig's @Vector type to leverage SIMD (Single Instruction, Multiple Data) capabilities. By casting slices of data into @Vector(nv, u64), you can perform arithmetic operations like multiplication on entire vectors at once, followed by @reduce(.Add, result) to sum the elements of the vector.

    const nv: usize = 10;
    // ... setup data ...
    
    // Inside loops:
    @memcpy(&Aar, A[i][k..(k + nv)]);
    for (0..nv) |l| {
        Bar[l] = B[k + l][j];
    }
    
    const Av: @Vector(nv, u64) = Aar;
    const Bv: @Vector(nv, u64) = Bar;
    const result = Av * Bv;
    C[i][j] += @reduce(.Add, result);
  6. Use stdout and stderr for application output

    main

    When writing command-line tools in Zig, use std.debug.print to send messages to stderr (standard error). This is ideal for debugging or status messages that should not interfere with the application's primary data output. For the actual application output (e.g., compressed bytes or processed data), use std.io.getStdOut().writer(). It is highly recommended to wrap the stdout writer in a std.io.bufferedWriter for performance, but you must explicitly call .flush() to ensure all buffered data is written to the terminal or file.

    const std = @import("std");
    
    pub fn main(init: std.process.Init) !void {
        // Use stderr for debugging/status messages
        std.debug.print("All your {s} are belong to us.\n", .{"codebase"});
    
        // Use stdout for actual application output
        const stdout_file = std.io.getStdOut().writer();
        var bw = std.io.bufferedWriter(stdout_file);
        const stdout = bw.writer();
    
        try stdout.print("Run `zig build test` to run the tests.\n", .{});
    
        // Important: flush the buffer to ensure output is sent
        try bw.flush();
    }
  7. Implement a basic HTTP server using the Zig HTTP server example

    main

    This example demonstrates how to initialize a Server, accept a connection, read an incoming Request, and send a Response using the project's HTTP modules.

    Key steps:

    1. Initialize the Server with an IO provider.
    2. Call server.listen() to start listening for connections.
    3. Use listening.accept(io) to obtain a connection.
    4. Read the request into a buffer using Request.read_request(io, connection, buffer).
    5. Parse the buffer using Request.parse_request(buffer).
    6. Inspect request.method and request.uri to determine the logic.
    7. Send responses using helper functions like Response.send_200(connection, io) or Response.send_404(connection, io).
    const std = @import("std");
    const Method = @import("request.zig").Method;
    const Request = @import("request.zig");
    const Response = @import("response.zig");
    const Server = @import("server.zig").Server;
    
    pub fn main(init: std.process.Init) !void {
        const io = init.io;
        const server = try Server.init(io);
        var listening = try server.listen();
        const connection = try listening.accept(io);
        defer connection.close(io);
    
        var request_buffer: [1000]u8 = undefined;
        @memset(request_buffer[0..], 0);
        
        try Request.read_request(io, connection, request_buffer[0..]);
        const request = Request.parse_request(request_buffer[0..]);
    
        if (request.method == Method.GET) {
            if (std.mem.eql(u8, request.uri, "/")) {
                try Response.send_200(connection, io);
            } else {
                try Response.send_404(connection, io);
            }
        }
    }
  8. Implement a main function with std.process.Init

    main

    In Zig, the entry point main can accept a std.process.Init argument. This object provides access to essential runtime resources:

    • init.arena: An allocator that lives for the duration of the process.
    • init.minimal.args: Command line arguments. Use .toSlice(allocator) to convert them into a slice.
    • init.io: An Io instance required for performing I/O operations.

    Note that for application output, you should use a dedicated writer (like stdout) rather than std.debug.print, which is intended for debugging and prints to stderr.

    pub fn main(init: std.process.Init) !void {
        const arena: std.mem.Allocator = init.arena.allocator();
        const args = try init.minimal.args.toSlice(arena);
        const io = init.io;
        // ...
    }
  9. Set up a buffered stdout writer using Io.File.Writer

    main

    To perform efficient I/O, you can wrap stdout in a buffered writer using std.Io.File.Writer. This requires an Io instance and a buffer.

    Important: Always call .flush() on the writer before the program exits to ensure all buffered data is written to the output stream.

    var stdout_buffer: [1024]u8 = undefined;
    var stdout_file_writer: Io.File.Writer = .init(.stdout(), io, &stdout_buffer);
    const stdout_writer = &stdout_file_writer.interface;
    
    try stdout_writer.write("Hello World\n");
    try stdout_writer.flush();
  10. Print a matrix with print_matrix

    main

    The print_matrix function provides a way to format and print a 2D matrix (represented as an array of slices) to standard output. It accepts a matrix of size [100][]u64 and a name string for labeling.

    fn print_matrix(matrix: [100][]u64, name: []const u8) !void {
        try stdout.print("Matrix {s}\n\n", .{name});
        for (0..matrix.len) |i| {
            const row = matrix[i];
            _ = try stdout.write("| ");
            for (0..row.len) |j| {
                try stdout.print("{d} | ", .{row[j]});
            }
            _ = try stdout.write("\n-----------------------------------------------------------------------------------\n");
        }
    }