# Zigbook LLM Dataset project: Zigbook – The Zig Programming Language Book url: https://zigbook.net repository: https://github.com/zigbook/zigbook license: MIT (see LICENSE in repository) generated_at: 2025-11-16T08:47:05.234Z zig_version: 0.15.2 notes: - This file is a human-written dataset export for LLMs and tools. - See llms.txt and LLM.md/README for usage guidelines and citation expectations. # Chapter 00 — Introduction [chapter_id: 00__zigbook_introduction] [chapter_slug: zigbook_introduction] [chapter_number: 00] [chapter_url: https://zigbook.net/chapters/00__zigbook_introduction] ## Section: Welcome to Zig [section_id: welcome] [section_url: https://zigbook.net/chapters/00__zigbook_introduction#welcome] Most programming languages hide complexity from you—they abstract away memory management, mask control flow with implicit operations, and shield you from the machine beneath. This feels simple at first, but eventually you hit a wall. You need to understand why something is slow, where a crash happened, or how to squeeze every ounce of performance from your hardware. Suddenly, the abstractions that helped you get started are now in your way. Zig takes a different path. It reveals complexity—and then gives you the tools to master it. This book will take you from `Hello, world!` to building systems that cross-compile to any platform, manage memory with surgical precision, and generate code at compile time. You will learn not just how Zig works, but why it works the way it does. Every allocation will be explicit. Every control path will be visible. Every abstraction will be precise, not vague. By the end of these sixty-one chapters, you will not just know Zig. You will understand systems programming at a level that makes other languages feel like they are hiding something from you. Because they are. This journey begins with simplicity—the kind you encounter on the first day. By the end, you will discover a different kind of simplicity: the kind you earn by climbing through complexity and emerging with complete understanding on the other side. Welcome to the Zigbook. Your transformation starts now. ## Section: What You’ll Become [section_id: what-youll-become] [section_url: https://zigbook.net/chapters/00__zigbook_introduction#what-youll-become] Learning Zig is not just about adding a language to your resume. It is about fundamentally changing how you think about software. When you finish this book, you will be able to: - Understand your programs completely. You will know where every byte lives in memory, when the compiler executes your code, and what machine instructions your abstractions compile to. No hidden allocations. No mystery overhead. No surprises. - Control the entire stack. From bare metal embedded systems to WebAssembly in the browser, from kernel modules to networked services—you will have one toolchain, one language, and complete control over how your code runs everywhere. - Debug with confidence. When something goes wrong, you will not be guessing. You will read stack traces, inspect memory layouts, verify allocator behavior, and pinpoint issues with the same tools that built the Zig compiler itself. - Build reliable systems. Through explicit error handling, resource cleanup guarantees, and safety modes that catch mistakes during development without sacrificing release performance, you will ship code you can trust. - Contribute to the future. Zig is young, evolving, and hungry for contributors. You will have the foundation to propose features, fix bugs, write libraries, and help shape a language that values clarity and correctness. You will become the developer who looks at a garbage collector and thinks, "I can do better." Who reads assembly without fear. Who cross-compiles to a new architecture without installing a separate toolchain. Who understands not just what works, but why. This is not about memorizing syntax. This is about earning mastery. ## Section: About This Book [section_id: what-is-the-zigbook] [section_url: https://zigbook.net/chapters/00__zigbook_introduction#what-is-the-zigbook] IMPORTANT: The Zigbook intentionally contains no AI-generated content—it is hand-written, carefully curated, and continuously updated to reflect the latest language features and best practices. A Quick Note from the Author: QUOTE: Hello, reader! Thank you for choosing the Zigbook as your guide to learning Zig. I would like to formally invite the Zig community to contribute to the Zigbook. Whether you spot a typo, want to improve an explanation, or have a better way to demonstrate a concept, your contributions help everyone who learns from this book. You can contribute by opening issues or pull requests here (https://github.com/zigbook/zigbook). Please Note: I review every submission personally to ensure accuracy and clarity. Together, we can make this resource even better for future Zig developers. The Zigbook was initially written by @zigbook (https://github.com/zigbook), an experienced Systems programmer and Zig community member to fill gaps in the existing resources, and share that knowledge with others. It has since grown into a a comprehensive guide to the Zig programming language, structured as a journey from fundamentals to advanced systems programming. It is designed for developers who want to understand, not just use; who value transparency over magic and precision over convenience. TIP: The Zigbook complements the official documentation by providing in-depth explanations, practical projects, and a curated learning path. Where the language reference tells you what a feature does, this book shows you when to use it, why it matters, and how it fits into real-world code. Structure: The Zigbook is organized into seven parts, alternating between concept chapters (teaching) and project chapters (applying). Early chapters intentionally defer deep dives until you have the foundation to understand them. Later chapters assume you have internalized earlier material. This is a path, not a reference manual: read it in order for the first time, then use it as a reference afterward. Prerequisites: You should be comfortable with at least one programming language and basic command-line operations. Experience with C, C++, or Rust will help you draw comparisons, but it is not required. Zig can be your first systems language if you are willing to engage deeply with the concepts. ## Section: What is Zig? [section_id: what-is-zig] [section_url: https://zigbook.net/chapters/00__zigbook_introduction#what-is-zig] Zig is a systems programming language designed for developers who need full control, efficiency, and simplicity without sacrificing safety or performance. It positions itself as a "no surprises" toolchain: every control path, allocation, and optimization decision is something you can trace, modify, or opt out of. Zig mirrors the directness of C while layering in a modern standard library, better compile-time guarantees, and first-class cross-compilation support. The language intentionally avoids "magic" features—no hidden control flow, no garbage collector, no mandatory runtime—so you can audit binaries and understand exactly what your code compiles to. ### Subsection: Core Philosophy [section_id: _core_philosophy] [section_url: https://zigbook.net/chapters/00__zigbook_introduction#_core_philosophy] Zig’s mission revolves around clarity and mechanical sympathy. The compiler trusts you to make the right decisions while providing safety nets during development. Debug builds catch overflow, use-after-free, and other mistakes. Release builds remove those checks for maximum performance. You choose the tradeoff explicitly through build modes, not through language-level compromises. The standard library embraces straightforward building blocks: files as modules, explicit allocators, and typed errors. Newcomers can reason about code without memorizing vast frameworks. This simplicity extends to tooling—`zig build`, `zig test`, and `zig run` handle most workflows, while `build.zig` scripts are just Zig code, not a separate configuration language. 22 (22__build-system-deep-dive.xml) ### Subsection: How Zig Compares [section_id: _how_zig_compares] [section_url: https://zigbook.net/chapters/00__zigbook_introduction#_how_zig_compares] Zig and C: Zig honors C’s "you are in charge" philosophy while removing undefined behavior footguns. Checked arithmetic, tagged unions, optionals, and explicit error handling replace C’s silent failures. You get the same level of control with modern syntax and better diagnostics. Zig and Rust: Where Rust enforces safety through borrow checking at compile time, Zig offers manual control with optional runtime checks. You decide when lifetimes matter and when performance trumps static enforcement. Zig’s learning curve is gentler—fewer language features to master, though you carry more responsibility. 17 (17__generic-apis-and-type-erasure.xml) Zig and Go/Python: Compared to garbage-collected languages, Zig gives you granular control over memory and performance. Its simplicity and explicit allocators make it ideal for embedded systems, kernels, and performance-critical paths. But Zig’s reach extends beyond traditional systems programming—developers use it for CLI tools, game development, WebAssembly modules, and high-performance network services. 41 (41__cross-compilation-and-wasm.xml) Zig does not try to be everything to everyone. It chooses transparency over convenience, explicitness over inference, and understanding over abstraction. If you value knowing exactly what your code does, Zig is your language. ## Section: What Zig Gives You [section_id: key-capabilities] [section_url: https://zigbook.net/chapters/00__zigbook_introduction#key-capabilities] Four capabilities define the Zig experience and appear throughout this book: v0.15.2 (https://ziglang.org/download/0.15.1/release-notes.html) 1. No hidden control flow. The compiler never injects allocators, goroutines, or implicit destructors. Machine code corresponds directly to what you wrote. When you read Zig, you know exactly what will execute. 2. Manual memory with guardrails. Allocator APIs are first-class parameters, not hidden runtime machinery. Debug and ReleaseSafe modes catch double frees, use-after-free, and buffer overflows during development. ReleaseFast strips those checks for production. You control the tradeoff. 10 (10__allocators-and-memory-management.xml) 3. Compile-time execution. Any function can run at `comptime`, turning the compiler into a metaprogramming engine. Generate lookup tables, validate schemas, or tailor generic APIs, all before the binary ships. Zero runtime cost, full language access. 15 (15__comptime-and-reflection.xml) 4. Effortless cross-compilation. The bundled toolchain targets dozens of OS/architecture pairs with a single command. No separate toolchains, no cross-compilation SDKs, no configuration files—just `-target` and go. 41 (41__cross-compilation-and-wasm.xml) These are not bullet points on a marketing slide—they are principles that shape how you write, debug, and deploy Zig code. You will encounter them in every chapter, from `Hello, world!` to building your own allocators. ## Section: Getting Started Quickly [section_id: getting-started] [section_url: https://zigbook.net/chapters/00__zigbook_introduction#getting-started] It only takes a few steps to go from download to executing Zig code. Everything else in this book assumes you have the toolchain on your `PATH`. The official downloads page (https://ziglang.org/download/) offers release binaries for Linux, macOS, and Windows. Package managers such as Homebrew and popular Linux distributions track the latest stable release, but grabbing the tarball or zip directly guarantees version parity with the examples in this book. After unpacking, confirm your installation: ```shell $ zig version 0.15.X ``` CAUTION: If `zig version` reports an earlier release, revisit the download step so the examples in forthcoming chapters align with the safety-mode behavior introduced in v0.15.2+ (https://ziglang.org/download/0.15.0/release-notes.html). ### Subsection: Your First Program [section_id: getting-started-first-program] [section_url: https://zigbook.net/chapters/00__zigbook_introduction#getting-started-first-program] Compile and run your first `main` function to verify the toolchain and standard library work as expected. Create a file called `hello_world.zig` with the following contents: ```zig const std = @import("std"); pub fn main() void { std.debug.print("Hello, world!\n", .{}); } ``` Run: ```shell $ zig run hello_world.zig ``` Output: ```shell Hello, world! ``` NOTE: `std.debug.print` writes to stderr. Chapter 1 explores buffered stdout writers when you care about output channels and syscalls. 1 (01__boot-basics.xml) ### Subsection: Exploring the Tooling Surface [section_id: getting-started-runner] [section_url: https://zigbook.net/chapters/00__zigbook_introduction#getting-started-runner] Even this minimal example showcases Zig’s uniform tooling story: the same `zig run` command handles compile, link, and execute, while `zig test` and `zig build` extend the workflow without changing languages. 22 (22__build-system-deep-dive.xml) Keep your code in `main.zig` or any filename you pass to the CLI; the root module is whatever file you invoke. ## Section: An Interactive Loop [section_id: interactive-example] [section_url: https://zigbook.net/chapters/00__zigbook_introduction#interactive-example] Once “Hello, world!” works, extend the program into a simple loop to witness Zig’s explicit control flow and formatting syntax, as described in #While (https://ziglang.org/documentation/master/#while). ```zig const std = @import("std"); pub fn main() void { var i: u32 = 1; while (i <= 10) : (i += 1) { std.debug.print("{d} squared is {d}\n", .{ i, i * i }); } } ``` Run: ```shell $ zig run squares_demo.zig ``` Output: ```shell 1 squared is 1 2 squared is 4 3 squared is 9 4 squared is 16 5 squared is 25 6 squared is 36 7 squared is 49 8 squared is 64 9 squared is 81 10 squared is 100 ``` TIP: Zig’s `while` loop allows an inline increment clause, `while (cond) : (update)`, making it easy to port C-style loops without introducing hidden iterators. ## Section: The Path Ahead [section_id: the-path-ahead] [section_url: https://zigbook.net/chapters/00__zigbook_introduction#the-path-ahead] You now have a working Zig toolchain and two small programs under your belt. This is the foundation. Everything that follows builds on this moment—the first time you compiled Zig code and saw it run. The next chapter (01__boot-basics.xml) introduces how Zig treats source files as modules, how entry points propagate errors, and how build modes transform the same code into different safety and performance profiles. You will learn that `main` is not magic: it is discovered by `std.start`, which you can bypass if needed. By Chapter 61 (61__the-simplicity-you-earned.xml), you will not just know Zig; you will understand it deeply enough to teach others, contribute to the ecosystem, and build systems that reflect your complete mastery. This journey begins with simplicity. It ends with a different kind of simplicity: the kind you earn through understanding. Your transformation starts now. Turn the page. # Chapter 01 — Boot & Basics [chapter_id: 01__boot-basics] [chapter_slug: boot-basics] [chapter_number: 01] [chapter_url: https://zigbook.net/chapters/01__boot-basics] ## Section: Overview [section_id: overview] [section_url: https://zigbook.net/chapters/01__boot-basics#overview] Zig treats every source file as a namespaced module, and the compilation model revolves around explicitly wiring those units together with `@import`, keeping dependencies and program boundaries discoverable at a glance, as described in #Compilation Model (https://ziglang.org/documentation/master/#Compilation-Model). This chapter builds the first mile of that journey by showing how the root module, `std`, and `builtin` cooperate to produce a runnable program from a single file while preserving explicit control over targets and optimization modes. We also establish the ground rules for data and execution: how `const` and `var` guide mutability, why literals such as `void {}` matter for API design, how Zig handles default overflow, and how to select the right printing surface for the job, as described in #Values (https://ziglang.org/documentation/master/#Values). Along the way, we preview the release mode variants and buffered output helpers you will rely on in later chapters; see #Build-Mode (https://ziglang.org/documentation/master/#Build-Mode). ## Section: Learning Goals [section_id: learning-goals] [section_url: https://zigbook.net/chapters/01__boot-basics#learning-goals] - Explain how Zig resolves modules through `@import` and the role of the root namespace. - Describe how `std.start` discovers `main` and why entry points commonly return `!void`, as described in #Entry Point (https://ziglang.org/documentation/master/#Entry-Point). - Use `const`, `var`, and literal forms such as `void {}` to express intent about mutability and unit values. - Choose between `std.debug.print`, unbuffered writers, and buffered stdout depending on the output channel and performance needs. ## Section: Starting from a Single Source File [section_id: section-1] [section_url: https://zigbook.net/chapters/01__boot-basics#section-1] The fastest way to get something on screen in Zig is to lean on the default module graph: the root file you compile becomes the canonical namespace, and `@import` lets you reach everything from the standard library to compiler metadata. You will use these hooks constantly to align runtime behavior with build-time decisions. ## Section: Entry Point Selection [section_id: _entry_point_selection] [section_url: https://zigbook.net/chapters/01__boot-basics#_entry_point_selection] The Zig compiler exports different entry point symbols based on the target platform, linking mode, and user declarations. This selection happens at compile time in lib/std/start.zig:28-100. ### Subsection: Entry Point Symbol Table [section_id: _entry_point_symbol_table] [section_url: https://zigbook.net/chapters/01__boot-basics#_entry_point_symbol_table] | Platform | Link Mode | Conditions | Exported Symbol | Handler Function | | --- | --- | --- | --- | --- | | POSIX/Linux | Executable | Default | `_start` | `_start()` | | POSIX/Linux | Executable | Linking libc | `main` | `main()` | | Windows | Executable | Default | `wWinMainCRTStartup` | `WinStartup()` / `wWinMainCRTStartup()` | | Windows | Dynamic Library | Default | `_DllMainCRTStartup` | `_DllMainCRTStartup()` | | UEFI | Executable | Default | `EfiMain` | `EfiMain()` | | WASI | Executable (command) | Default | `_start` | `wasi_start()` | | WASI | Executable (reactor) | Default | `_initialize` | `wasi_start()` | | WebAssembly | Freestanding | Default | `_start` | `wasm_freestanding_start()` | | WebAssembly | Linking libc | Default | `__main_argc_argv` | `mainWithoutEnv()` | | OpenCL/Vulkan | Kernel | Default | `main` | `spirvMain2()` | | MIPS | Any | Default | `__start` | (same as `_start`) | ### Subsection: Compile-Time Entry Point Logic [section_id: _compile_time_entry_point_logic] [section_url: https://zigbook.net/chapters/01__boot-basics#_compile_time_entry_point_logic] ```text graph TB Start["comptime block
(start.zig:28)"] CheckMode["Check builtin.output_mode"] CheckSimplified["simplified_logic?
(stage2 backends)"] CheckLinkC["link_libc or
object_format == .c?"] CheckWindows["builtin.os == .windows?"] CheckUEFI["builtin.os == .uefi?"] CheckWASI["builtin.os == .wasi?"] CheckWasm["arch.isWasm() &&
os == .freestanding?"] ExportMain["@export(&main, 'main')"] ExportWinMain["@export(&WinStartup,
'wWinMainCRTStartup')"] ExportStart["@export(&_start, '_start')"] ExportEfi["@export(&EfiMain, 'EfiMain')"] ExportWasi["@export(&wasi_start,
wasm_start_sym)"] ExportWasmStart["@export(&wasm_freestanding_start,
'_start')"] Start --> CheckMode CheckMode -->|".Exe or has main"| CheckSimplified CheckSimplified -->|"true"| Simple["Simplified logic
(lines 33-51)"] CheckSimplified -->|"false"| CheckLinkC CheckLinkC -->|"yes"| ExportMain CheckLinkC -->|"no"| CheckWindows CheckWindows -->|"yes"| ExportWinMain CheckWindows -->|"no"| CheckUEFI CheckUEFI -->|"yes"| ExportEfi CheckUEFI -->|"no"| CheckWASI CheckWASI -->|"yes"| ExportWasi CheckWASI -->|"no"| CheckWasm CheckWasm -->|"yes"| ExportWasmStart CheckWasm -->|"no"| ExportStart ``` ### Subsection: Modules and Imports [section_id: section-1-sub-a] [section_url: https://zigbook.net/chapters/01__boot-basics#section-1-sub-a] The root module is just your top-level file, so any declarations you mark `pub` are immediately re-importable under `@import("root")`. Pair that with `@import("builtin")` to inspect the target chosen by your current compiler invocation, as described in #Builtin-Functions (https://ziglang.org/documentation/master/#Builtin-Functions). ```zig // File: chapters-data/code/01__boot-basics/imports.zig // Import the standard library for I/O, memory management, and core utilities const std = @import("std"); // Import builtin to access compile-time information about the build environment const builtin = @import("builtin"); // Import root to access declarations from the root source file // In this case, we reference app_name which is defined in this file const root = @import("root"); // Public constant that can be accessed by other modules importing this file pub const app_name = "Boot Basics Tour"; // Main entry point of the program // Returns an error union to propagate any I/O errors during execution pub fn main() !void { // Allocate a fixed-size buffer on the stack for stdout operations // This buffer batches write operations to reduce syscalls var stdout_buffer: [256]u8 = undefined; // Create a buffered writer wrapping stdout var stdout_writer = std.fs.File.stdout().writer(&stdout_buffer); // Get the generic writer interface for polymorphic I/O operations const stdout = &stdout_writer.interface; // Print the application name by referencing the root module's declaration // Demonstrates how @import("root") allows access to the entry file's public declarations try stdout.print("app: {s}\n", .{root.app_name}); // Print the optimization mode (Debug, ReleaseSafe, ReleaseFast, or ReleaseSmall) // @tagName converts the enum value to its string representation try stdout.print("optimize mode: {s}\n", .{@tagName(builtin.mode)}); // Print the target triple showing CPU architecture, OS, and ABI // Each component is extracted from builtin.target and converted to a string try stdout.print( "target: {s}-{s}-{s}\n", .{ @tagName(builtin.target.cpu.arch), @tagName(builtin.target.os.tag), @tagName(builtin.target.abi), }, ); // Flush the buffer to ensure all accumulated output is written to stdout try stdout.flush(); } ``` Run: ```shell $ zig run imports.zig ``` Output: ```shell app: Boot Basics Tour optimize mode: Debug target: x86_64-linux-gnu ``` Actual target identifiers depend on your host triple; the important part is seeing how `@tagName` exposes each enum so you can branch on them later. Because the buffered stdout writer batches data, always call `flush()` before exiting so the terminal receives the final line. TIP: Reach for `@import("root")` to surface configuration constants without baking extra globals into your namespace. ### Subsection: Entry Points and Early Errors [section_id: section-1-sub-b] [section_url: https://zigbook.net/chapters/01__boot-basics#section-1-sub-b] Zig’s runtime glue (`std.start`) looks for a `pub fn main`, forwards command-line state, and treats an error return as a signal to abort with diagnostics. Because `main` commonly performs I/O, giving it the `!void` return type keeps error propagation explicit. ```zig // File: chapters-data/code/01__boot-basics/entry_point.zig // Import the standard library for I/O and utility functions const std = @import("std"); // Import builtin to access compile-time information like build mode const builtin = @import("builtin"); // Define a custom error type for build mode violations const ModeError = error{ReleaseOnly}; // Main entry point of the program // Returns an error union to propagate any errors that occur during execution pub fn main() !void { // Attempt to enforce debug mode requirement // If it fails, catch the error and print a warning instead of terminating requireDebugSafety() catch |err| { std.debug.print("warning: {s}\n", .{@errorName(err)}); }; // Print startup message to stdout try announceStartup(); } // Validates that the program is running in Debug mode // Returns an error if compiled in Release mode to demonstrate error handling fn requireDebugSafety() ModeError!void { // Check compile-time build mode if (builtin.mode == .Debug) return; // Return error if not in Debug mode return ModeError.ReleaseOnly; } // Writes a startup announcement message to standard output // Demonstrates buffered I/O operations in Zig fn announceStartup() !void { // Allocate a fixed-size buffer on the stack for stdout operations var stdout_buffer: [128]u8 = undefined; // Create a buffered writer wrapping stdout var stdout_writer = std.fs.File.stdout().writer(&stdout_buffer); // Get the generic writer interface for polymorphic I/O const stdout = &stdout_writer.interface; // Write formatted message to the buffer try stdout.print("Zig entry point reporting in.\n", .{}); // Flush the buffer to ensure message is written to stdout try stdout.flush(); } ``` Run: ```shell $ zig run entry_point.zig ``` Output: ```shell Zig entry point reporting in. ``` In release modes (`zig run -OReleaseFast …​`), the `ModeError.ReleaseOnly` branch fires and the warning surfaces before the program continues, neatly demonstrating how `catch` converts errors into user-facing diagnostics without suppressing later work. ### Subsection: How Return Types Are Handled [section_id: section-1-sub-b2] [section_url: https://zigbook.net/chapters/01__boot-basics#section-1-sub-b2] Zig’s startup code in `std.start` inspects your `main()` function’s return type at compile time and generates appropriate handling logic. This flexibility allows you to choose the signature that best fits your program’s needs—whether you want simple success/failure semantics with `!void`, explicit exit codes with `u8`, or an infinite event loop with `noreturn`. The `callMain()` function orchestrates this dispatch, ensuring errors are logged and exit codes propagate correctly to the operating system. ### Subsection: callMain Return Type Handling [section_id: _callmain_return_type_handling] [section_url: https://zigbook.net/chapters/01__boot-basics#_callmain_return_type_handling] The `callMain()` function handles different return type signatures from the user’s `main()`: ```text graph TB Start["callMain()"] GetRetType["ReturnType = @TypeOf(root.main)
.return_type"] CheckType["switch ReturnType"] Void["void"] CallVoid["root.main()
return 0"] NoReturn["noreturn"] CallNoReturn["return root.main()"] U8["u8"] CallU8["return root.main()"] ErrorUnion["error union"] CheckInner["@TypeOf(result)?"] InnerVoid["void"] ReturnZero["return 0"] InnerU8["u8"] ReturnResult["return result"] Invalid["@compileError"] CallCatch["result = root.main()
catch |err|"] LogError["Log error name
and stack trace
(lines 707-712)"] ReturnOne["return 1"] Start --> GetRetType GetRetType --> CheckType CheckType --> Void CheckType --> NoReturn CheckType --> U8 CheckType --> ErrorUnion CheckType --> Invalid Void --> CallVoid NoReturn --> CallNoReturn U8 --> CallU8 ErrorUnion --> CallCatch CallCatch --> CheckInner CallCatch --> LogError LogError --> ReturnOne CheckInner --> InnerVoid CheckInner --> InnerU8 CheckInner --> Invalid InnerVoid --> ReturnZero InnerU8 --> ReturnResult ``` Valid return types from `main()`: - `void` - Returns exit code 0 - `noreturn` - Never returns (infinite loop or explicit exit) - `u8` - Returns exit code directly - `!void` - Returns 0 on success, 1 on error (logs error with stack trace) - `!u8` - Returns exit code on success, 1 on error (logs error with stack trace) The `!void` signature used in our examples provides the best balance: explicit error handling with automatic logging and appropriate exit codes. ### Subsection: Naming and Scope Preview [section_id: section-1-sub-c] [section_url: https://zigbook.net/chapters/01__boot-basics#section-1-sub-c] Variables obey lexical scope: every block introduces a new region where you may shadow or extend bindings, while `const` and `var` signal immutability versus mutability and help the compiler reason about safety, as described in #Blocks (https://ziglang.org/documentation/master/#Blocks). Zig defers a deeper discussion of style and shadowing to Chapter 38, but keep in mind that thoughtful naming at the top level (often via `pub const`) is the idiomatic way to share configuration between files; see #Variables (https://ziglang.org/documentation/master/#Variables). ## Section: Working with Values and Builds [section_id: section-2] [section_url: https://zigbook.net/chapters/01__boot-basics#section-2] Once you have an entry point, the next stop is data: numeric types come in explicitly sized flavors (`iN`, `uN`, `fN`), literals infer their type from context, and Zig uses debug safety checks to trap overflows unless you opt into wrapping or saturating operators. Build modes (`-O` flags) decide which checks remain in place and how aggressively the compiler optimizes. ### Subsection: Optimization Modes [section_id: section-2-sub-a] [section_url: https://zigbook.net/chapters/01__boot-basics#section-2-sub-a] Zig provides four optimization modes that control the trade-offs between code speed, binary size, and safety checks: | Mode | Priority | Safety Checks | Speed | Binary Size | Use Case | | --- | --- | --- | --- | --- | --- | | `Debug` | Safety + Debug Info | ✓ All enabled | Slowest | Largest | Development and debugging | | `ReleaseSafe` | Speed + Safety | ✓ All enabled | Fast | Large | Production with safety | | `ReleaseFast` | Maximum Speed | ✗ Disabled | Fastest | Medium | Performance-critical production | | `ReleaseSmall` | Minimum Size | ✗ Disabled | Fast | Smallest | Embedded systems, size-constrained | The optimization mode is specified via the `-O` flag and affects: - Runtime safety checks (overflow, bounds checking, null checks) - Stack traces and debug information generation - LLVM optimization level (when using the LLVM backend) - Inlining heuristics and code generation strategies ```text graph TB subgraph "Optimization Mode Effects" OptMode["optimize_mode: OptimizeMode"] OptMode --> SafetyChecks["Runtime Safety Checks"] OptMode --> DebugInfo["Debug Information"] OptMode --> CodegenStrategy["Codegen Strategy"] OptMode --> LLVMOpt["LLVM Optimization Level"] SafetyChecks --> Overflow["Integer overflow checks"] SafetyChecks --> Bounds["Bounds checking"] SafetyChecks --> Null["Null pointer checks"] SafetyChecks --> Unreachable["Unreachable assertions"] DebugInfo --> StackTraces["Stack traces"] DebugInfo --> DWARF["DWARF debug info"] DebugInfo --> LineInfo["Source line information"] CodegenStrategy --> Inlining["Inlining heuristics"] CodegenStrategy --> Unrolling["Loop unrolling"] CodegenStrategy --> Vectorization["SIMD vectorization"] LLVMOpt --> O0["Debug: -O0"] LLVMOpt --> O2Safe["ReleaseSafe: -O2 + safety"] LLVMOpt --> O3["ReleaseFast: -O3"] LLVMOpt --> Oz["ReleaseSmall: -Oz"] end ``` In this chapter we use `Debug` (the default) for development and preview `ReleaseFast` to demonstrate how optimization choices affect behavior and binary characteristics. ### Subsection: Values, Literals, and Debug Printing [section_id: workflow-1] [section_url: https://zigbook.net/chapters/01__boot-basics#workflow-1] `std.debug.print` writes to stderr and is perfect for early experiments; it accepts any value you throw at it, revealing how `@TypeOf` and friends reflect on literals. ```zig // File: chapters-data/code/01__boot-basics/values_and_literals.zig const std = @import("std"); pub fn main() !void { // Declare a mutable variable with explicit type annotation // u32 is an unsigned 32-bit integer, initialized to 1 var counter: u32 = 1; // Declare an immutable constant with inferred type (comptime_int) // The compiler infers the type from the literal value 2 const increment = 2; // Declare a constant with explicit floating-point type // f64 is a 64-bit floating-point number const ratio: f64 = 0.5; // Boolean constant with inferred type // Demonstrates Zig's type inference for simple literals const flag = true; // Character literal representing a newline // Single-byte characters are u8 values in Zig const newline: u8 = '\n'; // The unit type value, analogous to () in other languages // Represents "no value" or "nothing" explicitly const unit_value = void{}; // Mutate the counter by adding the increment // Only var declarations can be modified counter += increment; // Print formatted output showing different value types // {} is a generic format specifier that works with any type std.debug.print("counter={} ratio={} safety={}\n", .{ counter, ratio, flag }); // Cast the newline byte to u32 for display as its ASCII decimal value // @as performs explicit type coercion std.debug.print("newline byte={} (ASCII)\n", .{@as(u32, newline)}); // Use compile-time reflection to print the type name of unit_value // @TypeOf gets the type, @typeName converts it to a string std.debug.print("unit literal has type {s}\n", .{@typeName(@TypeOf(unit_value))}); } ``` Run: ```shell $ zig run values_and_literals.zig ``` Output: ```shell counter=3 ratio=0.5 safety=true newline byte=10 (ASCII) unit literal has type void ``` Treat `void {}` as a communicative literal indicating "nothing to configure," and remember that debug prints default to stderr so they never interfere with stdout pipelines. ### Subsection: Buffering stdout and Build Modes [section_id: workflow-2] [section_url: https://zigbook.net/chapters/01__boot-basics#workflow-2] When you want deterministic stdout with fewer syscalls, borrow a buffer and flush once—especially in release profiles where throughput matters. The example below shows how to set up a buffered writer around `std.fs.File.stdout()` and highlights the differences between build modes. ```zig // File: chapters-data/code/01__boot-basics/buffered_stdout.zig const std = @import("std"); pub fn main() !void { // Allocate a 256-byte buffer on the stack for output batching // This buffer accumulates write operations to minimize syscalls var stdout_buffer: [256]u8 = undefined; // Create a buffered writer wrapping stdout // The writer batches output into stdout_buffer before making syscalls var writer_state = std.fs.File.stdout().writer(&stdout_buffer); const stdout = &writer_state.interface; // These print calls write to the buffer, not directly to the terminal // No syscalls occur yet—data accumulates in stdout_buffer try stdout.print("Buffering saves syscalls.\n", .{}); try stdout.print("Flush once at the end.\n", .{}); // Explicitly flush the buffer to write all accumulated data at once // This triggers a single syscall instead of one per print operation try stdout.flush(); } ``` Run: ```shell $ zig build-exe buffered_stdout.zig -OReleaseFast $ $ ./buffered_stdout ``` Output: ```shell Buffering saves syscalls. Flush once at the end. ``` Using a buffered writer mirrors the standard library’s own initialization template and keeps writes cohesive; always flush before exiting to guarantee the OS sees your final message. ## Section: Notes & Caveats [section_id: notes-caveats] [section_url: https://zigbook.net/chapters/01__boot-basics#notes-caveats] - `std.debug.print` targets stderr and bypasses stdout buffering, so reserve it for diagnostics even in simple tools. - Wrapping (`%`) and saturating (`|`) arithmetic are available when you deliberately want to skip overflow traps; the default operators still panic in Debug mode to catch mistakes early, as documented in #Operators (https://ziglang.org/documentation/master/#Operators). - `std.fs.File.stdout().writer(&buffer)` mirrors the patterns used by `zig init` and requires an explicit `flush()` to push buffered bytes downstream. ## Section: Exercises [section_id: exercises] [section_url: https://zigbook.net/chapters/01__boot-basics#exercises] - Extend `imports.zig` to print the pointer size reported by `@sizeOf(usize)` and compare targets by toggling `-Dtarget` values on the command line. - Refactor `entry_point.zig` so that `requireDebugSafety` returns a descriptive error union (`error{ReleaseOnly}![]const u8`) and have `main` write the message to stdout before rethrowing. - Build `buffered_stdout.zig` with `-OReleaseSafe` and `-OReleaseSmall`, measuring the binary sizes to see how optimization choices impact deployment footprints. # Chapter 02 — Control Flow Essentials [chapter_id: 02__control-flow-essentials] [chapter_slug: control-flow-essentials] [chapter_number: 02] [chapter_url: https://zigbook.net/chapters/02__control-flow-essentials] ## Section: Overview [section_id: overview] [section_url: https://zigbook.net/chapters/02__control-flow-essentials#overview] Chapter 1 established the building blocks for running a Zig program and working with data; now we turn those values into decisions by walking through the language’s control-flow primitives, as described in #if (https://ziglang.org/documentation/master/#if). Control flow in Zig is expression-oriented, so choosing a branch or looping often produces a value instead of merely guiding execution. We explore the semantics behind loops, labeled flow, and `switch`, emphasizing how `break`, `continue`, and `else` clauses communicate intent in both safe and release builds; see #While (https://ziglang.org/documentation/master/#while), #for (https://ziglang.org/documentation/master/#for), and #switch (https://ziglang.org/documentation/master/#switch). ## Section: Learning Goals [section_id: learning-goals] [section_url: https://zigbook.net/chapters/02__control-flow-essentials#learning-goals] - Use `if` expressions (with optional payload capture) to derive values while handling missing data paths explicitly. - Combine `while`/`for` loops with labeled `break`/`continue` to manage nested iteration and exit conditions clearly. - Apply `switch` to enumerate exhaustive decision tables, including ranges, multiple values, and enumerations. - Leverage loop `else` clauses and labeled breaks to return values directly from iteration constructs. ## Section: What Happens to Control Flow Code [section_id: pipeline-overview] [section_url: https://zigbook.net/chapters/02__control-flow-essentials#pipeline-overview] Before diving into control flow syntax, it is helpful to understand what the compiler does with your `if`, `while`, and `switch` statements. Zig transforms source code through multiple intermediate representations (IRs), each serving a specific purpose: ```text graph LR SOURCE["Source Code
.zig files"] TOKENS["Token Stream"] AST["AST
(Ast.zig)"] ZIR["ZIR
(Zir)"] AIR["AIR
(Air.zig)"] MIR["MIR
(codegen.AnyMir)"] MACHINE["Machine Code"] SOURCE -->|"tokenizer.zig"| TOKENS TOKENS -->|"Parse.zig"| AST AST -->|"AstGen.zig"| ZIR ZIR -->|"Sema.zig"| AIR AIR -->|"codegen.generateFunction()"| MIR MIR -->|"codegen.emitFunction()"| MACHINE ``` | IR Stage | Representation | Key Properties | Purpose for Control Flow | | --- | --- | --- | --- | | Tokens | Flat token stream | Raw lexical analysis | Recognizes `if`, `while`, `switch` keywords | | AST | Tree structure | Syntax-correct, untyped | Preserves structure of nested control flow | | ZIR | Instruction-based IR | Untyped, single SSA form per declaration | Lowers control flow to blocks and branches | | AIR | Instruction-based IR | Fully typed, single SSA form per function | Type-checked branches with known outcomes | | MIR | Backend-specific IR | Near machine code, register-allocated | Converts to jumps and conditional instructions | The control flow constructs you write—`if` expressions, `switch` statements, labeled loops—are systematically lowered through these stages. By the time your code reaches machine code, a `switch` has become a jump table, and a `while` loop is a conditional branch instruction. The diagrams in this chapter show how this lowering happens at the ZIR stage, where control flow becomes explicit blocks and branches. ## Section: Core Control Structures [section_id: section-1] [section_url: https://zigbook.net/chapters/02__control-flow-essentials#section-1] Control flow in Zig treats blocks and loops as expressions, which means each construct can yield a value and participate directly in assignment or return statements. This section steps through conditionals, loops, and `switch`, showing how each fits into the expression model while keeping readability high, as described in #Blocks (https://ziglang.org/documentation/master/#Blocks). ### Subsection: Conditionals as Expressions [section_id: section-1-sub-a] [section_url: https://zigbook.net/chapters/02__control-flow-essentials#section-1-sub-a] `if` evaluates to the value of whichever branch runs, and the optional capture form (`if (opt) |value|`) is a concise way to unwrap optionals without shadowing earlier names. Nested labeled blocks (`blk: { …​ }`) let you choose among multiple outcomes while still returning a single value. ```zig // File: chapters-data/code/02__control-flow-essentials/branching.zig // Demonstrates Zig's control flow and optional handling capabilities const std = @import("std"); /// Determines a descriptive label for an optional integer value. /// Uses labeled blocks to handle different numeric cases cleanly. /// Returns a string classification based on the value's properties. fn chooseLabel(value: ?i32) []const u8 { // Unwrap the optional value using payload capture syntax return if (value) |v| blk: { // Check for zero first if (v == 0) break :blk "zero"; // Positive numbers if (v > 0) break :blk "positive"; // All remaining cases are negative break :blk "negative"; } else "missing"; // Handle null case } pub fn main() !void { // Array containing both present and absent (null) values const samples = [_]?i32{ 5, 0, null, -3 }; // Iterate through samples with index capture for (samples, 0..) |item, index| { // Classify each sample value const label = chooseLabel(item); // Display the index and corresponding label std.debug.print("sample {d}: {s}\n", .{ index, label }); } } ``` Run: ```shell $ zig run branching.zig ``` Output: ```shell sample 0: positive sample 1: zero sample 2: missing sample 3: negative ``` The function returns a `[]const u8` because the `if` expression itself produces the string, stressing how expression-oriented branching keeps call sites compact. The `samples` loop shows that `for` can iterate with an index tuple `(item, index)` yet still rely on the upstream expression to format output. ### Subsection: How if-else Expressions Lower to ZIR [section_id: _how_if_else_expressions_lower_to_zir] [section_url: https://zigbook.net/chapters/02__control-flow-essentials#_how_if_else_expressions_lower_to_zir] When the compiler encounters an `if` expression, it transforms it into blocks and conditional branches in ZIR (Zig Intermediate Representation). The exact lowering depends on whether a result location is needed; see result location (60__advanced-result-location-semantics.xml#patterns): ```text graph TB IfNode["if (cond) then_expr else else_expr"] --> EvalCond["Evaluate condition"] EvalCond --> CheckRL["Result location needed?"] CheckRL -->|No RL| SimpleIf["Generate condbr
Two blocks with breaks"] CheckRL -->|With RL| BlockIf["Generate block_inline
Shared result pointer"] SimpleIf --> ThenBlock["then_block:
eval then_expr
break value"] SimpleIf --> ElseBlock["else_block:
eval else_expr
break value"] BlockIf --> AllocResult["alloc_inferred"] BlockIf --> ThenBlockRL["then_block:
write to result ptr"] BlockIf --> ElseBlockRL["else_block:
write to result ptr"] ``` When you write `const result = if (x > 0) "positive" else "negative"`, the compiler creates two blocks (one for each branch) and uses `break` statements to return the chosen value. This is why `if` expressions can participate in assignments—they compile to blocks that yield values through their break statements. ### Subsection: While and For Loops with Labels [section_id: section-1-sub-b] [section_url: https://zigbook.net/chapters/02__control-flow-essentials#section-1-sub-b] Loops in Zig can deliver values directly by pairing a `break` result with the loop’s `else` clause, which fires when execution completes without breaking. Labeled loops (`outer: while (…​)`) coordinate nested iteration so you can exit early or skip work without temporary booleans. ```zig // File: chapters-data/code/02__control-flow-essentials/loop_labels.zig // Demonstrates labeled loops and while-else constructs in Zig const std = @import("std"); /// Searches for the first row where both elements are even numbers. /// Uses a while loop with continue statements to skip invalid rows. /// Returns the zero-based index of the matching row, or null if none found. fn findAllEvenPair(rows: []const [2]i32) ?usize { // Track current row index during iteration var row: usize = 0; // while-else construct: break provides value, else provides fallback const found = while (row < rows.len) : (row += 1) { // Extract current pair for examination const pair = rows[row]; // Skip row if first element is odd if (@mod(pair[0], 2) != 0) continue; // Skip row if second element is odd if (@mod(pair[1], 2) != 0) continue; // Both elements are even: return this row's index break row; } else null; // No matching row found after exhausting all rows return found; } pub fn main() !void { // Test data containing pairs of integers with mixed even/odd values const grid = [_][2]i32{ .{ 3, 7 }, // Both odd .{ 2, 4 }, // Both even (target) .{ 5, 6 }, // Mixed }; // Search for first all-even pair and report result if (findAllEvenPair(&grid)) |row| { std.debug.print("first all-even row: {d}\n", .{row}); } else { std.debug.print("no all-even rows\n", .{}); } // Demonstrate labeled loop for multi-level break control var attempts: usize = 0; // Label the outer while loop to enable breaking from nested for loop outer: while (attempts < grid.len) : (attempts += 1) { // Iterate through columns of current row with index capture for (grid[attempts], 0..) |value, column| { // Check if target value is found if (value == 4) { // Report location of target value std.debug.print( "found target value at row {d}, column {d}\n", .{ attempts, column }, ); // Break out of both loops using the outer label break :outer; } } } } ``` Run: ```shell $ zig run loop_labels.zig ``` Output: ```shell first all-even row: 1 found target value at row 1, column 1 ``` The `while` loop’s `else null` captures the "no match" case without extra state, and the labeled `break :outer` instantly exits both loops once the target is found. This pattern keeps state handling tight while remaining explicit about the control transfer. ### Subsection: How Loops Lower to ZIR [section_id: _how_loops_lower_to_zir] [section_url: https://zigbook.net/chapters/02__control-flow-essentials#_how_loops_lower_to_zir] Loops are transformed into labeled blocks with explicit break and continue targets. This is what makes labeled breaks and loop `else` clauses possible: ```text graph TB Loop["while/for"] --> LoopLabel["Create labeled block"] LoopLabel --> Condition["Generate loop condition"] Condition --> Body["Generate loop body"] Body --> Continue["Generate continue expression"] LoopLabel --> BreakTarget["break_block target"] Body --> ContinueTarget["continue_block target"] Continue --> CondCheck["Jump back to condition"] ``` When you write `outer: while (x < 10)`, the compiler creates: - break_block: The target for `break :outer` statements—exits the loop - continue_block: The target for `continue :outer` statements—jumps to the next iteration - Loop body: Contains your code, with access to both targets This is why you can nest loops and use labeled breaks to exit to a specific level—each loop label creates its own break_block in ZIR. The loop `else` clause is attached to the break_block and only executes when the loop completes without breaking. ### Subsection: for Exhaustive Decisions [section_id: section-1-sub-c] [section_url: https://zigbook.net/chapters/02__control-flow-essentials#section-1-sub-c] `switch` checks values exhaustively—covering literals, ranges, and enums—and the compiler enforces totality unless you provide an `else` branch. Combining `switch` with helper functions is a clean way to centralize categorization logic. ```zig // File: chapters-data/code/02__control-flow-essentials/switch_examples.zig // Import the standard library for I/O operations const std = @import("std"); // Define an enum representing different compilation modes const Mode = enum { fast, safe, tiny }; /// Converts a numeric score into a descriptive text message. /// Demonstrates switch expressions with ranges, multiple values, and catch-all cases. /// Returns a string literal describing the score's progress level. fn describeScore(score: u8) []const u8 { return switch (score) { 0 => "no progress", // Exact match for zero 1...3 => "warming up", // Range syntax: matches 1, 2, or 3 4, 5 => "halfway there", // Multiple discrete values 6...9 => "almost done", // Range: matches 6 through 9 10 => "perfect run", // Maximum valid score else => "out of range", // Catch-all for any other value }; } pub fn main() !void { // Array of test scores to demonstrate switch behavior const samples = [_]u8{ 0, 2, 5, 8, 10, 12 }; // Iterate through each score and print its description for (samples) |score| { std.debug.print("{d}: {s}\n", .{ score, describeScore(score) }); } // Demonstrate switch with enum values const mode: Mode = .safe; // Switch on enum to assign different numeric factors based on mode // All enum cases must be handled (exhaustive matching) const factor = switch (mode) { .fast => 32, // Optimization for speed .safe => 16, // Balanced mode .tiny => 4, // Optimization for size }; // Print the selected mode and its corresponding factor std.debug.print("mode {s} -> factor {d}\n", .{ @tagName(mode), factor }); } ``` Run: ```shell $ zig run switch_examples.zig ``` Output: ```shell 0: no progress 2: warming up 5: halfway there 8: almost done 10: perfect run 12: out of range mode safe -> factor 16 ``` Every `switch` must account for all possibilities—once every tag is covered, the compiler verifies there is no missing case. Enumerations eliminate magic numbers while still letting you branch on compile-time-known variants. ### Subsection: How Expressions Lower to ZIR [section_id: _how_switch_expressions_lower_to_zir] [section_url: https://zigbook.net/chapters/02__control-flow-essentials#_how_switch_expressions_lower_to_zir] The compiler transforms `switch` statements into a structured block that handles all cases exhaustively. Range cases, multiple values per prong, and payload captures are all encoded in the ZIR representation: ```text graph TB Switch["switch (target) { ... }"] --> EvalTarget["Evaluate target operand"] EvalTarget --> Prongs["Process switch prongs"] Prongs --> Multi["Multiple cases per prong"] Prongs --> Range["Range cases (a...b)"] Prongs --> Capture["Capture payload"] Multi --> SwitchBlock["Generate switch_block"] Range --> SwitchBlock Capture --> SwitchBlock SwitchBlock --> ExtraData["Store in extra:
- prong count
- case items
- prong bodies"] ``` Exhaustiveness checking happens during semantic analysis (after ZIR generation) when types are known. The compiler verifies that: - All enum tags are covered (or an `else` branch exists) - Integer ranges don’t overlap - No unreachable prongs exist This is why you cannot accidentally forget a case in a `switch` over an enum—the type system ensures totality at compile time. Range syntax like `0…​5` is encoded in the ZIR as a range case, not as individual values. ## Section: Workflow Patterns [section_id: section-2] [section_url: https://zigbook.net/chapters/02__control-flow-essentials#section-2] Combining these constructs unlocks more expressive pipelines: loops gather or filter data, `switch` routes actions, and loop labels keep nested flows precise without introducing mutable sentinels. This section chains the primitives into reusable patterns you can adapt for parsing, simulation, or state machines. ### Subsection: Script Processing with Values [section_id: workflow-1] [section_url: https://zigbook.net/chapters/02__control-flow-essentials#workflow-1] This example interprets a mini instruction stream, using a labeled `for` loop to maintain a running total and stop when a threshold is reached. The `switch` handles command dispatch, including a deliberate `unreachable` when an unknown tag appears during development. ```zig // File: chapters-data/code/02__control-flow-essentials/script_runner.zig // Demonstrates advanced control flow: switch expressions, labeled loops, // and early termination based on threshold conditions const std = @import("std"); /// Enumeration of all possible action types in the script processor const Action = enum { add, skip, threshold, unknown }; /// Represents a single processing step with an associated action and value const Step = struct { tag: Action, value: i32, }; /// Contains the final state after script execution completes or terminates early const Outcome = struct { index: usize, // Step index where processing stopped total: i32, // Accumulated total at termination }; /// Maps single-character codes to their corresponding Action enum values. /// Returns .unknown for unrecognized codes to maintain exhaustive handling. fn mapCode(code: u8) Action { return switch (code) { 'A' => .add, 'S' => .skip, 'T' => .threshold, else => .unknown, }; } /// Executes a sequence of steps, accumulating values and checking threshold limits. /// Processing stops early if a threshold step finds the total meets or exceeds the limit. /// Returns an Outcome containing the stop index and final accumulated total. fn process(script: []const Step, limit: i32) Outcome { // Running accumulator for add operations var total: i32 = 0; // for-else construct: break provides early termination value, else provides completion value const stop = outer: for (script, 0..) |step, index| { // Dispatch based on the current step's action type switch (step.tag) { // Add operation: accumulate the step's value to the running total .add => total += step.value, // Skip operation: bypass this step without modifying state .skip => continue :outer, // Threshold check: terminate early if limit is reached or exceeded .threshold => { if (total >= limit) break :outer Outcome{ .index = index, .total = total }; // Threshold not met: continue to next step continue :outer; }, // Safety assertion: unknown actions should never appear in validated scripts .unknown => unreachable, } } else Outcome{ .index = script.len, .total = total }; // Normal completion after all steps return stop; } pub fn main() !void { // Define a script sequence demonstrating all action types const script = [_]Step{ .{ .tag = mapCode('A'), .value = 2 }, // Add 2 → total: 2 .{ .tag = mapCode('S'), .value = 0 }, // Skip (no effect) .{ .tag = mapCode('A'), .value = 5 }, // Add 5 → total: 7 .{ .tag = mapCode('T'), .value = 6 }, // Threshold check (7 >= 6: triggers early exit) .{ .tag = mapCode('A'), .value = 10 }, // Never executed due to early termination }; // Execute the script with a threshold limit of 6 const outcome = process(&script, 6); // Report where execution stopped and the final accumulated value std.debug.print( "stopped at step {d} with total {d}\n", .{ outcome.index, outcome.total }, ); } ``` Run: ```shell $ zig run script_runner.zig ``` Output: ```shell stopped at step 3 with total 7 ``` The `break :outer` returns a full `Outcome` struct, making the loop act like a search that either finds its target or falls back to the loop’s `else`. The explicit `unreachable` documents assumptions for future contributors and activates safety checks in debug builds. ### Subsection: Loop Guards and Early Termination [section_id: workflow-2] [section_url: https://zigbook.net/chapters/02__control-flow-essentials#workflow-2] Sometimes the data itself signals when to stop. This walkthrough identifies the first negative number, then accumulates even values until a `0` sentinel appears, demonstrating loop `else` clauses, labeled `continue`, and conventional `break`. ```zig // File: chapters-data/code/02__control-flow-essentials/range_scan.zig // Demonstrates while loops with labeled breaks and continue statements const std = @import("std"); pub fn main() !void { // Sample data array containing mixed positive, negative, and zero values const data = [_]i16{ 12, 5, 9, -1, 4, 0 }; // Search for the first negative value in the array var index: usize = 0; // while-else construct: break provides value, else provides fallback const first_negative = while (index < data.len) : (index += 1) { // Check if current element is negative if (data[index] < 0) break index; } else null; // No negative value found after scanning entire array // Report the result of the negative value search if (first_negative) |pos| { std.debug.print("first negative at index {d}\n", .{pos}); } else { std.debug.print("no negatives in sequence\n", .{}); } // Accumulate sum of even numbers until encountering zero var sum: i64 = 0; var count: usize = 0; // Label the loop to enable explicit break targeting accumulate: while (count < data.len) : (count += 1) { const value = data[count]; // Stop accumulation if zero is encountered if (value == 0) { std.debug.print("encountered zero, breaking out\n", .{}); break :accumulate; } // Skip odd values using labeled continue if (@mod(value, 2) != 0) continue :accumulate; // Add even values to the running sum sum += value; } // Display the accumulated sum of even values before zero std.debug.print("sum of even prefix values = {d}\n", .{sum}); } ``` Run: ```shell $ zig run range_scan.zig ``` Output: ```shell first negative at index 3 encountered zero, breaking out sum of even prefix values = 16 ``` The two loops showcase complementary exit styles: a loop expression with an `else` default, and a labeled loop where `continue` and `break` spell out which iterations contribute to the running total. ## Section: Notes & Caveats [section_id: notes-caveats] [section_url: https://zigbook.net/chapters/02__control-flow-essentials#notes-caveats] - Prefer labeled loops for clarity any time you have nested iteration; it keeps `break`/`continue` explicit and avoids sentinel variables. - `switch` must remain exhaustive—if you rely on `else`, document the invariant with comments or `unreachable` so future cases are not silently ignored. - Loop `else` clauses are evaluated only when the loop exits naturally; make sure your `break` paths return values to avoid falling back to unintended defaults. ## Section: Exercises [section_id: exercises] [section_url: https://zigbook.net/chapters/02__control-flow-essentials#exercises] - Extend `branching.zig` with a third branch that formats values greater than 100 differently, confirming the `if` expression still returns a single string. - Adapt `loop_labels.zig` to return the exact coordinates as a struct via `break :outer`, then print them from `main`. - Modify `script_runner.zig` to parse characters at runtime (for example, from a byte slice) and add a new command that resets the total, ensuring the `switch` stays exhaustive. # Chapter 03 — Data Fundamentals [chapter_id: 03__data-fundamentals] [chapter_slug: data-fundamentals] [chapter_number: 03] [chapter_url: https://zigbook.net/chapters/03__data-fundamentals] ## Section: Overview [section_id: overview] [section_url: https://zigbook.net/chapters/03__data-fundamentals#overview] Control flow is only as useful as the data it pilots, so this chapter grounds Zig’s core collection types—arrays, slices, and sentinel-terminated strings—in practical usage while keeping value semantics explicit. See #Arrays (https://ziglang.org/documentation/master/#Arrays) and #Slices (https://ziglang.org/documentation/master/#Slices) for reference. We also make pointers, optionals, and alignment-friendly casts feel routine, showing how to safely reinterpret memory while retaining bounds checks and clarity about mutability. See #Pointers (https://ziglang.org/documentation/master/#Pointers) and #alignCast (https://ziglang.org/documentation/master/#alignCast) for details. ### Subsection: Zig’s Type System Categories [section_id: type-system-context] [section_url: https://zigbook.net/chapters/03__data-fundamentals#type-system-context] Before diving into specific collection types, it’s helpful to understand where arrays, slices, and pointers fit within Zig’s type system. Every type in Zig belongs to a category, and each category provides specific operations: ```text graph TB subgraph "Type Categories" PRIMITIVE["Primitive Types
bool, u8, i32, f64, void, ..."] POINTER["Pointer Types
*T, [*]T, []T, [:0]T"] AGGREGATE["Aggregate Types
struct, array, tuple"] FUNCTION["Function Types
fn(...) ReturnType"] SPECIAL["Special Types
anytype, type, comptime_int"] end subgraph "Common Type Operations" ABISIZE["abiSize()
Byte size in memory"] ABIALIGN["abiAlignment()
Required alignment"] HASRUNTIME["hasRuntimeBits()
Has runtime storage?"] ELEMTYPE["elemType()
Element type (arrays/slices)"] end PRIMITIVE --> ABISIZE POINTER --> ABISIZE AGGREGATE --> ABISIZE PRIMITIVE --> ABIALIGN POINTER --> ABIALIGN AGGREGATE --> ABIALIGN POINTER --> ELEMTYPE AGGREGATE --> ELEMTYPE ``` Key insights for this chapter: - Arrays are aggregate types with compile-time-known length—their size is `element_size * length` - Slices are pointer types that store both a pointer and runtime length—always 2 × pointer size - Pointers come in multiple shapes (single-item `*T`, many-item `[*]T`, slice `[]T`) with different safety guarantees - All types expose their size and alignment, which affect struct layout and memory allocation This type-aware design lets the compiler enforce bounds checking on slices while allowing pointer arithmetic on many-item pointers when you explicitly opt out of safety. ## Section: Learning Goals [section_id: learning-goals] [section_url: https://zigbook.net/chapters/03__data-fundamentals#learning-goals] - Distinguish array value semantics from slice views, including zero-length idioms for safe fallbacks. - Navigate pointer shapes (`*T`, `[*]T`, `?*T`) and unwrap optionals without sacrificing safety instrumentation (see #Optionals (https://ziglang.org/documentation/master/#Optionals)). - Apply sentinel-terminated strings and alignment-aware casts (`@alignCast`, `@bitCast`, `@intCast`) when interoperating with other APIs (see #Sentinel-Terminated-Pointers (https://ziglang.org/documentation/master/#Sentinel-Terminated-Pointers) and #Explicit-Casts (https://ziglang.org/documentation/master/#Explicit-Casts)). ## Section: Structuring Collections in Memory [section_id: section-1] [section_url: https://zigbook.net/chapters/03__data-fundamentals#section-1] Arrays own storage while slices borrow it, so the compiler enforces different guarantees around length, mutability, and lifetimes; mastering their interplay keeps iteration predictable and moves most bounds checks into debug builds. ### Subsection: Arrays as Owned Storage [section_id: section-1-sub-a] [section_url: https://zigbook.net/chapters/03__data-fundamentals#section-1-sub-a] Arrays carry length in their type, copy by value, and give you a mutable baseline from which to carve read-only and read-write slices. ```zig const std = @import("std"); /// Prints information about a slice including its label, length, and first element. /// If the slice is empty, displays -1 as the head value. fn describe(label: []const u8, data: []const i32) void { // Get first element or -1 if slice is empty const head = if (data.len > 0) data[0] else -1; std.debug.print("{s}: len={} head={d}\n", .{ label, data.len, head }); } /// Demonstrates array and slice fundamentals in Zig, including: /// - Array declaration and initialization /// - Creating slices from arrays with different mutability /// - Modifying arrays through direct indexing and slices /// - Array copying behavior (value semantics) /// - Creating empty and zero-length slices pub fn main() !void { // Declare mutable array with inferred size var values = [_]i32{ 3, 5, 8, 13 }; // Declare const array with explicit size using anonymous struct syntax const owned: [4]i32 = .{ 1, 2, 3, 4 }; // Create a mutable slice covering the entire array var mutable_slice: []i32 = values[0..]; // Create an immutable slice of the first two elements const prefix: []const i32 = values[0..2]; // Create a zero-length slice (empty but valid) const empty = values[0..0]; // Modify array directly by index values[1] = 99; // Modify array through mutable slice mutable_slice[0] = -3; std.debug.print("array len={} allows mutation\n", .{values.len}); describe("mutable_slice", mutable_slice); describe("prefix", prefix); // Demonstrate that slice modification affects the underlying array std.debug.print("values[0] after slice write = {d}\n", .{values[0]}); std.debug.print("empty slice len={} is zero-length\n", .{empty.len}); // Arrays are copied by value in Zig var copy = owned; copy[0] = -1; // Show that modifying the copy doesn't affect the original std.debug.print("copy[0]={d} owned[0]={d}\n", .{ copy[0], owned[0] }); // Create a slice from an empty array literal using address-of operator const zero: []const i32 = &[_]i32{}; std.debug.print("zero slice len={} from literal\n", .{zero.len}); } ``` Run: ```shell $ zig run arrays_and_slices.zig ``` Output: ```shell array len=4 allows mutation mutable_slice: len=4 head=-3 prefix: len=2 head=-3 values[0] after slice write = -3 empty slice len=0 is zero-length copy[0]=-1 owned[0]=1 zero slice len=0 from literal ``` NOTE: The mutable slice and the original array share storage, while the `[]const` prefix resists writes—an intentional boundary that forces read-only consumers to stay honest. ### Subsection: Memory Layout: Arrays vs Slices [section_id: _memory_layout_arrays_vs_slices] [section_url: https://zigbook.net/chapters/03__data-fundamentals#_memory_layout_arrays_vs_slices] Understanding how arrays and slices are laid out in memory clarifies why "arrays own storage while slices borrow it" and why array-to-slice coercion is a cheap operation: ```text graph TB subgraph "Array in Memory" ARRAY_DECL["const values: [4]i32 = .{1, 2, 3, 4}"] ARRAY_MEM["Memory Layout (16 bytes)\n\nstack frame\n| 1 | 2 | 3 | 4 |"] ARRAY_DECL --> ARRAY_MEM end subgraph "Slice in Memory" SLICE_DECL["const slice: []const i32 = &values"] SLICE_MEM["Memory Layout (16 bytes on 64-bit)\n\nstack frame\n| ptr | len=4 |"] POINTS["ptr points to array data"] SLICE_DECL --> SLICE_MEM SLICE_MEM --> POINTS end POINTS -.->|"references"| ARRAY_MEM subgraph "Key Differences" DIFF1["Array: Stores data inline
Size = elem_size × length"] DIFF2["Slice: Stores pointer + length
Size = 2 × pointer_size (16 bytes on 64-bit)"] DIFF3["Coercion: &array → slice
Just creates {ptr, len} pair"] end ``` Why this matters: - Arrays have value semantics: assigning an array copies all elements - Slices have reference semantics: assigning a slice copies just the pointer and length - Array-to-slice coercion (`&array`) is cheap—it doesn’t copy data, just creates a descriptor - Slices are "fat pointers": they carry runtime length information, enabling bounds checking This is why functions typically accept slices as parameters—they can work with arrays, slices, and portions of either without copying the underlying data. ### Subsection: Strings and Sentinels in Practice [section_id: section-1-sub-b] [section_url: https://zigbook.net/chapters/03__data-fundamentals#section-1-sub-b] Sentinel-terminated arrays bridge to C APIs without forfeiting the safety of slices; you can reinterpret the byte stream with `std.mem.span` and still mutate the underlying buffer when the sentinel convention is preserved. ```zig const std = @import("std"); /// Demonstrates sentinel-terminated strings and arrays in Zig, including: /// - Zero-terminated string literals ([:0]const u8) /// - Many-item sentinel pointers ([*:0]const u8) /// - Sentinel-terminated arrays ([N:0]T) /// - Converting between sentinel slices and regular slices /// - Mutation through sentinel pointers pub fn main() !void { // String literals in Zig are sentinel-terminated by default with a zero byte // [:0]const u8 denotes a slice with a sentinel value of 0 at the end const literal: [:0]const u8 = "data fundamentals"; // Convert the sentinel slice to a many-item sentinel pointer // [*:0]const u8 is compatible with C-style null-terminated strings const c_ptr: [*:0]const u8 = literal; // std.mem.span converts a sentinel-terminated pointer back to a slice // It scans until it finds the sentinel value (0) to determine the length const bytes = std.mem.span(c_ptr); std.debug.print("literal len={} contents=\"{s}\"\n", .{ bytes.len, bytes }); // Declare a sentinel-terminated array with explicit size and sentinel value // [6:0]u8 means an array of 6 elements plus a sentinel 0 byte at position 6 var label: [6:0]u8 = .{ 'l', 'a', 'b', 'e', 'l', 0 }; // Create a mutable sentinel slice from the array // The [0.. :0] syntax creates a slice from index 0 to the end, with sentinel 0 var sentinel_view: [:0]u8 = label[0.. :0]; // Modify the first element through the sentinel slice sentinel_view[0] = 'L'; // Create a regular (non-sentinel) slice from the first 4 elements // This drops the sentinel guarantees but provides a bounded slice const trimmed: []const u8 = sentinel_view[0..4]; std.debug.print("trimmed slice len={} -> {s}\n", .{ trimmed.len, trimmed }); // Convert the sentinel slice to a many-item sentinel pointer // This allows unchecked indexing while preserving sentinel information const tail: [*:0]u8 = sentinel_view; // Modify element at index 4 through the many-item sentinel pointer // No bounds checking occurs, but the sentinel guarantees remain valid tail[4] = 'X'; // Demonstrate that mutations through the pointer affected the original array // std.mem.span uses the sentinel to reconstruct the full slice std.debug.print("full label after mutation: {s}\n", .{std.mem.span(tail)}); } ``` Run: ```shell $ zig run sentinel_strings.zig ``` Output: ```shell literal len=17 contents="data fundamentals" trimmed slice len=4 -> Labe full label after mutation: LabeX ``` The sentinel slice keeps the trailing zero intact, so taking a `[*:0]u8` for FFI remains sound even after local mutations, while the plain slice gives ergonomic iteration within Zig (see #Type-Coercion (https://ziglang.org/documentation/master/#Type-Coercion)). TIP: `std.mem.span` converts sentinel pointers into ordinary slices without cloning data, making it ideal when you temporarily need bounds checks or slice helpers before returning to pointer APIs. ### Subsection: Immutable and Mutable Views [section_id: section-1-sub-c] [section_url: https://zigbook.net/chapters/03__data-fundamentals#section-1-sub-c] Prefer `[]const T` when callers only inspect data—Zig will gladly coerce a mutable slice to a const view, giving you API clarity and keeping accidental writes from compiling in the first place. ## Section: Pointer Patterns and Cast Workflows [section_id: section-2] [section_url: https://zigbook.net/chapters/03__data-fundamentals#section-2] Pointers surface when you share storage, interoperate with foreign layouts, or step outside slice bounds; by leaning on optional wrappers and explicit casts, you keep intent clear and allow safety checks to fire whenever assumptions break. ### Subsection: Pointer Shape Reference [section_id: pointer-shapes] [section_url: https://zigbook.net/chapters/03__data-fundamentals#pointer-shapes] Zig offers multiple pointer types, each with different safety guarantees and use cases. Understanding when to use each shape is essential for writing safe, efficient code: ```text graph TB subgraph "Pointer Shapes" SINGLE["*T
Single-Item Pointer"] MANY["[*]T
Many-Item Pointer"] SLICE["[]T
Slice"] OPTIONAL["?*T
Optional Pointer"] SENTINEL_PTR["[*:0]T
Sentinel Many-Item"] SENTINEL_SLICE["[:0]T
Sentinel Slice"] end subgraph "Characteristics" SINGLE --> S_BOUNDS["✓ Bounds: Single element
✓ Safety: Dereference checked
📍 Use: Function parameters, references"] MANY --> M_BOUNDS["⚠ Bounds: Unknown length
✗ Safety: No bounds checking
📍 Use: C interop, tight loops"] SLICE --> SL_BOUNDS["✓ Bounds: Runtime length
✓ Safety: Bounds checked
📍 Use: Most Zig code, iteration"] OPTIONAL --> O_BOUNDS["✓ Bounds: May be null
✓ Safety: Must unwrap first
📍 Use: Optional references"] SENTINEL_PTR --> SP_BOUNDS["✓ Bounds: Until sentinel
~ Safety: Sentinel must exist
📍 Use: C strings, null-terminated"] SENTINEL_SLICE --> SS_BOUNDS["✓ Bounds: Length + sentinel
✓ Safety: Both length and sentinel
📍 Use: Zig ↔ C string bridge"] end ``` Comparison Table: | Shape | Example | Length Known? | Bounds Checked? | Common Use | | --- | --- | --- | --- | --- | | `*T` | `*i32` | Single element | Yes (implicit) | Reference to one item | | `[*]T` | `[*]i32` | Unknown | No | C arrays, pointer arithmetic | | `[]T` | `[]i32` | Runtime (in slice) | Yes | Primary Zig collection type | | `?*T` | `?*i32` | Single (if non-null) | Yes + null check | Optional references | | `[*:0]T` | `[*:0]u8` | Until sentinel | Sentinel must exist | C strings (`char*`) | | `[:0]T` | `[:0]u8` | Runtime + sentinel | Yes + sentinel guarantee | Zig strings for C APIs | Guidelines: - Default to slices (`[]T`) for all Zig code—they provide safety and convenience - Use single-item pointers (`*T`) when you need to mutate a single value or pass by reference - Avoid many-item pointers (`[*]T`) unless interfacing with C or in performance-critical inner loops - Use optional pointers (`?*T`) when null is a meaningful state, not for error handling - Use sentinel types (`[*:0]T`, `[:0]T`) at the C boundary, convert to slices internally ### Subsection: Optional Pointers for Shared Mutability [section_id: workflow-1] [section_url: https://zigbook.net/chapters/03__data-fundamentals#workflow-1] Optional single-item pointers expose mutability without guessing at lifetimes—capture them only when present, mutate through the dereference, and fall back gracefully when the pointer is absent. ```zig const std = @import("std"); /// A simple structure representing a sensor device with a numeric reading. const Sensor = struct { reading: i32, }; /// Prints a sensor's reading value to debug output. /// Takes a single pointer to a Sensor and displays its current reading. fn report(label: []const u8, ptr: *Sensor) void { std.debug.print("{s} -> reading {d}\n", .{ label, ptr.reading }); } /// Demonstrates pointer fundamentals, optional pointers, and many-item pointers in Zig. /// This example covers: /// - Single-item pointers (*T) and pointer dereferencing /// - Pointer aliasing and mutation through aliases /// - Optional pointers (?*T) for representing nullable references /// - Unwrapping optional pointers with if statements /// - Many-item pointers ([*]T) for unchecked multi-element access /// - Converting slices to many-item pointers via .ptr property pub fn main() !void { // Create a sensor instance on the stack var sensor = Sensor{ .reading = 41 }; // Create a single-item pointer alias to the sensor // The & operator takes the address of sensor var alias: *Sensor = &sensor; // Modify the sensor through the pointer alias // Zig automatically dereferences pointer fields alias.reading += 1; report("alias", alias); // Declare an optional pointer initialized to null // ?*T represents a pointer that may or may not hold a valid address var maybe_alias: ?*Sensor = null; // Attempt to unwrap the optional pointer // This branch will not execute because maybe_alias is null if (maybe_alias) |pointer| { std.debug.print("unexpected pointer: {d}\n", .{pointer.reading}); } else { std.debug.print("optional pointer empty\n", .{}); } // Assign a valid address to the optional pointer maybe_alias = &sensor; // Unwrap and use the optional pointer // The |pointer| capture syntax extracts the non-null value if (maybe_alias) |pointer| { pointer.reading += 10; std.debug.print("optional pointer mutated to {d}\n", .{sensor.reading}); } // Create an array and a slice view of it var samples = [_]i32{ 5, 7, 9, 11 }; const view: []i32 = samples[0..]; // Extract a many-item pointer from the slice // Many-item pointers ([*]T) allow unchecked indexing without length tracking const many: [*]i32 = view.ptr; // Modify the underlying array through the many-item pointer // No bounds checking is performed at this point many[2] = 42; std.debug.print("slice view len={}\n", .{view.len}); // Verify that the modification through many-item pointer affected the original array std.debug.print("samples[2] via many pointer = {d}\n", .{samples[2]}); } ``` Run: ```shell $ zig run pointers_and_optionals.zig ``` Output: ```shell alias -> reading 42 optional pointer empty optional pointer mutated to 52 slice view len=4 samples[2] via many pointer = 42 ``` The `?*Sensor` gate keeps mutation behind pattern matching, while the many-item pointer (`[*]i32`) documents aliasing risk by dropping bounds checks—a deliberate trade-off reserved for tight loops and FFI. ### Subsection: Aligning and Reinterpreting Data [section_id: workflow-2] [section_url: https://zigbook.net/chapters/03__data-fundamentals#workflow-2] When you must reinterpret raw bytes, use the casting builtins to promote alignment, change pointer element types, and keep integer/float conversions explicit so debug builds can catch undefined assumptions (see #bitCast (https://ziglang.org/documentation/master/#bitCast)). ```zig const std = @import("std"); /// Demonstrates memory alignment concepts and various type casting operations in Zig. /// This example covers: /// - Memory alignment guarantees with align() attribute /// - Pointer casting with alignment adjustments using @alignCast /// - Type punning with @ptrCast for reinterpreting memory /// - Bitwise reinterpretation with @bitCast /// - Truncating integers with @truncate /// - Widening integers with @intCast /// - Floating-point precision conversion with @floatCast pub fn main() !void { // Create a byte array aligned to u64 boundary, initialized with little-endian bytes // representing 0x11223344 in the first 4 bytes var raw align(@alignOf(u64)) = [_]u8{ 0x44, 0x33, 0x22, 0x11, 0, 0, 0, 0 }; // Get a pointer to the first byte with explicit u64 alignment const base: *align(@alignOf(u64)) u8 = &raw[0]; // Adjust alignment constraint from u64 to u32 using @alignCast // This is safe because u64 alignment (8 bytes) satisfies u32 alignment (4 bytes) const aligned_bytes = @as(*align(@alignOf(u32)) const u8, @alignCast(base)); // Reinterpret the byte pointer as a u32 pointer to read 4 bytes as a single integer const word_ptr = @as(*const u32, @ptrCast(aligned_bytes)); // Dereference to get the 32-bit value (little-endian: 0x11223344) const number = word_ptr.*; std.debug.print("32-bit value = 0x{X:0>8}\n", .{number}); // Alternative approach: directly reinterpret the first 4 bytes using @bitCast // This creates a copy and doesn't require pointer manipulation const from_bytes = @as(u32, @bitCast(raw[0..4].*)); std.debug.print("bitcast copy = 0x{X:0>8}\n", .{from_bytes}); // Demonstrate @truncate: extract the least significant 8 bits (0x44) const small: u8 = @as(u8, @truncate(number)); // Demonstrate @intCast: widen unsigned u32 to signed i64 without data loss const widened: i64 = @as(i64, @intCast(number)); std.debug.print("truncate -> 0x{X:0>2}, widen -> {d}\n", .{ small, widened }); // Demonstrate @floatCast: reduce f64 precision to f32 // May result in precision loss for values that cannot be exactly represented in f32 const ratio64: f64 = 1.875; const ratio32: f32 = @as(f32, @floatCast(ratio64)); std.debug.print("floatCast ratio -> {}\n", .{ratio32}); } ``` Run: ```shell $ zig run alignment_and_casts.zig ``` Output: ```shell 32-bit value = 0x11223344 bitcast copy = 0x11223344 truncate -> 0x44, widen -> 287454020 floatCast ratio -> 1.875 ``` By chaining `@alignCast`, `@ptrCast`, and `@bitCast` you assert layout relationships explicitly, and the subsequent `@truncate`/`@intCast` conversions keep integer widths honest when narrowing or widening across APIs. ## Section: Notes & Caveats [section_id: notes-caveats] [section_url: https://zigbook.net/chapters/03__data-fundamentals#notes-caveats] - Sentinel-terminated pointers are great for C bridges, but within Zig prefer slices so bounds checks stay available and APIs expose lengths. - Upgrading pointer alignment with `@alignCast` still traps in Debug mode if the address is misaligned—prove the precondition before promoting. - Many-item pointers (`[*]T`) drop bounds checks; reach for them sparingly and document invariants that a safe slice would have enforced. ## Section: Exercises [section_id: exercises] [section_url: https://zigbook.net/chapters/03__data-fundamentals#exercises] - Extend `arrays_and_slices.zig` to create a zero-length mutable slice from a runtime array, then append via `std.ArrayList` to observe how slice views remain valid. - Modify `sentinel_strings.zig` to accept a user-supplied `[:0]u8` and guard against inputs missing the sentinel by returning an error union. - Enhance `alignment_and_casts.zig` by adding a branch that rejects values whose low byte is zero before truncation, surfacing how `@intCast` depends on caller-supplied range guarantees. # Chapter 04 — Errors & Resource Cleanup [chapter_id: 04__errors-resource-cleanup] [chapter_slug: errors-resource-cleanup] [chapter_number: 04] [chapter_url: https://zigbook.net/chapters/04__errors-resource-cleanup] ## Section: Overview [section_id: overview] [section_url: https://zigbook.net/chapters/04__errors-resource-cleanup#overview] Chapter 3 gave us the tools to shape data; now we need rigorous ways to report when operations fail and to unwind resources predictably. Zig’s error unions let you define precise failure vocabularies, propagate them with `try`, and surface informative names without reaching for exceptions, as described in #Error-Set-Type (https://ziglang.org/documentation/master/#Error-Set-Type) and #try (https://ziglang.org/documentation/master/#try). We also explore `defer` and `errdefer`, the paired statements that keep cleanup adjacent to acquisition so you never lose track of file handles, buffers, or other scarce resources when an error forces an early return; see #defer (https://ziglang.org/documentation/master/#defer) and #errdefer (https://ziglang.org/documentation/master/#errdefer). ## Section: Learning Goals [section_id: learning-goals] [section_url: https://zigbook.net/chapters/04__errors-resource-cleanup#learning-goals] - Declare dedicated error sets, merge them as needed, and propagate failures with `try` so callers explicitly acknowledge what might go wrong. - Translate errors into recoverable states using `catch`, including logging, fallback values, and structured control-flow exits, as described in #catch (https://ziglang.org/documentation/master/#catch). - Pair `defer` and `errdefer` to guarantee deterministic cleanup, even when you intentionally silence an error with constructs like `catch unreachable`; see #unreachable (https://ziglang.org/documentation/master/#unreachable). ## Section: Error Sets and Propagation [section_id: section-1] [section_url: https://zigbook.net/chapters/04__errors-resource-cleanup#section-1] Error-aware APIs in Zig embrace explicit unions: a function that might fail returns `E!T`, and each helper it calls uses `try` to bubble errors upward until a site decides how to recover. This keeps control flow observable while still letting successful paths look straightforward, as described in #Error-Handling (https://ziglang.org/documentation/master/#Error-Handling). ### Subsection: Declaring Error Sets and Propagating with try [section_id: section-1-sub-a] [section_url: https://zigbook.net/chapters/04__errors-resource-cleanup#section-1-sub-a] By naming the exact errors a function can return, callers get compile-time exhaustiveness and readable diagnostics when values go sideways. `try` forwards those errors automatically, avoiding boilerplate while remaining honest about failure modes. ```zig const std = @import("std"); // Chapter 4 §1.1 – this sample names an error set and shows how `try` forwards // failures up to the caller without hiding them along the way. const ParseError = error{ InvalidDigit, Overflow }; fn decodeDigit(ch: u8) ParseError!u8 { return switch (ch) { '0'...'9' => @as(u8, ch - '0'), else => error.InvalidDigit, }; } fn accumulate(input: []const u8) ParseError!u8 { var total: u8 = 0; for (input) |ch| { // Each digit must parse successfully; `try` re-raises any // `ParseError` so the outer function's contract stays accurate. const digit = try decodeDigit(ch); total = total * 10 + digit; if (total > 99) { // Propagate a second error variant to demonstrate that callers see // a complete vocabulary of what can go wrong. return error.Overflow; } } return total; } pub fn main() !void { const samples = [_][]const u8{ "27", "9x", "120" }; for (samples) |sample| { const value = accumulate(sample) catch |err| { // Chapter 4 §1.2 will build on this pattern, but even here we log // the error name so failed inputs remain observable. std.debug.print("input \"{s}\" failed with {s}\n", .{ sample, @errorName(err) }); continue; }; std.debug.print("input \"{s}\" -> {}\n", .{ sample, value }); } } ``` Run: ```shell $ zig run propagation_basics.zig ``` Output: ```shell input "27" -> 27 input "9x" failed with InvalidDigit input "120" failed with Overflow ``` TIP: The loop keeps moving because each `catch` branch documents its intent—report and continue—which mirrors how production code would skip a malformed record while still surfacing its name. ### Subsection: How Error Sets Work Internally [section_id: _how_error_sets_work_internally] [section_url: https://zigbook.net/chapters/04__errors-resource-cleanup#_how_error_sets_work_internally] When you declare an error set in Zig, you are creating a subset of a global error registry maintained by the compiler. Understanding this architecture clarifies why error operations are fast and how error set merging works: ```text graph LR subgraph "Global Error Set" GES["global_error_set"] NAMES["Error name strings
Index 0 = empty"] GES --> NAMES NAMES --> ERR1["Index 1: 'OutOfMemory'"] NAMES --> ERR2["Index 2: 'FileNotFound'"] NAMES --> ERR3["Index 3: 'AccessDenied'"] NAMES --> ERRN["Index N: 'CustomError'"] end subgraph "Error Value" ERRVAL["Value{
err: {name: Index}
}"] ERRVAL -->|"name = 1"| ERR1 end subgraph "Error Set Type" ERRSET["Type{
error_set_type: {
names: [1,2,3]
}
}"] ERRSET --> ERR1 ERRSET --> ERR2 ERRSET --> ERR3 end ``` Key insights: - Global Registry: All error names across your entire program are stored in a single global registry with unique indices. - Lightweight Values: Error values are just `u16` tags pointing into this registry—comparing errors is as fast as comparing integers. - Error Set Types: When you write `error{InvalidDigit, Overflow}`, you are creating a type that references a subset of the global registry. - Merging is Simple: The `||` operator combines error sets by creating a new type with the union of indices—no string manipulation needed. - Uniqueness Guarantee: Error names are globally unique, so `error.InvalidDigit` always refers to the same registry entry. This design makes error handling in Zig extremely efficient while preserving informative error names for debugging. The tag-based representation means error unions add minimal overhead compared to plain values. ### Subsection: Shaping Recovery with catch [section_id: section-1-sub-b] [section_url: https://zigbook.net/chapters/04__errors-resource-cleanup#section-1-sub-b] `catch` blocks can branch on specific errors, choose fallback values, or decide that a failure ends the current iteration. Labeling the loop clarifies which control path we resume after handling a timeout versus a disconnect. ```zig const std = @import("std"); // Chapter 4 §1.2 – demonstrate how `catch` branches per error to shape // recovery strategies without losing control-flow clarity. const ProbeError = error{ Disconnected, Timeout }; fn readProbe(id: usize) ProbeError!u8 { return switch (id) { 0 => 42, 1 => error.Timeout, 2 => error.Disconnected, else => 88, }; } pub fn main() !void { const ids = [_]usize{ 0, 1, 2, 3 }; var total: u32 = 0; probe_loop: for (ids) |id| { const raw = readProbe(id) catch |err| handler: { switch (err) { error.Timeout => { // Timeouts can be softened with a fallback value, allowing // the loop to continue exercising the “recover and proceed” path. std.debug.print("probe {} timed out; using fallback 200\n", .{id}); break :handler 200; }, error.Disconnected => { // A disconnected sensor demonstrates the “skip entirely” // recovery branch discussed in the chapter. std.debug.print("probe {} disconnected; skipping sample\n", .{id}); continue :probe_loop; }, } }; total += raw; std.debug.print("probe {} -> {}\n", .{ id, raw }); } std.debug.print("aggregate total = {}\n", .{total}); } ``` Run: ```shell $ zig run catch_and_recover.zig ``` Output: ```shell probe 0 -> 42 probe 1 timed out; using fallback 200 probe 1 -> 200 probe 2 disconnected; skipping sample probe 3 -> 88 aggregate total = 330 ``` TIP: Timeouts degrade to a cached number, whereas disconnects abandon the sample entirely—two distinct recovery strategies made explicit in code. ### Subsection: Merging Error Sets into Stable APIs [section_id: section-1-sub-c] [section_url: https://zigbook.net/chapters/04__errors-resource-cleanup#section-1-sub-c] When reusable helpers stem from different domains—parsing, networking, storage—you can union their error sets with `||` to publish a single contract while still letting internal code `try` each step. Keeping the merged set narrow means downstream callers only reckon with the failures you actually intend to expose. ### Subsection: Inferred Error Sets [section_id: _inferred_error_sets] [section_url: https://zigbook.net/chapters/04__errors-resource-cleanup#_inferred_error_sets] Often you do not need to explicitly list every error a function might return. Zig supports inferred error sets using the `!T` syntax, where the compiler automatically determines which errors can be returned by analyzing your function body: ```text graph TB subgraph "Inferred Error Set Structure" IES["InferredErrorSet"] FUNC["func: Index
Owning function"] ERRORS["errors: NameMap
Direct errors"] INFERREDSETS["inferred_error_sets
Dependent IES"] RESOLVED["resolved: Index
Final error set"] end subgraph "Error Sources" DIRECTRET["return error.Foo
Direct error returns"] FUNCALL["foo() catch
Called function errors"] IESCALL["bar() catch
IES function call"] end subgraph "Resolution Process" BODYANAL["Analyze function body"] COLLECTERRS["Collect all errors"] RESOLVEDEPS["Resolve dependent IES"] CREATESET["Create error set type"] end DIRECTRET --> ERRORS FUNCALL --> ERRORS IESCALL --> INFERREDSETS BODYANAL --> COLLECTERRS COLLECTERRS --> ERRORS COLLECTERRS --> INFERREDSETS RESOLVEDEPS --> CREATESET CREATESET --> RESOLVED FUNC --> BODYANAL ERRORS --> COLLECTERRS INFERREDSETS --> RESOLVEDEPS ``` How it works: 1. During Analysis: As the compiler analyzes your function body: 2. After Body Analysis: Once the function body is fully analyzed: 3. Special Cases: Why use inferred error sets? - Less maintenance: Errors propagate automatically when you add `try` calls - Refactoring friendly: Adding error-returning calls doesn’t require updating signatures - Still type-safe: Callers see the complete error set through type inference When you want explicit control over your API contract, declare the error set. When internal implementation details should determine errors, use `!T` and let the compiler infer them. ## Section: Deterministic Cleanup with defer [section_id: section-2] [section_url: https://zigbook.net/chapters/04__errors-resource-cleanup#section-2] Resource lifetime clarity comes from placing acquisition, use, and release in one lexical block. `defer` ensures releases happen in reverse order of registration, and `errdefer` supplements it for partial setup sequences that must roll back when an error interrupts progress. ### Subsection: defer Keeps Releases Next to Acquisition [section_id: workflow-1] [section_url: https://zigbook.net/chapters/04__errors-resource-cleanup#workflow-1] Using `defer` right after acquiring a resource documents ownership and guarantees cleanup on both success and failure, which is especially valuable for fallible jobs that may bail early. ```zig const std = @import("std"); // Chapter 4 §2.1 – `defer` binds cleanup to acquisition so readers see the // full lifetime of a resource inside one lexical scope. const JobError = error{CalibrateFailed}; const Resource = struct { name: []const u8, cleaned: bool = false, fn release(self: *Resource) void { if (!self.cleaned) { self.cleaned = true; std.debug.print("release {s}\n", .{self.name}); } } }; fn runJob(name: []const u8, should_fail: bool) JobError!void { std.debug.print("acquiring {s}\n", .{name}); var res = Resource{ .name = name }; // Place `defer` right after acquiring the resource so its release triggers // on every exit path, successful or otherwise. defer res.release(); std.debug.print("working with {s}\n", .{name}); if (should_fail) { std.debug.print("job {s} failed\n", .{name}); return error.CalibrateFailed; } std.debug.print("job {s} succeeded\n", .{name}); } pub fn main() !void { const jobs = [_]struct { name: []const u8, fail: bool }{ .{ .name = "alpha", .fail = false }, .{ .name = "beta", .fail = true }, }; for (jobs) |job| { std.debug.print("-- cycle {s} --\n", .{job.name}); runJob(job.name, job.fail) catch |err| { // Even when a job fails, the earlier `defer` has already scheduled // the cleanup that keeps our resource balanced. std.debug.print("{s} bubbled up {s}\n", .{ job.name, @errorName(err) }); }; } } ``` Run: ```shell $ zig run defer_cleanup.zig ``` Output: ```shell -- cycle alpha -- acquiring alpha working with alpha job alpha succeeded release alpha -- cycle beta -- acquiring beta working with beta job beta failed release beta beta bubbled up CalibrateFailed ``` NOTE: The release call fires even on the failing job, proving that defers execute before the error reaches the caller. ### Subsection: How Defer Execution Order Works [section_id: _how_defer_execution_order_works] [section_url: https://zigbook.net/chapters/04__errors-resource-cleanup#_how_defer_execution_order_works] Understanding the execution order of `defer` and `errdefer` statements is crucial for writing correct cleanup code. Zig executes these statements in LIFO (Last In, First Out) order—the reverse of their registration: ```text graph TB subgraph "Function Execution" ENTER["Function Entry"] ACQUIRE1["Step 1: Acquire Resource A
defer cleanup_A()"] ACQUIRE2["Step 2: Acquire Resource B
defer cleanup_B()"] ACQUIRE3["Step 3: Acquire Resource C
errdefer cleanup_C()"] WORK["Step 4: Do work (may error)"] EXIT["Function Exit"] end subgraph "Success Path" SUCCESS["Work succeeds"] DEFER_C["Step 3: Run cleanup_C()"] DEFER_B["Step 2: Run cleanup_B()"] DEFER_A["Step 1: Run cleanup_A()"] RETURN_OK["Return success"] end subgraph "Error Path" ERROR["Work errors"] ERRDEFER_C["Step 3: Run cleanup_C() via errdefer"] ERRDEFER_B["Step 2: Run cleanup_B() via defer"] ERRDEFER_A["Step 1: Run cleanup_A() via defer"] RETURN_ERR["Return error"] end ENTER --> ACQUIRE1 ACQUIRE1 --> ACQUIRE2 ACQUIRE2 --> ACQUIRE3 ACQUIRE3 --> WORK WORK -->|"success"| SUCCESS WORK -->|"error"| ERROR SUCCESS --> DEFER_C DEFER_C --> DEFER_B DEFER_B --> DEFER_A DEFER_A --> RETURN_OK ERROR --> ERRDEFER_C ERRDEFER_C --> ERRDEFER_B ERRDEFER_B --> ERRDEFER_A ERRDEFER_A --> RETURN_ERR RETURN_OK --> EXIT RETURN_ERR --> EXIT ``` Key execution rules: - LIFO Order: Defers execute in reverse registration order—last registered runs first. - Mirror Setup: This naturally mirrors initialization order, so cleanup happens in reverse of acquisition. - Always Runs: Regular `defer` statements execute on both success and error paths. - Conditional: `errdefer` statements only execute when the scope exits via error. - Scope-Based: Defers are tied to their enclosing scope (function, block, etc.). This LIFO guarantee ensures that resources are cleaned up in the opposite order of acquisition. This is especially important when resources depend on each other, as it prevents use-after-free scenarios during cleanup. ### Subsection: errdefer Rolls Back Partial Initialization [section_id: workflow-2] [section_url: https://zigbook.net/chapters/04__errors-resource-cleanup#workflow-2] `errdefer` is ideal for staged setups: it runs only when the surrounding scope exits with an error, giving you a single place to undo whatever succeeded before the failure. ```zig const std = @import("std"); // Chapter 4 §2.2 – staged setup guarded with `errdefer` so partially // initialized channels roll back automatically on failure. const SetupError = error{ OpenFailed, RegisterFailed }; const Channel = struct { name: []const u8, opened: bool = false, registered: bool = false, fn teardown(self: *Channel) void { if (self.registered) { std.debug.print("deregister \"{s}\"\n", .{self.name}); self.registered = false; } if (self.opened) { std.debug.print("closing \"{s}\"\n", .{self.name}); self.opened = false; } } }; fn setupChannel(name: []const u8, fail_on_register: bool) SetupError!Channel { std.debug.print("opening \"{s}\"\n", .{name}); if (name.len == 0) { return error.OpenFailed; } var channel = Channel{ .name = name, .opened = true }; errdefer { // If any later step fails we run the rollback block, mirroring the // “errdefer Rolls Back Partial Initialization” section. std.debug.print("rollback \"{s}\"\n", .{name}); channel.teardown(); } std.debug.print("registering \"{s}\"\n", .{name}); if (fail_on_register) { return error.RegisterFailed; } channel.registered = true; return channel; } pub fn main() !void { std.debug.print("-- success path --\n", .{}); var primary = try setupChannel("primary", false); defer primary.teardown(); std.debug.print("-- register failure --\n", .{}); _ = setupChannel("backup", true) catch |err| { std.debug.print("setup failed with {s}\n", .{@errorName(err)}); }; std.debug.print("-- open failure --\n", .{}); _ = setupChannel("", false) catch |err| { std.debug.print("setup failed with {s}\n", .{@errorName(err)}); }; } ``` Run: ```shell $ zig run errdefer_recovery.zig ``` Output: ```shell -- success path -- opening "primary" registering "primary" -- register failure -- opening "backup" registering "backup" rollback "backup" closing "backup" setup failed with RegisterFailed -- open failure -- opening "" setup failed with OpenFailed deregister "primary" closing "primary" ``` NOTE: The staging function cleans up only the partially initialized `backup` channel, while leaving the untouched empty name alone and deferring the real teardown of the successful `primary` until the caller exits. ### Subsection: Ignoring Errors with Intent [section_id: section-2-sub-c] [section_url: https://zigbook.net/chapters/04__errors-resource-cleanup#section-2-sub-c] Sometimes you decide an error is impossible—perhaps you validated input earlier—so you write `try foo() catch unreachable;` to crash immediately if the invariant is broken. Do this sparingly: in Debug and ReleaseSafe builds, `unreachable` traps so such assumptions are loudly revalidated at runtime. ## Section: Notes & Caveats [section_id: notes-caveats] [section_url: https://zigbook.net/chapters/04__errors-resource-cleanup#notes-caveats] - Favor small, descriptive error sets so API consumers read the type and instantly grasp all the failure branches they must handle. - Remember that defers execute in reverse order; put the most fundamental cleanup last so shutdown mirrors setup. - Treat `catch unreachable` as a debugging assertion—not as a way to silence legitimate failures—because safety modes turn it into a runtime trap. ## Section: Exercises [section_id: exercises] [section_url: https://zigbook.net/chapters/04__errors-resource-cleanup#exercises] - Extend `propagation_basics.zig` so `accumulate` accepts arbitrarily long inputs by checking for overflow before multiplying, and surface a new error variant for "too many digits." - Augment `catch_and_recover.zig` with a struct that records how many timeouts occurred, returning it from `main` so tests can assert the recovery policy. - Modify `errdefer_recovery.zig` to inject an additional configuration step guarded by its own `defer`, then observe how both `defer` and `errdefer` cooperate when initialization stops midway. ## Section: Alternatives & Edge Cases: [section_id: caveats-alternatives-edge-cases] [section_url: https://zigbook.net/chapters/04__errors-resource-cleanup#caveats-alternatives-edge-cases] - When interoperating with C, translate foreign error codes into Zig error sets once at the boundary so the rest of your code keeps the richer typing. - If a cleanup routine itself can fail, prefer logging within the `defer` and keep the original error primary; otherwise callers may misinterpret the cleanup failure as the root cause. - For deferred allocations, consider arenas or owned buffers: they integrate with `defer` by freeing everything at once, reducing the number of individual cleanup statements you need. # Chapter 05 — Project [chapter_id: 05__project-tempconv-cli] [chapter_slug: project-tempconv-cli] [chapter_number: 05] [chapter_url: https://zigbook.net/chapters/05__project-tempconv-cli] ## Section: Overview [section_id: overview] [section_url: https://zigbook.net/chapters/05__project-tempconv-cli#overview] Our first project turns the language fundamentals from Chapters 1–4 into a handheld command-line utility that converts temperatures between Celsius, Fahrenheit, and Kelvin. We compose argument parsing, enums, and floating-point math into a single program while keeping diagnostics friendly for end users, as described in #Command-line-flags (https://ziglang.org/documentation/master/#Command-line-flags) and #Floats (https://ziglang.org/documentation/master/#Floats). Along the way, we reinforce the error-handling philosophy from the previous chapter: validation produces human-readable hints, and the process exits with intent instead of a stack trace; see #Error-Handling (https://ziglang.org/documentation/master/#Error-Handling). ## Section: Learning Goals [section_id: learning-goals] [section_url: https://zigbook.net/chapters/05__project-tempconv-cli#learning-goals] - Build a minimal CLI harness that reads arguments, handles `--help`, and emits usage guidance. - Represent temperature units with enums and use `switch` to normalise conversions, as described in #switch (https://ziglang.org/documentation/master/#switch). - Present conversion results while surfacing validation failures through concise diagnostics instead of unwinding traces. ## Section: Shaping the Command Interface [section_id: section-1] [section_url: https://zigbook.net/chapters/05__project-tempconv-cli#section-1] Before touching any math, we need a predictable contract: three arguments (`value`, `from-unit`, `to-unit`) plus `--help` for documentation. The program should explain mistakes up front so callers never see a panic. ### Subsection: How CLI Arguments Reach Your Program [section_id: section-1-sub-a-pre] [section_url: https://zigbook.net/chapters/05__project-tempconv-cli#section-1-sub-a-pre] When you run your program from the command line, the operating system passes arguments through a well-defined startup sequence before your `main()` function ever runs. Understanding this flow clarifies where `std.process.args()` gets its data: ```text graph TB OS["Operating System"] EXEC["execve() system call"] KERNEL["Kernel loads ELF"] STACK["Stack setup:
argc, argv[], envp[]"] START["_start entry point
(naked assembly)"] POSIX["posixCallMainAndExit
(argc_argv_ptr)"] PARSE["Parse stack layout:
argc at [0]
argv at [1..argc+1]
envp after NULL"] GLOBALS["Set global state:
std.os.argv = argv[0..argc]
std.os.environ = envp"] CALLMAIN["callMainWithArgs
(argc, argv, envp)"] USERMAIN["Your main() function"] ARGS["std.process.args()
reads std.os.argv"] OS --> EXEC EXEC --> KERNEL KERNEL --> STACK STACK --> START START --> POSIX POSIX --> PARSE PARSE --> GLOBALS GLOBALS --> CALLMAIN CALLMAIN --> USERMAIN USERMAIN --> ARGS ``` Key points: - OS Preparation: The operating system places `argc` (argument count) and `argv` (argument array) on the stack before transferring control to your program. - Assembly Entry: The `_start` symbol (written in inline assembly) is the true entry point, not `main()`. - Stack Parsing: `posixCallMainAndExit` reads the stack layout to extract `argc`, `argv`, and environment variables. - Global State: Before calling your `main()`, the runtime populates `std.os.argv` and `std.os.environ` with the parsed data. - User Access: When you call `std.process.args()`, it simply returns an iterator over the already-populated `std.os.argv` slice. Why this matters for CLI programs: - Arguments are available from the moment `main()` runs—no separate initialization needed. - The first argument (`argv[0]`) is always the program name. - Argument parsing happens once during startup, not per access. - This sequence is the same whether you use `zig run` or a compiled binary. This infrastructure means your TempConv CLI can immediately start parsing arguments without worrying about the low-level details of how they arrived. ### Subsection: Parsing Arguments with Guard Rails [section_id: section-1-sub-a] [section_url: https://zigbook.net/chapters/05__project-tempconv-cli#section-1-sub-a] The entry point allocates the full argument vector, checks for `--help`, and verifies the arity. When a rule is violated we print the usage banner and exit with a failure code, relying on `std.process.exit` to avoid noisy stack traces. ### Subsection: Units and Validation Helpers [section_id: section-1-sub-b] [section_url: https://zigbook.net/chapters/05__project-tempconv-cli#section-1-sub-b] We describe the supported units with an enum and a `parseUnit` helper that accepts either uppercase or lowercase tokens. Invalid tokens trigger a friendly diagnostic and immediate exit, keeping the CLI resilient when embedded in scripts, as described in #enum (https://ziglang.org/documentation/master/#enum). ## Section: Converting and Reporting Results [section_id: section-2] [section_url: https://zigbook.net/chapters/05__project-tempconv-cli#section-2] With the interface in place, the rest of the program leans on deterministic conversions: every value is normalised to Kelvin and then projected into the requested unit, guaranteeing consistent results regardless of the input combination. ### Subsection: Complete TempConv Listing [section_id: workflow-1] [section_url: https://zigbook.net/chapters/05__project-tempconv-cli#workflow-1] The listing below includes argument parsing, unit helpers, and the conversion logic. Focus on how the CLI structure keeps every failure path obvious while keeping the happy path concise. ```zig const std = @import("std"); // Chapter 5 – TempConv CLI: walk from parsing arguments through producing a // formatted result, exercising everything we have learned about errors and // deterministic cleanup along the way. const CliError = error{ MissingArgs, BadNumber, BadUnit }; const Unit = enum { c, f, k }; fn printUsage() void { std.debug.print("usage: tempconv \n", .{}); std.debug.print("units: C (celsius), F (fahrenheit), K (kelvin)\n", .{}); } fn parseUnit(token: []const u8) CliError!Unit { // Section 1: we accept a single-letter token and normalise it so the CLI // remains forgiving about casing. if (token.len != 1) return CliError.BadUnit; const ascii = std.ascii; const lower = ascii.toLower(token[0]); return switch (lower) { 'c' => .c, 'f' => .f, 'k' => .k, else => CliError.BadUnit, }; } fn toKelvin(value: f64, unit: Unit) f64 { return switch (unit) { .c => value + 273.15, .f => (value + 459.67) * 5.0 / 9.0, .k => value, }; } fn fromKelvin(value: f64, unit: Unit) f64 { return switch (unit) { .c => value - 273.15, .f => (value * 9.0 / 5.0) - 459.67, .k => value, }; } fn convert(value: f64, from: Unit, to: Unit) f64 { // Section 2: normalise through Kelvin so every pair of units reuses the // same formulas, keeping the CLI easy to extend. if (from == to) return value; const kelvin = toKelvin(value, from); return fromKelvin(kelvin, to); } pub fn main() !void { const allocator = std.heap.page_allocator; const args = try std.process.argsAlloc(allocator); defer std.process.argsFree(allocator, args); if (args.len == 1 or (args.len == 2 and std.mem.eql(u8, args[1], "--help"))) { printUsage(); return; } if (args.len != 4) { std.debug.print("error: expected three arguments\n", .{}); printUsage(); std.process.exit(1); } const raw_value = args[1]; const value = std.fmt.parseFloat(f64, raw_value) catch { // Section 1 also highlights how parsing failures become user-facing // diagnostics rather than backtraces. std.debug.print("error: '{s}' is not a floating-point value\n", .{raw_value}); std.process.exit(1); }; const from = parseUnit(args[2]) catch { std.debug.print("error: unknown unit '{s}'\n", .{args[2]}); std.process.exit(1); }; const to = parseUnit(args[3]) catch { std.debug.print("error: unknown unit '{s}'\n", .{args[3]}); std.process.exit(1); }; const result = convert(value, from, to); std.debug.print( "{d:.2} {s} -> {d:.2} {s}\n", .{ value, @tagName(from), result, @tagName(to) }, ); } ``` Run: ```shell $ zig run tempconv_cli.zig -- 32 F C ``` Output: ```shell 32.00 f -> 0.00 c ``` TIP: The program prints diagnostics before exiting whenever it spots an invalid value or unit, so scripts can rely on a non-zero exit status without parsing stack traces. ### Subsection: Exercising Additional Conversions [section_id: workflow-2] [section_url: https://zigbook.net/chapters/05__project-tempconv-cli#workflow-2] You can run the same binary for Kelvin or Celsius inputs—the shared conversion helpers guarantee symmetry because everything flows through Kelvin. ```shell $ zig run tempconv_cli.zig -- 273.15 K C ``` Output: ```shell 273.15 k -> 0.00 c ``` ## Section: Notes & Caveats [section_id: notes-caveats] [section_url: https://zigbook.net/chapters/05__project-tempconv-cli#notes-caveats] - Argument parsing remains minimal by design; production tools might add long-form flags or richer help text using the same guard patterns. - Temperature conversions are linear, so double-precision floats suffice; adapt the formulas carefully if you add niche scales such as Rankine. - `std.debug.print` writes to stderr, which keeps scripted pipelines safe—swap to buffered stdout writers if you need clean stdout output; see #Debug (https://ziglang.org/documentation/master/#Debug). ## Section: Exercises [section_id: exercises] [section_url: https://zigbook.net/chapters/05__project-tempconv-cli#exercises] - Expand `parseUnit` to recognise the full words `celsius`, `fahrenheit`, and `kelvin` alongside their single-letter abbreviations. - Add a flag that toggles between rounded output (`{d:.2}`) and full precision using Zig’s formatting verbs; see fmt.zig (https://github.com/ziglang/zig/tree/master/lib/std/fmt.zig). - Introduce a `--table` mode that prints conversions for a range of values, reinforcing slice iteration with `for`, as described in #for (https://ziglang.org/documentation/master/#for). ## Section: Alternatives & Edge Cases: [section_id: caveats-alternatives-edge-cases] [section_url: https://zigbook.net/chapters/05__project-tempconv-cli#caveats-alternatives-edge-cases] - Kelvin never drops below zero; attach a guard if your CLI should reject negative Kelvin inputs instead of accepting the mathematical value. - International audiences sometimes expect comma decimals; connect `std.fmt.formatFloat` with locale-aware post-processing if you need that behaviour. - To support scripted usage without invoking `zig run`, package the program with `zig build-exe` and place the binary on your `PATH`. # Chapter 06 — Project [chapter_id: 06__project-grep-lite] [chapter_slug: project-grep-lite] [chapter_number: 06] [chapter_url: https://zigbook.net/chapters/06__project-grep-lite] ## Section: Overview [section_id: overview] [section_url: https://zigbook.net/chapters/06__project-grep-lite#overview] Our second project graduates from arithmetic to text processing: a tiny `grep` clone that accepts a search pattern and a file path, then prints only the matching lines. The exercise reinforces argument handling from the previous chapter while introducing file I/O and slice utilities from the standard library. #Command-line-flags (https://ziglang.org/documentation/master/#Command-line-flags), File.zig (https://github.com/ziglang/zig/tree/master/lib/std/fs/File.zig) Instead of streaming byte-by-byte, we lean on Zig’s memory-safe helpers to load the file, split it into lines, and surface hits with straightforward substring checks. Every failure path produces a user-friendly message before exiting, so the tool behaves predictably inside shell scripts—a theme we will carry into the next project. See #Command-line-flags (https://ziglang.org/documentation/master/#Command-line-flags) and File.zig (https://github.com/ziglang/zig/tree/master/lib/std/fs/File.zig) for related APIs, and #Error-Handling (https://ziglang.org/documentation/master/#Error-Handling) for error-handling patterns. ## Section: Learning Goals [section_id: learning-goals] [section_url: https://zigbook.net/chapters/06__project-grep-lite#learning-goals] - Implement a command-line parsing routine that supports `--help`, enforces arity, and terminates gracefully on misuse. - Use `std.fs.File.readToEndAlloc` and `std.mem.splitScalar` to load and iterate over file contents (see mem.zig (https://github.com/ziglang/zig/tree/master/lib/std/mem.zig)). - Filter lines with `std.mem.indexOf` and report results via stdout while directing diagnostics to stderr (see debug.zig (https://github.com/ziglang/zig/tree/master/lib/std/debug.zig)). ## Section: Building the Search Harness [section_id: section-1] [section_url: https://zigbook.net/chapters/06__project-grep-lite#section-1] We start by wiring the CLI front end: allocate arguments, honor `--help`, and confirm that exactly two positional parameters — pattern and path — are present. Any deviation prints a usage banner and exits with code 1, avoiding stack traces while still signaling failure to the caller. ### Subsection: Validating Arguments and Usage Paths [section_id: section-1-sub-a] [section_url: https://zigbook.net/chapters/06__project-grep-lite#section-1-sub-a] The skeleton mirrors Chapter 5’s TempConv CLI, but now we emit diagnostics to stderr and exit explicitly whenever input is wrong or a file cannot be opened. `printUsage` keeps the banner in one place, and `std.process.exit` guarantees we stop immediately after the message is written. ### Subsection: Loading and Splitting the File [section_id: section-1-sub-b] [section_url: https://zigbook.net/chapters/06__project-grep-lite#section-1-sub-b] Rather than juggling partial reads, we load the file into memory with `File.readToEndAlloc`, capping the size to eight megabytes to guard against unexpected giants. A single call to `std.mem.splitScalar` then produces an iterator over newline-delimited segments, which we trim for Windows-style carriage returns. ### Subsection: Understanding std.fs Structure [section_id: _understanding_std_fs_structure] [section_url: https://zigbook.net/chapters/06__project-grep-lite#_understanding_std_fs_structure] Before diving into file operations, it’s helpful to understand how Zig’s filesystem API is organized. The `std.fs` module provides a layered hierarchy that makes file access portable and composable: ```text graph TB subgraph "File System API Hierarchy" CWD["std.fs.cwd()
Returns: Dir"] DIR["Dir type
(fs/Dir.zig)"] FILE["File type
(fs/File.zig)"] end subgraph "Dir Operations" OPENFILE["openFile(path, flags)
Returns: File"] MAKEDIR["makeDir(path)"] OPENDIR["openDir(path)
Returns: Dir"] ITERATE["iterate()
Returns: Iterator"] end subgraph "File Operations" READ["read(buffer)
Returns: bytes read"] READTOEND["readToEndAlloc(allocator, max_size)
Returns: []u8"] WRITE["write(bytes)
Returns: bytes written"] SEEK["seekTo(pos)"] CLOSE["close()"] end CWD --> DIR DIR --> OPENFILE DIR --> MAKEDIR DIR --> OPENDIR DIR --> ITERATE OPENFILE --> FILE OPENDIR --> DIR FILE --> READ FILE --> READTOEND FILE --> WRITE FILE --> SEEK FILE --> CLOSE ``` Key concepts: - Entry Point: `std.fs.cwd()` returns a `Dir` handle representing the current working directory - Dir Type: Provides directory-level operations like opening files, creating subdirectories, and iterating contents - File Type: Represents an open file with read/write operations - Chained Calls: You call `cwd().openFile()` because `openFile()` is a method on the `Dir` type Why this structure matters for Grep-Lite: ```zig // This is why we write: const file = try std.fs.cwd().openFile(path, .{}); // ^ ^ // | +-- Method on Dir // +----------- Returns Dir handle ``` The two-step process (`cwd()` → `openFile()`) gives you control over which directory to open files in. While this example uses the current directory, you could equally use: - `std.fs.openDirAbsolute()` for absolute paths - `dir.openFile()` for files relative to any directory handle - `std.fs.openFileAbsolute()` to skip the `Dir` entirely This composable design makes filesystem code testable (use a temporary directory) and portable (the same API works across platforms). ## Section: Scanning for Matches [section_id: section-2] [section_url: https://zigbook.net/chapters/06__project-grep-lite#section-2] Once we own a slice for each line, matching is a one-liner with `std.mem.indexOf`. We reuse the TempConv pattern of reserving stdout for successful output and stderr for diagnostics, making the tool piping-friendly. ### Subsection: Complete Grep-Lite Listing [section_id: workflow-1] [section_url: https://zigbook.net/chapters/06__project-grep-lite#workflow-1] The full listing below highlights how the helper functions slot together. Pay attention to the comments that tie each block back to the sections above. ```zig const std = @import("std"); // Chapter 6 – Grep-Lite: stream a file line by line and echo only the matches // to stdout while errors become clear diagnostics on stderr. const CliError = error{MissingArgs}; fn printUsage() void { std.debug.print("usage: grep-lite \n", .{}); } fn trimNewline(line: []const u8) []const u8 { if (line.len > 0 and line[line.len - 1] == '\r') { return line[0 .. line.len - 1]; } return line; } pub fn main() !void { const allocator = std.heap.page_allocator; const args = try std.process.argsAlloc(allocator); defer std.process.argsFree(allocator, args); if (args.len == 1 or (args.len == 2 and std.mem.eql(u8, args[1], "--help"))) { printUsage(); return; } if (args.len != 3) { std.debug.print("error: expected a pattern and a path\n", .{}); printUsage(); std.process.exit(1); } const pattern = args[1]; const path = args[2]; var file = std.fs.cwd().openFile(path, .{ .mode = .read_only }) catch { std.debug.print("error: unable to open '{s}'\n", .{path}); std.process.exit(1); }; defer file.close(); // Buffered stdout using modern Writer API var out_buf: [8 * 1024]u8 = undefined; var file_writer = std.fs.File.writer(std.fs.File.stdout(), &out_buf); const stdout = &file_writer.interface; // Section 1.2: load the complete file eagerly while enforcing a guard so // unexpected multi-megabyte inputs do not exhaust memory. const max_bytes = 8 * 1024 * 1024; const contents = file.readToEndAlloc(allocator, max_bytes) catch |err| switch (err) { error.FileTooBig => { std.debug.print("error: file exceeds {} bytes limit\n", .{max_bytes}); std.process.exit(1); }, else => return err, }; defer allocator.free(contents); // Section 2.1: split the buffer on newlines; each slice references the // original allocation so we incur zero extra copies. var lines = std.mem.splitScalar(u8, contents, '\n'); var matches: usize = 0; while (lines.next()) |raw_line| { const line = trimNewline(raw_line); // Section 2: reuse `std.mem.indexOf` so we highlight exact matches // without building temporary slices. if (std.mem.indexOf(u8, line, pattern) != null) { matches += 1; try stdout.print("{s}\n", .{line}); } } if (matches == 0) { std.debug.print("no matches for '{s}' in {s}\n", .{ pattern, path }); } // Flush buffered stdout and finalize file position try file_writer.end(); } ``` Run: ```shell $ zig run grep_lite.zig -- pattern grep_lite.zig ``` Output: ```shell std.debug.print("usage: grep-lite \n", .{}); std.debug.print("error: expected a pattern and a path\n", .{}); const pattern = args[1]; if (std.mem.indexOf(u8, line, pattern) != null) { std.debug.print("no matches for '{s}' in {s}\n", .{ pattern, path }); ``` NOTE: The output shows every source line containing the literal word `pattern`. Your match list will differ when run against other files. ### Subsection: Detecting Missing Files Gracefully [section_id: workflow-2] [section_url: https://zigbook.net/chapters/06__project-grep-lite#workflow-2] To keep shell scripts predictable, the tool emits a single-line diagnostic and exits with a non-zero status when a file path cannot be opened. ```shell $ zig run grep_lite.zig -- foo missing.txt ``` Output: ```shell error: unable to open 'missing.txt' ``` ## Section: Notes & Caveats [section_id: notes-caveats] [section_url: https://zigbook.net/chapters/06__project-grep-lite#notes-caveats] - `readToEndAlloc` is simple but loads the entire file; add a streaming reader later if you need to handle very large inputs. - The size cap prevents runaway allocations. Raise or make it configurable once you trust your deployment environment. - This example uses a buffered stdout writer for matches and `std.debug.print` for diagnostics to stderr; we flush via the writer’s `end()` at exit (see Io.zig (https://github.com/ziglang/zig/tree/master/lib/std/Io.zig)). ## Section: Exercises [section_id: exercises] [section_url: https://zigbook.net/chapters/06__project-grep-lite#exercises] - Accept multiple files on the command line and print a `path:line` prefix for each match (see #for (https://ziglang.org/documentation/master/#for)). - Add a `--ignore-case` flag by normalizing both the pattern and each line with `std.ascii.toLower` (see ascii.zig (https://github.com/ziglang/zig/tree/master/lib/std/ascii.zig)). - Support regular expressions by integrating a third-party matcher after loading the entire buffer. ## Section: Alternatives & Edge Cases [section_id: caveats-alternatives-edge-cases] [section_url: https://zigbook.net/chapters/06__project-grep-lite#caveats-alternatives-edge-cases] - Windows files often end lines with `\r\n`; trimming the carriage return keeps substring checks clean. - Empty patterns currently match every line. Introduce an explicit guard if you prefer to treat an empty string as misuse. - To integrate with larger builds, replace `zig run` with a `zig build-exe` step and package the binary on your `PATH`. # Chapter 07 — Project [chapter_id: 07__project-safe-file-copier] [chapter_slug: project-safe-file-copier] [chapter_number: 07] [chapter_url: https://zigbook.net/chapters/07__project-safe-file-copier] ## Section: Overview [section_id: overview] [section_url: https://zigbook.net/chapters/07__project-safe-file-copier#overview] Our third project turns file I/O up a notch: build a small, robust file copier that is safe by default, emits clear diagnostics, and cleans up after itself. We’ll connect the dots from Chapter 4’s `defer`/`errdefer` patterns to real-world error handling while showcasing the standard library’s atomic copy helpers; see 04 (04__errors-resource-cleanup.xml#overview) and Dir.zig (https://github.com/ziglang/zig/tree/master/lib/std/fs/Dir.zig). Two approaches illustrate the trade-offs: - High-level: a single call to `std.fs.Dir.copyFile` performs an atomic copy and preserves file mode. - Manual streaming: open, read, and write with `defer` and `errdefer`, deleting partial outputs if anything fails, as described in #defer and errdefer (https://ziglang.org/documentation/master/#defer-and-errdefer) and File.zig (https://github.com/ziglang/zig/tree/master/lib/std/fs/File.zig). ## Section: Learning Goals [section_id: learning-goals] [section_url: https://zigbook.net/chapters/07__project-safe-file-copier#learning-goals] - Design a CLI that refuses to overwrite existing files unless explicitly forced, as described in #Command-line-flags (https://ziglang.org/documentation/master/#Command-line-flags). - Use `defer`/`errdefer` to guarantee resource cleanup and remove partial files on failure. - Choose between `Dir.copyFile` for atomic convenience and manual streaming for fine-grained control. ## Section: Correctness First: Safe-by-Default CLI [section_id: section-1] [section_url: https://zigbook.net/chapters/07__project-safe-file-copier#section-1] Clobbering a user’s data is unforgivable. This tool adopts a conservative stance: unless `--force` is provided, an existing destination aborts the copy. We also validate that the source is a regular file and keep stdout silent on success so scripts can treat “no output” as a good sign, as described in #Error-Handling (https://ziglang.org/documentation/master/#Error-Handling). ### Subsection: Aborting on Existing Destinations [section_id: section-1-sub-a] [section_url: https://zigbook.net/chapters/07__project-safe-file-copier#section-1-sub-a] We probe the destination path first. If present and `--force` is absent, we print a single-line diagnostic and exit with a non-zero status. This mirrors common Unix utilities and makes failures unambiguous. ## Section: Atomic Copy in One Call [section_id: section-2] [section_url: https://zigbook.net/chapters/07__project-safe-file-copier#section-2] Leverage the standard library when possible. `Dir.copyFile` uses a temporary file and renames it into place, which means callers never observe a partially written destination even if the process crashes mid-copy. File mode is preserved by default; timestamps are handled by `updateFile` if you need them, which we mention below. ```zig const std = @import("std"); // Chapter 7 – Safe File Copier (atomic via std.fs.Dir.copyFile) // // A minimal, safe-by-default CLI that refuses to clobber an existing // destination unless --force is provided. Uses std.fs.Dir.copyFile, // which writes to a temporary file and atomically renames it into place. // // Usage: // zig run safe_copy.zig -- // zig run safe_copy.zig -- --force const Cli = struct { force: bool = false, src: []const u8 = &[_]u8{}, dst: []const u8 = &[_]u8{}, }; fn printUsage() void { std.debug.print("usage: safe-copy [--force] \n", .{}); } fn parseArgs(allocator: std.mem.Allocator) !Cli { var cli: Cli = .{}; const args = try std.process.argsAlloc(allocator); defer std.process.argsFree(allocator, args); if (args.len == 1 or (args.len == 2 and std.mem.eql(u8, args[1], "--help"))) { printUsage(); std.process.exit(0); } var i: usize = 1; while (i < args.len and std.mem.startsWith(u8, args[i], "--")) : (i += 1) { const flag = args[i]; if (std.mem.eql(u8, flag, "--force")) { cli.force = true; } else if (std.mem.eql(u8, flag, "--help")) { printUsage(); std.process.exit(0); } else { std.debug.print("error: unknown flag '{s}'\n", .{flag}); printUsage(); std.process.exit(2); } } const remaining = args.len - i; if (remaining != 2) { std.debug.print("error: expected and \n", .{}); printUsage(); std.process.exit(2); } // Duplicate paths so they remain valid after freeing args. cli.src = try allocator.dupe(u8, args[i]); cli.dst = try allocator.dupe(u8, args[i + 1]); return cli; } pub fn main() !void { const allocator = std.heap.page_allocator; const cli = try parseArgs(allocator); const cwd = std.fs.cwd(); // Validate that source exists and is a regular file. var src_file = cwd.openFile(cli.src, .{ .mode = .read_only }) catch { std.debug.print("error: unable to open source '{s}'\n", .{cli.src}); std.process.exit(1); }; defer src_file.close(); const st = try src_file.stat(); if (st.kind != .file) { std.debug.print("error: source is not a regular file\n", .{}); std.process.exit(1); } // Respect safe-by-default semantics: refuse to overwrite unless --force. const dest_exists = blk: { _ = cwd.statFile(cli.dst) catch |err| switch (err) { error.FileNotFound => break :blk false, else => |e| return e, }; break :blk true; }; if (dest_exists and !cli.force) { std.debug.print("error: destination exists; pass --force to overwrite\n", .{}); std.process.exit(2); } // Perform an atomic copy preserving mode by default. On success, there is // intentionally no output to keep pipelines quiet and scripting-friendly. cwd.copyFile(cli.src, cwd, cli.dst, .{ .override_mode = null }) catch |err| { std.debug.print("error: copy failed ({s})\n", .{@errorName(err)}); std.process.exit(1); }; } ``` Run: ```shell $ printf 'hello, copier!\n' > from.txt $ zig run safe_copy.zig -- from.txt to.txt ``` Output: ```shell (no output) ``` TIP: `copyFile` overwrites existing files. Our wrapper checks for existence first and requires `--force` to clobber. Prefer `Dir.updateFile` if you want to also preserve atime/mtime. ### Subsection: Overwrite with Intent [section_id: workflow-1] [section_url: https://zigbook.net/chapters/07__project-safe-file-copier#workflow-1] When an output already exists, demonstrate explicit overwrite: ```shell $ printf 'v1\n' > from.txt $ printf 'old\n' > to.txt $ zig run safe_copy.zig -- from.txt to.txt error: destination exists; pass --force to overwrite $ zig run safe_copy.zig -- --force from.txt to.txt ``` Output: ```shell error: destination exists; pass --force to overwrite (no output) ``` NOTE: Success remains quiet by design; combine with `echo $?` to consume status codes in scripts. ## Section: Manual Streaming with defer/errdefer [section_id: section-3] [section_url: https://zigbook.net/chapters/07__project-safe-file-copier#section-3] For fine-grained control (or as a learning exercise), wire a `Reader` to a `Writer` and stream the bytes yourself. The crucial bit is `errdefer` to remove the destination if anything goes wrong after creation—this prevents leaving a truncated file behind. ```zig const std = @import("std"); // Chapter 7 – Safe File Copier (manual streaming with errdefer cleanup) // // Demonstrates opening, reading, writing, and cleaning up safely using // defer/errdefer. If the copy fails after destination creation, we remove // the partial file so callers never observe a truncated artifact. // // Usage: // zig run copy_stream.zig -- // zig run copy_stream.zig -- --force const Cli = struct { force: bool = false, src: []const u8 = &[_]u8{}, dst: []const u8 = &[_]u8{}, }; fn printUsage() void { std.debug.print("usage: copy-stream [--force] \n", .{}); } fn parseArgs(allocator: std.mem.Allocator) !Cli { var cli: Cli = .{}; const args = try std.process.argsAlloc(allocator); defer std.process.argsFree(allocator, args); if (args.len == 1 or (args.len == 2 and std.mem.eql(u8, args[1], "--help"))) { printUsage(); std.process.exit(0); } var i: usize = 1; while (i < args.len and std.mem.startsWith(u8, args[i], "--")) : (i += 1) { const flag = args[i]; if (std.mem.eql(u8, flag, "--force")) { cli.force = true; } else if (std.mem.eql(u8, flag, "--help")) { printUsage(); std.process.exit(0); } else { std.debug.print("error: unknown flag '{s}'\n", .{flag}); printUsage(); std.process.exit(2); } } const remaining = args.len - i; if (remaining != 2) { std.debug.print("error: expected and \n", .{}); printUsage(); std.process.exit(2); } // Duplicate paths so they remain valid after freeing args. cli.src = try allocator.dupe(u8, args[i]); cli.dst = try allocator.dupe(u8, args[i + 1]); return cli; } pub fn main() !void { const allocator = std.heap.page_allocator; const cli = try parseArgs(allocator); const cwd = std.fs.cwd(); // Open source and inspect its metadata. var src = cwd.openFile(cli.src, .{ .mode = .read_only }) catch { std.debug.print("error: unable to open source '{s}'\n", .{cli.src}); std.process.exit(1); }; defer src.close(); const st = try src.stat(); if (st.kind != .file) { std.debug.print("error: source is not a regular file\n", .{}); std.process.exit(1); } // Safe-by-default: refuse to overwrite unless --force. if (!cli.force) { const dest_exists = blk: { _ = cwd.statFile(cli.dst) catch |err| switch (err) { error.FileNotFound => break :blk false, else => |e| return e, }; break :blk true; }; if (dest_exists) { std.debug.print("error: destination exists; pass --force to overwrite\n", .{}); std.process.exit(2); } } // Create destination with exclusive mode when not forcing overwrite. var dest = cwd.createFile(cli.dst, .{ .read = false, .truncate = cli.force, .exclusive = !cli.force, .mode = st.mode, }) catch |err| switch (err) { error.PathAlreadyExists => { std.debug.print("error: destination exists; pass --force to overwrite\n", .{}); std.process.exit(2); }, else => |e| { std.debug.print("error: cannot create destination ({s})\n", .{@errorName(e)}); std.process.exit(1); }, }; // Ensure closure and cleanup order: close first, then delete on error. defer dest.close(); errdefer cwd.deleteFile(cli.dst) catch {}; // Wire a Reader/Writer pair and copy using the Writer interface. var reader: std.fs.File.Reader = .initSize(src, &.{}, st.size); var write_buf: [64 * 1024]u8 = undefined; // buffered writes var writer = std.fs.File.writer(dest, &write_buf); _ = writer.interface.sendFileAll(&reader, .unlimited) catch |err| switch (err) { error.ReadFailed => return reader.err.?, error.WriteFailed => return writer.err.?, }; // Flush buffered bytes and set the final file length. try writer.end(); } ``` Run: ```shell $ printf 'stream me\n' > src.txt $ zig run copy_stream.zig -- src.txt dst.txt ``` Output: ```shell (no output) ``` IMPORTANT: When creating the destination with `.exclusive = true`, the open fails if the file already exists. That, plus `errdefer deleteFile`, gives strong safety guarantees without races in typical single-process scenarios. ## Section: Notes & Caveats [section_id: notes-caveats] [section_url: https://zigbook.net/chapters/07__project-safe-file-copier#notes-caveats] - Atomic semantics: `Dir.copyFile` creates a temporary file and renames it into place, avoiding partial reads by other processes. On older Linux kernels, power loss may leave a temp file; see the function’s doc comment for details. - Preserving timestamps: prefer `Dir.updateFile` when you need atime/mtime to match the source, in addition to content and mode. - Performance hints: the `Writer` interface uses platform accelerations (`sendfile`, `copy_file_range`, or `fcopyfile`) when available, falling back to buffered loops; see posix.zig (https://github.com/ziglang/zig/tree/master/lib/std/posix.zig). - CLI lifetimes: duplicate `args` strings before freeing them to avoid dangling `[]u8` slices (both examples use `allocator.dupe`); see process.zig (https://github.com/ziglang/zig/tree/master/lib/std/process.zig). - Sanity checks: open the source first, then `stat()` it and require `kind == .file` to reject directories and special files. ## Section: Exercises [section_id: exercises] [section_url: https://zigbook.net/chapters/07__project-safe-file-copier#exercises] - Add a `--no-clobber` flag that forces an error even when `--force` is also present—then emit a helpful message suggesting which one to remove. - Implement `--preserve-times` by switching to `Dir.updateFile` and verifying via `stat` that timestamps match. - Teach the tool to copy file permissions from a numeric mode override (e.g., `--mode=0644`) using `CopyFileOptions.override_mode` ## Section: Alternatives & Edge Cases: [section_id: caveats-alternatives-edge-cases] [section_url: https://zigbook.net/chapters/07__project-safe-file-copier#caveats-alternatives-edge-cases] - Copying special files (directories, fifos, devices) is intentionally rejected in these examples; handle them explicitly or skip. - Cross-filesystem moves: copying plus `deleteFile` is safer than `rename` when devices differ; Zig’s helpers do the right thing given a content copy. - Very large files: prefer the high-level copy first; manual loops should chunk reads and handle short writes carefully if you don’t use the `Writer` interface. # Chapter 08 — User Types [chapter_id: 08__user-types-structs-enums-unions] [chapter_slug: user-types-structs-enums-unions] [chapter_number: 08] [chapter_url: https://zigbook.net/chapters/08__user-types-structs-enums-unions] ## Section: Overview [section_id: overview] [section_url: https://zigbook.net/chapters/08__user-types-structs-enums-unions#overview] Zig’s user-defined types are deliberately small, sharp tools. Structs compose data and behavior under a clean namespace, enums encode closed sets of states with explicit integer representations, and unions model variant data—tagged for safety or untagged for low-level control. Together, these form the backbone of ergonomic APIs and memory-aware systems code; see #Structs (https://ziglang.org/documentation/master/#Structs), #Enums (https://ziglang.org/documentation/master/#Enums), and #Unions (https://ziglang.org/documentation/master/#Unions) for reference. This chapter builds pragmatic fluency: methods and defaults on structs, enum round-trips with `@intFromEnum`/`@enumFromInt`, and both tagged and untagged unions. We’ll also peek at layout modifiers (`packed`, `extern`) and anonymous structs/tuples, which become handy for lightweight return values and FFI. See fmt.zig (https://github.com/ziglang/zig/tree/master/lib/std/fmt.zig) and math.zig (https://github.com/ziglang/zig/tree/master/lib/std/math.zig) for related helpers. ## Section: Learning Goals [section_id: learning-goals] [section_url: https://zigbook.net/chapters/08__user-types-structs-enums-unions#learning-goals] - Define and use structs with methods, defaults, and clear namespacing. - Convert enums to and from integers safely and match on them exhaustively. - Choose between tagged and untagged unions; understand when `packed`/`extern` layout matters (see #packed struct (https://ziglang.org/documentation/master/#packed-struct) and #extern struct (https://ziglang.org/documentation/master/#extern-struct)). ## Section: Structs: Data + Namespace [section_id: structs] [section_url: https://zigbook.net/chapters/08__user-types-structs-enums-unions#structs] Structs gather fields and related helper functions. Methods are just functions with an explicit receiver parameter—no magic, which keeps call sites obvious and unit-testable. Defaults reduce boilerplate for common cases. ```zig const std = @import("std"); // Chapter 8 — Struct basics: fields, methods, defaults, namespacing // // Demonstrates defining a struct with fields and methods, including // default field values. Also shows namespacing of methods vs free functions. // // Usage: // zig run struct_basics.zig const Point = struct { x: i32, y: i32 = 0, // default value pub fn len(self: Point) f64 { const dx = @as(f64, @floatFromInt(self.x)); const dy = @as(f64, @floatFromInt(self.y)); return std.math.sqrt(dx * dx + dy * dy); } pub fn translate(self: *Point, dx: i32, dy: i32) void { self.x += dx; self.y += dy; } }; // Namespacing: free function in file scope vs method fn distanceFromOrigin(p: Point) f64 { return p.len(); } pub fn main() !void { var p = Point{ .x = 3 }; // y uses default 0 std.debug.print("p=({d},{d}) len={d:.3}\n", .{ p.x, p.y, p.len() }); p.translate(-3, 4); std.debug.print("p=({d},{d}) len={d:.3}\n", .{ p.x, p.y, distanceFromOrigin(p) }); } ``` Run: ```shell $ zig run struct_basics.zig ``` Output: ```shell p=(3,0) len=3.000 p=(0,4) len=4.000 ``` TIP: Methods are namespaced functions; you can freely mix free functions and methods depending on testability and API clarity. ## Section: Enums: States with Bit-Exact Reprs [section_id: enums] [section_url: https://zigbook.net/chapters/08__user-types-structs-enums-unions#enums] Enums can set their integer representation (e.g., `enum(u8)`) and convert to/from integers with builtins. A `switch` over an enum must be exhaustive unless you include `else`, which is perfect for catching new states at compile time. ```zig const std = @import("std"); // Chapter 8 — Enums: integer repr, conversions, exhaustiveness // // Demonstrates defining an enum with explicit integer representation, // converting between enum and integer using @intFromEnum and @enumFromInt, // and pattern matching with exhaustiveness checking. // // Usage: // zig run enum_roundtrip.zig const Mode = enum(u8) { Idle = 0, Busy = 1, Paused = 2, }; fn describe(m: Mode) []const u8 { return switch (m) { .Idle => "idle", .Busy => "busy", .Paused => "paused", }; } pub fn main() !void { const m: Mode = .Busy; const int_val: u8 = @intFromEnum(m); std.debug.print("m={s} int={d}\n", .{ describe(m), int_val }); // Round-trip using @enumFromInt; the integer must map to a declared tag. const m2: Mode = @enumFromInt(2); std.debug.print("m2={s} int={d}\n", .{ describe(m2), @intFromEnum(m2) }); } ``` Run: ```shell $ zig run enum_roundtrip.zig ``` Output: ```shell m=busy int=1 m2=paused int=2 ``` NOTE: `@enumFromInt` requires that the integer maps to a declared tag. If you expect unknown values (e.g., file formats), consider a sentinel tag, validation paths, or separate integer parsing with explicit error handling. ## Section: Unions: Variant Data [section_id: unions] [section_url: https://zigbook.net/chapters/08__user-types-structs-enums-unions#unions] A tagged union carries both a tag and a payload; pattern matching is straightforward and type-safe. Untagged unions require you to manage the active field manually and are appropriate for low-level bit reinterpretations or FFI shims. ```zig const std = @import("std"); // Chapter 8 — Unions: tagged and untagged // // Demonstrates a tagged union (with enum discriminant) and an untagged union // (without discriminant). Tagged unions are safe and idiomatic; untagged // unions are advanced and unsafe if used incorrectly. // // Usage: // zig run union_demo.zig const Kind = enum { number, text }; const Value = union(Kind) { number: i64, text: []const u8, }; // Untagged union (advanced): requires external tracking and is unsafe if used wrong. const Raw = union { u: u32, i: i32 }; pub fn main() !void { var v: Value = .{ .number = 42 }; printValue("start: ", v); v = .{ .text = "hi" }; printValue("update: ", v); // Untagged example: write as u32, read as i32 (bit reinterpret). const r = Raw{ .u = 0xFFFF_FFFE }; // -2 as signed 32-bit const as_i: i32 = @bitCast(r.u); std.debug.print("raw u=0x{X:0>8} i={d}\n", .{ r.u, as_i }); } fn printValue(prefix: []const u8, v: Value) void { switch (v) { .number => |n| std.debug.print("{s}number={d}\n", .{ prefix, n }), .text => |s| std.debug.print("{s}{s}\n", .{ prefix, s }), } } ``` Run: ```shell $ zig run union_demo.zig ``` Output: ```shell start: number=42 update: hi raw u=0xFFFFFFFE i=-2 ``` WARNING: Reading a different field from an untagged union without reinterpreting the bits (e.g., via `@bitCast`) is illegal; Zig prevents this at compile time. Prefer tagged unions for safety unless you truly need the control. ### Subsection: Tagged Union Memory Representation [section_id: _tagged_union_memory_representation] [section_url: https://zigbook.net/chapters/08__user-types-structs-enums-unions#_tagged_union_memory_representation] Understanding how tagged unions are laid out in memory clarifies the safety vs space trade-off and explains when to choose tagged vs untagged unions: ```text graph TB subgraph "Tagged Union Definition" TAGGED["const Value = union(enum) {
number: i32, // 4 bytes
text: []const u8, // 16 bytes (ptr+len)
}"] end subgraph "Tagged Union Memory (24 bytes on 64-bit)" TAG_MEM["Memory Layout:

| tag (u8) | padding | payload (16 bytes) |

Tag identifies active field
Payload holds largest variant"] end subgraph "Untagged Union Definition" UNTAGGED["const Raw = union {
number: i32,
text: []const u8,
}"] end subgraph "Untagged Union Memory (16 bytes)" UNTAG_MEM["Memory Layout:

| payload (16 bytes) |

No tag - you track active field
Size = largest variant only"] end TAGGED --> TAG_MEM UNTAGGED --> UNTAG_MEM subgraph "Access Patterns" SAFE["Tagged: Safe Pattern Matching
switch (value) {
.number => |n| use(n),
.text => |t| use(t),
}"] UNSAFE["Untagged: Manual Tracking
// You must know which field is active
const n = raw.number; // Unsafe!"] end TAG_MEM --> SAFE UNTAG_MEM --> UNSAFE ``` Memory layout details: Tagged Union: - Size = tag size + padding + largest variant size - Tag field (typically u8 or smallest integer that fits tag count) - Padding for alignment of payload - Payload space sized to hold the largest variant - Example: `union(enum) { i32, []const u8 }` = 1 byte tag + 7 bytes padding + 16 bytes payload = 24 bytes Untagged Union: - Size = largest variant size (no tag overhead) - No runtime tag to check - You’re responsible for tracking which field is active - Example: `union { i32, []const u8 }` = 16 bytes (just the payload) When to use each: - Use Tagged Unions (default choice): - Use Untagged Unions (rare, expert use): Safety guarantees: Tagged unions provide compile-time exhaustiveness checking and runtime tag validation: ```zig const val = Value{ .number = 42 }; switch (val) { .number => |n| print("{}", .{n}), // OK - matches tag .text => |t| print("{s}", .{t}), // Compiler ensures both cases covered } ``` Untagged unions require you to maintain safety invariants manually—the compiler can’t help you. ## Section: Layout and Anonymous Structs/Tuples [section_id: layout-anon] [section_url: https://zigbook.net/chapters/08__user-types-structs-enums-unions#layout-anon] When you must fit bits precisely (wire formats) or match C ABI layout, Zig offers `packed` and `extern`. Anonymous structs (often called "tuples") are convenient for quick multi-value returns. ```zig const std = @import("std"); // Chapter 8 — Layout (packed/extern) and anonymous structs/tuples const Packed = packed struct { a: u3, b: u5, }; const Extern = extern struct { a: u32, b: u8, }; pub fn main() !void { // Packed bit-fields combine into a single byte. std.debug.print("packed.size={d}\n", .{@sizeOf(Packed)}); // Extern layout matches the C ABI (padding may be inserted). std.debug.print("extern.size={d} align={d}\n", .{ @sizeOf(Extern), @alignOf(Extern) }); // Anonymous struct (tuple) literals and destructuring. const pair = .{ "x", 42 }; const name = @field(pair, "0"); const value = @field(pair, "1"); std.debug.print("pair[0]={s} pair[1]={d} via names: {s}/{d}\n", .{ @field(pair, "0"), @field(pair, "1"), name, value }); } ``` Run: ```shell $ zig run layout_and_anonymous.zig ``` Output: ```shell packed.size=1 extern.size=8 align=4 pair[0]=x pair[1]=42 via names: x/42 ``` NOTE: Tuple field access uses `@field(val, "0")` and `@field(val, "1")`. They’re anonymous structs with numeric field names, which keeps them simple and allocation-free. ### Subsection: Memory Layout: Default vs Packed vs Extern [section_id: _memory_layout_default_vs_packed_vs_extern] [section_url: https://zigbook.net/chapters/08__user-types-structs-enums-unions#_memory_layout_default_vs_packed_vs_extern] Zig offers three struct layout strategies, each with different trade-offs for memory efficiency, performance, and compatibility: ```text graph TB subgraph "Default Layout (Optimized)" DEF_CODE["const Point = struct {
x: u8, // 1 byte
y: u32, // 4 bytes
z: u8, // 1 byte
};"] DEF_MEM["Memory: 12 bytes

| x | pad(3) | y(4) | z | pad(3) |

Compiler reorders & pads for efficiency"] end subgraph "Packed Layout (No Padding)" PACK_CODE["const Flags = packed struct {
a: bool, // 1 bit
b: u3, // 3 bits
c: bool, // 1 bit
d: u3, // 3 bits
};"] PACK_MEM["Memory: 1 byte

| abcd(8 bits) |

No padding, bit-exact packing"] end subgraph "Extern Layout (C ABI)" EXT_CODE["const Data = extern struct {
x: u8,
y: u32,
z: u8,
};"] EXT_MEM["Memory: 12 bytes

| x | pad(3) | y(4) | z | pad(3) |

C ABI rules, field order preserved"] end DEF_CODE --> DEF_MEM PACK_CODE --> PACK_MEM EXT_CODE --> EXT_MEM subgraph "Key Differences" DIFF1["Default: Compiler can reorder fields
Extern: Field order fixed
Packed: Bit-level packing"] DIFF2["Default: Optimized alignment
Extern: Platform ABI alignment
Packed: No alignment (bitfields)"] end ``` Layout mode comparison: | Layout | Size/Alignment | Field Order | Use Case | | --- | --- | --- | --- | | Default | Optimized by compiler | Can be reordered | Normal Zig code | | Packed | Bit-exact, no padding | Fixed, bit-level | Wire formats, bit flags | | Extern | C ABI rules | Fixed (declaration order) | FFI, C interop | Detailed behavior: Default Layout: ```zig const Point = struct { x: u8, // Compiler might reorder this y: u32, // to minimize padding z: u8, }; // Compiler chooses optimal order, typically: // y (4 bytes, aligned) + x (1 byte) + z (1 byte) + padding ``` Packed Layout: ```zig const Flags = packed struct { enabled: bool, // bit 0 mode: u3, // bits 1-3 priority: u4, // bits 4-7 }; // Total: 8 bits = 1 byte, no padding // Perfect for hardware registers and wire protocols ``` Extern Layout: ```zig const CHeader = extern struct { version: u32, // Matches C struct layout exactly flags: u16, // Field order preserved padding: u16, // Explicit padding if needed }; // For calling C functions or reading C-written binary data ``` When to use each layout: - Default (no modifier): - Packed: - Extern: Important notes: - Use `@sizeOf(T)` and `@alignOf(T)` to verify layout - Packed structs can be slower—measure before optimizing - Extern structs must match the C definition exactly (including padding) - Default layout may change between compiler versions (always safe, but field order not guaranteed) ## Section: Notes & Caveats [section_id: notes-caveats] [section_url: https://zigbook.net/chapters/08__user-types-structs-enums-unions#notes-caveats] - Methods are sugar-free; consider making helpers `pub` inside the struct for discoverability and test scoping. - Enum reprs (`enum(uN)`) define size and affect ABI/FFI—choose the smallest that fits your protocol. - Untagged unions are sharp tools. In most application code, prefer tagged unions and pattern matching. ## Section: Exercises [section_id: exercises] [section_url: https://zigbook.net/chapters/08__user-types-structs-enums-unions#exercises] - Add a `scale` method to `Point` that multiplies both coordinates by a `f64`, then reworks `len` to avoid precision loss for large integers. - Extend `Mode` with a new `Error` state and observe how the compiler enforces an updated `switch`. - Create a tagged union representing a JSON scalar (`null`, `bool`, `number`, `string`) and write a `print` function that formats each case. ## Section: Alternatives & Edge Cases [section_id: caveats-alternatives-edge-cases] [section_url: https://zigbook.net/chapters/08__user-types-structs-enums-unions#caveats-alternatives-edge-cases] - ABI layout: `extern` respects the platform ABI. Verify sizes with `@sizeOf`/`@alignOf` and cross-compile when shipping libraries. - Bit packing: `packed struct` compresses fields but can increase instruction count; measure before committing in hot paths. - Tuples vs named structs: prefer named structs for stable APIs; tuples shine for local, short-lived glue. # Chapter 09 — Project [chapter_id: 09__project-hexdump] [chapter_slug: project-hexdump] [chapter_number: 09] [chapter_url: https://zigbook.net/chapters/09__project-hexdump] ## Section: Overview [section_id: overview] [section_url: https://zigbook.net/chapters/09__project-hexdump#overview] This project turns raw bytes into a tidy, alignment-aware hex view. We’ll read a file incrementally, format each line as `OFFSET: HEX ASCII`, and keep output stable across platforms. The writer interface uses buffered stdout via `std.fs.File.writer` and `std.Io.Writer`, as described in File.zig (https://github.com/ziglang/zig/tree/master/lib/std/fs/File.zig) and Io.zig (https://github.com/ziglang/zig/tree/master/lib/std/Io.zig). The formatter prints 16 bytes per line by default and can be configured with `--width N` (4..32). Bytes are grouped `8|8` to ease scanning, and non-printable ASCII becomes a dot in the right-hand gutter, as described in fmt.zig (https://github.com/ziglang/zig/tree/master/lib/std/fmt.zig) and #Command-line-flags (https://ziglang.org/documentation/master/#Command-line-flags). ## Section: Learning Goals [section_id: learning-goals] [section_url: https://zigbook.net/chapters/09__project-hexdump#learning-goals] - Parse CLI flags and validate numbers with `std.fmt.parseInt`. - Stream a file with a fixed buffer and assemble exact-width output lines. - Use the non-deprecated `File.Writer` + `Io.Writer` to buffer stdout and flush cleanly. ## Section: Building the Dump [section_id: building] [section_url: https://zigbook.net/chapters/09__project-hexdump#building] We’ll wire three pieces: a tiny CLI parser, a line formatter, and a loop that feeds the formatter in exact-width chunks. The implementation leans on Zig’s slices and explicit lifetimes (dup the path before freeing args) to stay robust; see process.zig (https://github.com/ziglang/zig/tree/master/lib/std/process.zig) and #Error-Handling (https://ziglang.org/documentation/master/#Error-Handling). ```zig const std = @import("std"); // Chapter 9 – Project: Hexdump // // A small, alignment-aware hexdump that prints: // OFFSET: 16 hex bytes (grouped 8|8) ASCII // Default width is 16 bytes per line; override with --width N (4..32). // // Usage: // zig run hexdump.zig -- // zig run hexdump.zig -- --width 8 const Cli = struct { width: usize = 16, path: []const u8 = &[_]u8{}, }; fn printUsage() void { std.debug.print("usage: hexdump [--width N] \n", .{}); } fn parseArgs(allocator: std.mem.Allocator) !Cli { var cli: Cli = .{}; const args = try std.process.argsAlloc(allocator); defer std.process.argsFree(allocator, args); if (args.len == 1 or (args.len == 2 and std.mem.eql(u8, args[1], "--help"))) { printUsage(); std.process.exit(0); } var i: usize = 1; while (i + 1 < args.len and std.mem.eql(u8, args[i], "--width")) : (i += 2) { const val = args[i + 1]; cli.width = std.fmt.parseInt(usize, val, 10) catch { std.debug.print("error: invalid width '{s}'\n", .{val}); std.process.exit(2); }; if (cli.width < 4 or cli.width > 32) { std.debug.print("error: width must be between 4 and 32\n", .{}); std.process.exit(2); } } if (i >= args.len) { std.debug.print("error: expected \n", .{}); printUsage(); std.process.exit(2); } // Duplicate the path so it remains valid after freeing args. cli.path = try allocator.dupe(u8, args[i]); return cli; } fn isPrintable(c: u8) bool { // Printable ASCII (space through tilde) return c >= 0x20 and c <= 0x7E; } fn dumpLine(stdout: *std.Io.Writer, offset: usize, bytes: []const u8, width: usize) !void { // OFFSET (8 hex digits), colon and space try stdout.print("{X:0>8}: ", .{offset}); // Hex bytes with grouping at 8 var i: usize = 0; while (i < width) : (i += 1) { if (i < bytes.len) { try stdout.print("{X:0>2} ", .{bytes[i]}); } else { // pad absent bytes to keep ASCII column aligned try stdout.print(" ", .{}); } if (i + 1 == width / 2) { try stdout.print(" ", .{}); // extra gap between 8|8 } } // Two spaces before ASCII gutter try stdout.print(" ", .{}); i = 0; while (i < width) : (i += 1) { if (i < bytes.len) { const ch: u8 = if (isPrintable(bytes[i])) bytes[i] else '.'; try stdout.print("{c}", .{ch}); } else { try stdout.print(" ", .{}); } } try stdout.print("\n", .{}); } pub fn main() !void { const allocator = std.heap.page_allocator; const cli = try parseArgs(allocator); var file = std.fs.cwd().openFile(cli.path, .{ .mode = .read_only }) catch { std.debug.print("error: unable to open '{s}'\n", .{cli.path}); std.process.exit(1); }; defer file.close(); // Buffered stdout using the modern File.Writer + Io.Writer interface. var out_buf: [16 * 1024]u8 = undefined; var file_writer = std.fs.File.writer(std.fs.File.stdout(), &out_buf); const stdout = &file_writer.interface; var offset: usize = 0; var carry: [64]u8 = undefined; // enough for max width 32 var carry_len: usize = 0; var buf: [64 * 1024]u8 = undefined; while (true) { const n = try file.read(buf[0..]); if (n == 0 and carry_len == 0) break; var idx: usize = 0; while (idx < n) { // fill a line from carry + buffer bytes const need = cli.width - carry_len; const take = @min(need, n - idx); @memcpy(carry[carry_len .. carry_len + take], buf[idx .. idx + take]); carry_len += take; idx += take; if (carry_len == cli.width) { try dumpLine(stdout, offset, carry[0..carry_len], cli.width); offset += carry_len; carry_len = 0; } } if (n == 0 and carry_len > 0) { try dumpLine(stdout, offset, carry[0..carry_len], cli.width); offset += carry_len; carry_len = 0; } } try file_writer.end(); } ``` Run: ```shell $ zig run hexdump.zig -- sample.txt ``` Output: ```shell 00000000: 48 65 6C 6C 6F 2C 20 48 65 78 64 75 6D 70 21 0A Hello, Hexdump!. ``` NOTE: The ASCII gutter replaces non-printable bytes with `.`; the newline at the end of the file shows up as `0A` and a dot on the right. ## Section: Width and Grouping [section_id: width] [section_url: https://zigbook.net/chapters/09__project-hexdump#width] Pass `--width N` to change bytes per line. Grouping still splits the line in half (`N/2`) to keep the eye anchored. Run: ```shell $ zig run hexdump.zig -- --width 8 sample.txt ``` Output: ```shell 00000000: 48 65 6C 6C 6F 2C 20 48 Hello, H 00000008: 65 78 64 75 6D 70 21 0A exdump!. ``` TIP: The line formatter pads both the hex and ASCII regions so that the columns align nicely on the last line, where bytes may not fill a complete width. ## Section: Notes & Caveats [section_id: notes-caveats] [section_url: https://zigbook.net/chapters/09__project-hexdump#notes-caveats] - Avoid deprecated I/O surfaces; this example uses `File.writer` plus an `Io.Writer` buffer and calls `end()` to flush and set the final position. - Hex formatting is kept simple—no `-C`-style index columns beyond the offset. Extending the formatter is an easy follow-on exercise. - Argument lifetimes matter: duplicate the path string if you free `args` before using `cli.path`. ## Section: Exercises [section_id: exercises] [section_url: https://zigbook.net/chapters/09__project-hexdump#exercises] - Add `--group N` to control the extra space position (currently `N = width/2`). - Support `--offset 0xNN` to start addresses at a base other than zero. - Include a right-hand hex checksum per line and a final footer (e.g., total bytes). ## Section: Alternatives & Edge Cases [section_id: caveats-alternatives-edge-cases] [section_url: https://zigbook.net/chapters/09__project-hexdump#caveats-alternatives-edge-cases] - Large files: the code streams in fixed-size blocks and assembles lines; adjust buffer sizes to match your I/O environment. - Non-ASCII encodings: the ASCII gutter is deliberately crude. For UTF-8 awareness, you’d need a more careful renderer; see unicode.zig (https://github.com/ziglang/zig/tree/master/lib/std/unicode.zig). - Binary pipes: read from `stdin` when no path is provided; adapt the open/loop accordingly if you want to support pipelines. # Chapter 10 — Allocators & Memory Management [chapter_id: 10__allocators-and-memory-management] [chapter_slug: allocators-and-memory-management] [chapter_number: 10] [chapter_url: https://zigbook.net/chapters/10__allocators-and-memory-management] ## Section: Overview [section_id: overview] [section_url: https://zigbook.net/chapters/10__allocators-and-memory-management#overview] Zig’s approach to dynamic memory is explicit, composable, and testable. Rather than hiding allocation behind implicit globals, APIs accept a `std.mem.Allocator` and return ownership clearly to their caller. This chapter shows the core allocator interface (`alloc`, `free`, `realloc`, `resize`, `create`, `destroy`), introduces the most common allocator implementations (page allocator, Debug/GPA with leak detection, arenas, and fixed buffers), and establishes patterns for passing allocators through your own APIs (see Allocator.zig (https://github.com/ziglang/zig/tree/master/lib/std/mem/Allocator.zig) and heap.zig (https://github.com/ziglang/zig/tree/master/lib/std/heap.zig)). You’ll learn when to prefer bulk-free arenas, how to use a fixed stack buffer to eliminate heap traffic, and how to grow and shrink allocations safely. These skills underpin the rest of the book—from collections to I/O adapters—and will make the later projects both faster and more robust (see 03 (03__data-fundamentals.xml)). ## Section: Learning Goals [section_id: learning-goals] [section_url: https://zigbook.net/chapters/10__allocators-and-memory-management#learning-goals] - Use `std.mem.Allocator` to allocate, free, and resize typed slices and single items. - Choose an allocator: page allocator, Debug/GPA (leak detection), arena, fixed buffer, or a stack-fallback composition. - Design functions that accept an allocator and return owned memory to the caller (see 08 (08__user-types-structs-enums-unions.xml)). ## Section: The Allocator Interface [section_id: interface] [section_url: https://zigbook.net/chapters/10__allocators-and-memory-management#interface] Zig’s allocator is a small, value-type interface with methods for typed allocation and explicit deallocation. The wrappers handle sentinels and alignment so you can stay at the `[]T` level most of the time. ### Subsection: alloc/free, create/destroy, and sentinels [section_id: interface-basics] [section_url: https://zigbook.net/chapters/10__allocators-and-memory-management#interface-basics] The essentials: allocate a typed slice, mutate its elements, then free. For single items, prefer `create`/`destroy`. Use `allocSentinel` (or `dupeZ`) when you need a null terminator for C interop. ```zig const std = @import("std"); pub fn main() !void { const allocator = std.heap.page_allocator; // OS-backed; fast & simple // Allocate a small buffer and fill it. const buf = try allocator.alloc(u8, 5); defer allocator.free(buf); for (buf, 0..) |*b, i| b.* = 'a' + @as(u8, @intCast(i)); std.debug.print("buf: {s}\n", .{buf}); // Create/destroy a single item. const Point = struct { x: i32, y: i32 }; const p = try allocator.create(Point); defer allocator.destroy(p); p.* = .{ .x = 7, .y = -3 }; std.debug.print("point: (x={}, y={})\n", .{ p.x, p.y }); // Allocate a null-terminated string (sentinel). Great for C APIs. var hello = try allocator.allocSentinel(u8, 5, 0); defer allocator.free(hello); @memcpy(hello[0..5], "hello"); std.debug.print("zstr: {s}\n", .{hello}); } ``` Run: ```shell $ zig run alloc_free_basics.zig ``` Output: ```shell buf: abcde point: (x=7, y=-3) zstr: hello ``` TIP: Prefer `{s}` to print `[]const u8` slices (no terminator required). Use `allocSentinel` or `dupeZ` when interoperating with APIs that require a trailing `\0`. ### Subsection: How the Allocator Interface Works Under the Hood [section_id: allocator-vtable] [section_url: https://zigbook.net/chapters/10__allocators-and-memory-management#allocator-vtable] The `std.mem.Allocator` type is a type-erased interface using a pointer and vtable. This design allows any allocator implementation to be passed through the same interface, enabling runtime polymorphism without virtual dispatch overhead for the common case. ```text graph TB ALLOC["Allocator"] PTR["ptr: *anyopaque"] VTABLE["vtable: *VTable"] ALLOC --> PTR ALLOC --> VTABLE subgraph "VTable Functions" ALLOCFN["alloc(*anyopaque, len, alignment, ret_addr)"] RESIZEFN["resize(*anyopaque, memory, alignment, new_len, ret_addr)"] REMAPFN["remap(*anyopaque, memory, alignment, new_len, ret_addr)"] FREEFN["free(*anyopaque, memory, alignment, ret_addr)"] end VTABLE --> ALLOCFN VTABLE --> RESIZEFN VTABLE --> REMAPFN VTABLE --> FREEFN subgraph "High-Level API" CREATE["create(T)"] DESTROY["destroy(ptr)"] ALLOCAPI["alloc(T, n)"] FREE["free(slice)"] REALLOC["realloc(slice, new_len)"] end ALLOC --> CREATE ALLOC --> DESTROY ALLOC --> ALLOCAPI ALLOC --> FREE ALLOC --> REALLOC ``` The vtable contains four fundamental operations: - alloc: Returns a pointer to `len` bytes with specified alignment, or error on failure - resize: Attempts to expand or shrink memory in place, returns `bool` - remap: Attempts to expand or shrink memory, allowing relocation (used by `realloc`) - free: Frees and invalidates a region of memory The high-level API (`create`, `destroy`, `alloc`, `free`, `realloc`) wraps these vtable functions with type-safe, ergonomic methods. This two-layer design keeps allocator implementations simple while providing convenient typed allocation to users (see Allocator.zig (https://github.com/ziglang/zig/blob/master/lib/std/mem/Allocator.zig)). ### Subsection: Debug/GPA and Arena Allocators [section_id: gpa-arena] [section_url: https://zigbook.net/chapters/10__allocators-and-memory-management#gpa-arena] For whole-program work, a Debug/GPA is the default: it tracks allocations and reports leaks at `deinit()`. For scoped, scratch allocations, an arena returns everything in one shot during `deinit()`. ```zig const std = @import("std"); pub fn main() !void { // GeneralPurposeAllocator with leak detection on deinit. var gpa: std.heap.GeneralPurposeAllocator(.{}) = .init; defer { const leaked = gpa.deinit() == .leak; if (leaked) @panic("leak detected"); } const alloc = gpa.allocator(); const nums = try alloc.alloc(u64, 4); defer alloc.free(nums); for (nums, 0..) |*n, i| n.* = @as(u64, i + 1); var sum: u64 = 0; for (nums) |n| sum += n; std.debug.print("gpa sum: {}\n", .{sum}); // Arena allocator: bulk free with deinit. var arena_inst = std.heap.ArenaAllocator.init(alloc); defer arena_inst.deinit(); const arena = arena_inst.allocator(); const msg = try arena.dupe(u8, "temporary allocations live here"); std.debug.print("arena msg len: {}\n", .{msg.len}); } ``` Run: ```shell $ zig run gpa_arena.zig ``` Output: ```shell gpa sum: 10 arena msg len: 31 ``` NOTE: In Zig 0.15.x, `std.heap.GeneralPurposeAllocator` is a thin alias to the Debug allocator. Always check the return of `deinit()`: `.leak` indicates something wasn’t freed. ## Section: Choosing and Composing Allocators [section_id: composition] [section_url: https://zigbook.net/chapters/10__allocators-and-memory-management#composition] Allocators are regular values: you can pass them, wrap them, and compose them. Two workhorse tools are the fixed buffer allocator (for stack-backed bursts of allocations) and `realloc`/`resize` for dynamic growth and shrinkage. ### Subsection: Wrapping Allocators for Safety and Debugging [section_id: allocator-wrapping] [section_url: https://zigbook.net/chapters/10__allocators-and-memory-management#allocator-wrapping] Because allocators are just values with a common interface, you can wrap one allocator to add functionality. The `std.mem.validationWrap` function demonstrates this pattern by adding safety checks before delegating to an underlying allocator. ```text graph TB VA["ValidationAllocator(T)"] UNDERLYING["underlying_allocator: T"] VA --> UNDERLYING subgraph "Validation Checks" CHECK1["Assert n > 0 in alloc"] CHECK2["Assert alignment is correct"] CHECK3["Assert buf.len > 0 in resize/free"] end VA --> CHECK1 VA --> CHECK2 VA --> CHECK3 UNDERLYING_PTR["getUnderlyingAllocatorPtr()"] VA --> UNDERLYING_PTR ``` The `ValidationAllocator` wrapper validates that: - Allocation sizes are greater than zero - Returned pointers have correct alignment - Memory lengths are valid in resize/free operations TIP: This pattern is powerful: you can build custom allocator wrappers that add logging, metrics collection, memory limits, or other cross-cutting concerns without modifying the underlying allocator. The wrapper simply delegates to `underlying_allocator` after performing its checks or side effects. mem.zig (https://github.com/ziglang/zig/blob/master/lib/std/mem.zig) ### Subsection: Fixed buffer on the stack [section_id: fixed-buffer] [section_url: https://zigbook.net/chapters/10__allocators-and-memory-management#fixed-buffer] Use a `FixedBufferAllocator` to get fast, zero-syscall allocations from a stack array. When you run out, you’ll get `error.OutOfMemory`—exactly the signal you need to fall back or trim inputs. ```zig const std = @import("std"); pub fn main() !void { var backing: [32]u8 = undefined; var fba = std.heap.FixedBufferAllocator.init(&backing); const A = fba.allocator(); // 3 small allocations should fit. const a = try A.alloc(u8, 8); const b = try A.alloc(u8, 8); const c = try A.alloc(u8, 8); _ = a; _ = b; _ = c; // This one should fail (32 total capacity, 24 already used). if (A.alloc(u8, 16)) |_| { std.debug.print("unexpected success\n", .{}); } else |err| switch (err) { error.OutOfMemory => std.debug.print("fixed buffer OOM as expected\n", .{}), else => return err, } } ``` Run: ```shell $ zig run fixed_buffer.zig ``` Output: ```shell fixed buffer OOM as expected ``` TIP: For a graceful fallback, compose a fixed buffer over a slower allocator with `std.heap.stackFallback(N, fallback)`. The returned object has a `.get()` method that yields a fresh `Allocator` each time. ### Subsection: Growing and shrinking safely with realloc/resize [section_id: resize-realloc] [section_url: https://zigbook.net/chapters/10__allocators-and-memory-management#resize-realloc] `realloc` returns a new slice (and may move the allocation). `resize` attempts to change length in place and returns `bool`; remember to also update your slice’s `len` when it succeeds. ```zig const std = @import("std"); pub fn main() !void { var gpa: std.heap.GeneralPurposeAllocator(.{}) = .init; defer { _ = gpa.deinit(); } const alloc = gpa.allocator(); var buf = try alloc.alloc(u8, 4); defer alloc.free(buf); for (buf, 0..) |*b, i| b.* = 'A' + @as(u8, @intCast(i)); std.debug.print("len={} contents={s}\n", .{ buf.len, buf }); // Grow using realloc (may move). buf = try alloc.realloc(buf, 8); for (buf[4..], 0..) |*b, i| b.* = 'a' + @as(u8, @intCast(i)); std.debug.print("grown len={} contents={s}\n", .{ buf.len, buf }); // Shrink in-place using resize; remember to slice. if (alloc.resize(buf, 3)) { buf = buf[0..3]; std.debug.print("shrunk len={} contents={s}\n", .{ buf.len, buf }); } else { // Fallback when in-place shrink not supported by allocator. buf = try alloc.realloc(buf, 3); std.debug.print("shrunk (realloc) len={} contents={s}\n", .{ buf.len, buf }); } } ``` Run: ```shell $ zig run resize_and_realloc.zig ``` Output: ```shell len=4 contents=ABCD grown len=8 contents=ABCDabcd shrunk (realloc) len=3 contents=ABC ``` WARNING: After `resize(buf, n) == true`, the old `buf` still has its previous `len`. Re-slice it (`buf = buf[0..n]`) so downstream code sees the new length. ### Subsection: How Alignment Works Under the Hood [section_id: alignment-system] [section_url: https://zigbook.net/chapters/10__allocators-and-memory-management#alignment-system] Zig’s memory system uses a compact power-of-two alignment representation. The `std.mem.Alignment` enum stores alignment as a log₂ value, allowing efficient storage while providing rich utility methods. ```text graph LR ALIGNMENT["Alignment enum"] subgraph "Alignment Values" A1["@'1' = 0"] A2["@'2' = 1"] A4["@'4' = 2"] A8["@'8' = 3"] A16["@'16' = 4"] end ALIGNMENT --> A1 ALIGNMENT --> A2 ALIGNMENT --> A4 ALIGNMENT --> A8 ALIGNMENT --> A16 subgraph "Key Methods" TOBYTES["toByteUnits() -> usize"] FROMBYTES["fromByteUnits(n) -> Alignment"] OF["of(T) -> Alignment"] FORWARD["forward(address) -> usize"] BACKWARD["backward(address) -> usize"] CHECK["check(address) -> bool"] end ALIGNMENT --> TOBYTES ALIGNMENT --> FROMBYTES ALIGNMENT --> OF ALIGNMENT --> FORWARD ALIGNMENT --> BACKWARD ALIGNMENT --> CHECK ``` This compact representation provides utility methods for: - Converting to/from byte units: `@"16".toByteUnits()` returns `16`, `fromByteUnits(16)` returns `@"16"` - Aligning addresses forward: `forward(addr)` rounds up to next aligned boundary - Aligning addresses backward: `backward(addr)` rounds down to previous aligned boundary - Checking alignment: `check(addr)` returns `true` if address meets alignment requirement - Type alignment: `of(T)` returns the alignment of type `T` When you see `alignedAlloc(T, .@"16", n)` or use alignment in custom allocators, you’re working with this log₂ representation. The compact storage allows Zig to track alignment efficiently without wasting space (see mem.zig (https://github.com/ziglang/zig/blob/master/lib/std/mem.zig)). ### Subsection: Allocator-as-parameter pattern [section_id: allocator-param] [section_url: https://zigbook.net/chapters/10__allocators-and-memory-management#allocator-param] Your APIs should accept an allocator and return owned memory to the caller. This keeps lifetimes explicit and lets your users pick the right allocator for their context (arena for scratch, GPA for general use, fixed buffer when available). ```zig const std = @import("std"); fn joinSep(allocator: std.mem.Allocator, parts: []const []const u8, sep: []const u8) ![]u8 { var total: usize = 0; for (parts) |p| total += p.len; if (parts.len > 0) total += sep.len * (parts.len - 1); var out = try allocator.alloc(u8, total); var i: usize = 0; for (parts, 0..) |p, idx| { @memcpy(out[i .. i + p.len], p); i += p.len; if (idx + 1 < parts.len) { @memcpy(out[i .. i + sep.len], sep); i += sep.len; } } return out; } pub fn main() !void { // Use GPA to build a string, then free. var gpa: std.heap.GeneralPurposeAllocator(.{}) = .init; defer { _ = gpa.deinit(); } const A = gpa.allocator(); const joined = try joinSep(A, &.{ "zig", "likes", "allocators" }, "-"); defer A.free(joined); std.debug.print("gpa: {s}\n", .{joined}); // Try with a tiny fixed buffer to demonstrate OOM. var buf: [8]u8 = undefined; var fba = std.heap.FixedBufferAllocator.init(&buf); const B = fba.allocator(); if (joinSep(B, &.{ "this", "is", "too", "big" }, ",")) |s| { // If it somehow fits, free it (unlikely with 16 bytes here). B.free(s); std.debug.print("fba unexpectedly succeeded\n", .{}); } else |err| switch (err) { error.OutOfMemory => std.debug.print("fba: OOM as expected\n", .{}), else => return err, } } ``` Run: ```shell $ zig run allocator_parameter.zig ``` Output: ```shell gpa: zig-likes-allocators fba: OOM as expected ``` NOTE: Returning `[]u8` (or `[]T`) shifts ownership cleanly to the caller; document that the caller must `free`. When you can, offer a `comptime`-friendly variant that writes into a caller-provided buffer. 04 (04__errors-resource-cleanup.xml) ## Section: Notes & Caveats [section_id: notes-caveats] [section_url: https://zigbook.net/chapters/10__allocators-and-memory-management#notes-caveats] - Free what you allocate. In this book, examples use `defer allocator.free(buf)` immediately after a successful `alloc`. - Shrinking: prefer `resize` for in-place shrink; fall back to `realloc` if it returns `false`. - Arenas: never return arena-owned memory to long-lived callers. Arena memory dies at `deinit()`. - GPA/Debug: check `deinit()` and wire leak detection into tests with `std.testing` (see testing.zig (https://github.com/ziglang/zig/tree/master/lib/std/testing.zig)). - Fixed buffers: great for bounded workloads; combine with `stackFallback` to degrade gracefully. ## Section: Exercises [section_id: exercises] [section_url: https://zigbook.net/chapters/10__allocators-and-memory-management#exercises] - Implement `splitJoin(allocator, s: []const u8, needle: u8) ![]u8` that splits on a byte and rejoins with `'-'`. Add a variant that writes into a caller buffer. - Rewrite one of your earlier CLI tools to accept an allocator from `main` and plumb it through. Try `ArenaAllocator` for transient buffers. 06 (06__project-grep-lite.xml) - Wrap `FixedBufferAllocator` with `stackFallback` and show how the same function succeeds on small inputs but falls back for larger ones. ## Section: Alternatives & Edge Cases [section_id: caveats-alternatives-edge-cases] [section_url: https://zigbook.net/chapters/10__allocators-and-memory-management#caveats-alternatives-edge-cases] - Alignment-sensitive allocations: use `alignedAlloc(T, .@"16", n)` or typed helpers that propagate alignment. - Zero-sized types and zero-length slices are supported by the interface; don’t special-case them. - C interop: when linking libc, consider `c_allocator`/`raw_c_allocator` for matching foreign allocation semantics; otherwise prefer page allocator/GPA. # Chapter 11 — Project [chapter_id: 11__project-dynamic-string-builder] [chapter_slug: project-dynamic-string-builder] [chapter_number: 11] [chapter_url: https://zigbook.net/chapters/11__project-dynamic-string-builder] ## Section: Overview [section_id: overview] [section_url: https://zigbook.net/chapters/11__project-dynamic-string-builder#overview] This project turns the raw allocator patterns from the previous chapter into a focused utility: a dynamic string builder that can stitch together reports, logs, and templates without scattering `[]u8` bookkeeping throughout your code. By wrapping `std.ArrayList(u8)` we keep amortized O(1) appends, expose growth metrics for debugging, and make it trivial to hand ownership to callers when the buffer is ready; see 10 (10__allocators-and-memory-management.xml) and array_list.zig (https://github.com/ziglang/zig/tree/master/lib/std/array_list.zig). Real programs live on more than one allocator, so we also stress-test the builder against stack buffers, arenas, and the general-purpose allocator. The result is a pattern you can drop into CLIs, templating tasks, or logging subsystems whenever you need flexible but explicit string assembly; see heap.zig (https://github.com/ziglang/zig/tree/master/lib/std/heap.zig). ## Section: Learning Goals [section_id: learning-goals] [section_url: https://zigbook.net/chapters/11__project-dynamic-string-builder#learning-goals] - Craft a reusable `StringBuilder` wrapper that tracks growth events while leaning on `std.ArrayList(u8)` for storage; see string_builder.zig (chapters-data/code/11__project-dynamic-string-builder/string_builder.zig). - Drive the builder through `std.io.GenericWriter` so formatted printing composes with ordinary appends; see writer.zig (https://github.com/ziglang/zig/tree/master/lib/std/io/writer.zig). - Choose between stack buffers, arenas, and heap allocators for dynamic text workflows using `std.heap.stackFallback`. ## Section: Builder Blueprint [section_id: builder-blueprint] [section_url: https://zigbook.net/chapters/11__project-dynamic-string-builder#builder-blueprint] The core utility lives in `string_builder.zig`: a thin struct that stores the caller’s allocator, an `std.ArrayList(u8)` buffer, and a handful of helpers for appends, formatting, and growth telemetry. Each operation goes through your chosen allocator, so handing the builder a different allocator instantly changes its behavior. ### Subsection: Rendering structured summaries [section_id: builder-blueprint-core] [section_url: https://zigbook.net/chapters/11__project-dynamic-string-builder#builder-blueprint-core] To see the builder in action, the following program composes a short report, captures a snapshot of length/capacity/growth, and returns an owned slice to the caller. The builder defers cleanup to `defer builder.deinit()`, so even if `toOwnedSlice` moves the buffer, the surrounding scope stays leak-free. ```zig const std = @import("std"); const builder_mod = @import("string_builder.zig"); const StringBuilder = builder_mod.StringBuilder; pub fn main() !void { // Initialize a general-purpose allocator with leak detection // This allocator tracks all allocations and reports leaks on deinit var gpa = std.heap.GeneralPurposeAllocator(.{}){}; defer { if (gpa.deinit() == .leak) std.log.err("leaked allocations detected", .{}); } const allocator = gpa.allocator(); // Create a StringBuilder with 64 bytes of initial capacity // Pre-allocating reduces reallocation overhead for known content size var builder = try StringBuilder.initCapacity(allocator, 64); defer builder.deinit(); // Build report header using basic string concatenation try builder.append("Report\n======\n"); try builder.append("source: dynamic builder\n\n"); // Define structured data for report generation // Each item represents a category with its count const items = [_]struct { name: []const u8, count: usize, }{ .{ .name = "widgets", .count = 7 }, .{ .name = "gadgets", .count = 13 }, .{ .name = "doodads", .count = 2 }, }; // Obtain a writer interface for formatted output // This allows using std.fmt.format-style print operations var writer = builder.writer(); for (items, 0..) |item, index| { // Format each item as a numbered list entry with name and count try writer.print("* {d}. {s}: {d}\n", .{ index + 1, item.name, item.count }); } // Capture allocation statistics before adding summary // Snapshot preserves metrics for analysis without affecting builder state const snapshot = builder.snapshot(); try writer.print("\nsummary: appended {d} entries\n", .{items.len}); // Transfer ownership of the constructed string to caller // After this call, builder is reset and cannot be reused without re-initialization const result = try builder.toOwnedSlice(); defer allocator.free(result); // Display the generated report alongside allocation statistics std.debug.print("{s}\n---\n{any}\n", .{ result, snapshot }); } ``` Run: ```shell $ zig run builder_core.zig ``` Output: ```shell Report ====== source: dynamic builder * 1. widgets: 7 * 2. gadgets: 13 * 3. doodads: 2 summary: appended 3 entries --- .{ .length = 88, .capacity = 224, .growth_events = 1 } ``` TIP: `snapshot()` is cheap enough to sprinkle through your code whenever you need to confirm that a given workload stays inside a particular capacity envelope. ## Section: Allocators in Action [section_id: allocators-in-action] [section_url: https://zigbook.net/chapters/11__project-dynamic-string-builder#allocators-in-action] Allocators define how the builder behaves under pressure: `stackFallback` gives blazing-fast stack writes until the buffer spills, an arena lets you bulk-free whole generations, and the GPA keeps leak detection in play. This section demonstrates how the same builder code adapts to different allocation strategies. ### Subsection: Stack buffer with an arena safety net [section_id: allocators-fallback] [section_url: https://zigbook.net/chapters/11__project-dynamic-string-builder#allocators-fallback] Here we wrap the builder in a stack-backed allocator that falls back to an arena once the 256-byte scratch space fills up. The output shows how the small report stays within the stack buffer while the larger one spills into the arena and grows four times; see 10 (10__allocators-and-memory-management.xml). ```zig const std = @import("std"); const builder_mod = @import("string_builder.zig"); const StringBuilder = builder_mod.StringBuilder; const Stats = builder_mod.Stats; /// Container for a generated report and its allocation statistics const Report = struct { text: []u8, stats: Stats, }; /// Builds a text report with random sample data /// Demonstrates StringBuilder usage with various allocator strategies fn buildReport(allocator: std.mem.Allocator, label: []const u8, sample_count: usize) !Report { // Initialize StringBuilder with the provided allocator var builder = StringBuilder.init(allocator); defer builder.deinit(); // Write report header try builder.append("label: "); try builder.append(label); try builder.append("\n"); // Initialize PRNG with a seed that varies based on sample_count // Ensures reproducible but different sequences for different report sizes var prng = std.Random.DefaultPrng.init(0x5eed1234 ^ @as(u64, sample_count)); var random = prng.random(); // Generate random sample data and accumulate totals var total: usize = 0; var writer = builder.writer(); for (0..sample_count) |i| { // Each sample represents a random KiB allocation between 8-64 const chunk = random.intRangeAtMost(u32, 8, 64); total += chunk; try writer.print("{d}: +{d} KiB\n", .{ i, chunk }); } // Write summary line with aggregated statistics try writer.print("total: {d} KiB across {d} samples\n", .{ total, sample_count }); // Capture allocation statistics before transferring ownership const stats = builder.snapshot(); // Transfer ownership of the built string to the caller const text = try builder.toOwnedSlice(); return .{ .text = text, .stats = stats }; } pub fn main() !void { // Arena allocator will reclaim all allocations at once when deinit() is called var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); defer arena.deinit(); // Small report: 256-byte stack buffer should be sufficient // stackFallback tries stack first, falls back to arena if needed var fallback_small = std.heap.stackFallback(256, arena.allocator()); const small_allocator = fallback_small.get(); const small = try buildReport(small_allocator, "stack-only", 6); defer small_allocator.free(small.text); // Large report: 256-byte stack buffer will overflow, forcing arena allocation // Demonstrates fallback behavior when stack space is insufficient var fallback_large = std.heap.stackFallback(256, arena.allocator()); const large_allocator = fallback_large.get(); const large = try buildReport(large_allocator, "needs-arena", 48); defer large_allocator.free(large.text); // Display both reports with their allocation statistics // Stats will reveal which allocator strategy was used (stack vs heap) std.debug.print("small buffer ->\n{s}stats: {any}\n\n", .{ small.text, small.stats }); std.debug.print("large buffer ->\n{s}stats: {any}\n", .{ large.text, large.stats }); } ``` Run: ```shell $ zig run allocator_fallback.zig ``` Output: ```shell small buffer -> label: stack-only 0: +40 KiB 1: +16 KiB 2: +13 KiB 3: +31 KiB 4: +44 KiB 5: +9 KiB total: 153 KiB across 6 samples stats: .{ .length = 115, .capacity = 128, .growth_events = 1 } large buffer -> label: needs-arena 0: +35 KiB 1: +29 KiB 2: +33 KiB 3: +14 KiB 4: +33 KiB 5: +20 KiB 6: +36 KiB 7: +21 KiB 8: +11 KiB 9: +58 KiB 10: +22 KiB 11: +53 KiB 12: +21 KiB 13: +41 KiB 14: +30 KiB 15: +20 KiB 16: +10 KiB 17: +39 KiB 18: +46 KiB 19: +59 KiB 20: +33 KiB 21: +8 KiB 22: +30 KiB 23: +22 KiB 24: +28 KiB 25: +32 KiB 26: +48 KiB 27: +50 KiB 28: +61 KiB 29: +53 KiB 30: +30 KiB 31: +27 KiB 32: +42 KiB 33: +24 KiB 34: +32 KiB 35: +58 KiB 36: +60 KiB 37: +27 KiB 38: +40 KiB 39: +17 KiB 40: +50 KiB 41: +50 KiB 42: +42 KiB 43: +54 KiB 44: +61 KiB 45: +10 KiB 46: +25 KiB 47: +50 KiB total: 1695 KiB across 48 samples stats: .{ .length = 618, .capacity = 1040, .growth_events = 4 } ``` NOTE: `stackFallback(N, allocator)` only tolerates one call to `.get()` per instance; spin up a fresh fallback wrapper when you need multiple concurrent builders. ## Section: Growth Planning [section_id: growth-planning] [section_url: https://zigbook.net/chapters/11__project-dynamic-string-builder#growth-planning] The builder records how many times capacity changed, which is perfect for profiling the difference between “append blindly” and “pre-size once.” The next example shows both paths producing identical text while the planned version keeps growth to a single reallocation. ### Subsection: Pre-sizing vs naive append [section_id: growth-planning-compare] [section_url: https://zigbook.net/chapters/11__project-dynamic-string-builder#growth-planning-compare] ```zig const std = @import("std"); const builder_mod = @import("string_builder.zig"); const StringBuilder = builder_mod.StringBuilder; const Stats = builder_mod.Stats; /// Container for built string and its allocation statistics const Result = struct { text: []u8, stats: Stats, }; /// Calculates the total byte length of all string segments /// Used to pre-compute capacity requirements for efficient allocation fn totalLength(parts: []const []const u8) usize { var sum: usize = 0; for (parts) |segment| sum += segment.len; return sum; } /// Builds a formatted string without pre-allocating capacity /// Demonstrates the cost of incremental growth through multiple reallocations /// Separators are spaces, with newlines every 8th segment fn buildNaive(allocator: std.mem.Allocator, parts: []const []const u8) !Result { // Initialize with default capacity (0 bytes) // Builder will grow dynamically as content is appended var builder = StringBuilder.init(allocator); defer builder.deinit(); for (parts, 0..) |segment, index| { // Each append may trigger reallocation if capacity is insufficient try builder.append(segment); if (index + 1 < parts.len) { // Insert newline every 8 segments, space otherwise const sep = if ((index + 1) % 8 == 0) "\n" else " "; try builder.append(sep); } } // Capture allocation statistics showing multiple growth operations const stats = builder.snapshot(); const text = try builder.toOwnedSlice(); return .{ .text = text, .stats = stats }; } /// Builds a formatted string with pre-calculated capacity /// Demonstrates performance optimization by eliminating reallocations /// Produces identical output to buildNaive but with fewer allocations fn buildPlanned(allocator: std.mem.Allocator, parts: []const []const u8) !Result { var builder = StringBuilder.init(allocator); defer builder.deinit(); // Calculate exact space needed: all segments plus separator count // Separators: n-1 for n parts (no separator after last segment) const separators = if (parts.len == 0) 0 else parts.len - 1; // Pre-allocate all required capacity in a single allocation try builder.ensureUnusedCapacity(totalLength(parts) + separators); for (parts, 0..) |segment, index| { // Append operations never reallocate due to pre-allocation try builder.append(segment); if (index + 1 < parts.len) { // Insert newline every 8 segments, space otherwise const sep = if ((index + 1) % 8 == 0) "\n" else " "; try builder.append(sep); } } // Capture statistics showing single allocation with no growth const stats = builder.snapshot(); const text = try builder.toOwnedSlice(); return .{ .text = text, .stats = stats }; } pub fn main() !void { // Initialize leak-detecting allocator to verify proper cleanup var gpa = std.heap.GeneralPurposeAllocator(.{}){}; defer { if (gpa.deinit() == .leak) std.log.err("leaked allocations detected", .{}); } const allocator = gpa.allocator(); // Sample data: 32 Greek letters and astronomy terms // Large enough to demonstrate multiple reallocations in naive approach const segments = [_][]const u8{ "alpha", "beta", "gamma", "delta", "epsilon", "zeta", "eta", "theta", "iota", "kappa", "lambda", "mu", "nu", "xi", "omicron", "pi", "rho", "sigma", "tau", "upsilon", "phi", "chi", "psi", "omega", "aurora", "borealis", "cosmos", "nebula", "quasar", "pulsar", "singularity", "zenith", }; // Build string without capacity planning // Stats will show multiple allocations and growth operations const naive = try buildNaive(allocator, &segments); defer allocator.free(naive.text); // Build string with exact capacity pre-allocation // Stats will show single allocation with no growth const planned = try buildPlanned(allocator, &segments); defer allocator.free(planned.text); // Compare allocation statistics side-by-side // Demonstrates the efficiency gain from capacity planning std.debug.print( "naive -> {any}\n{s}\n\nplanned -> {any}\n{s}\n", .{ naive.stats, naive.text, planned.stats, planned.text }, ); } ``` Run: ```shell $ zig run growth_comparison.zig ``` Output: ```shell naive -> .{ .length = 186, .capacity = 320, .growth_events = 2 } alpha beta gamma delta epsilon zeta eta theta iota kappa lambda mu nu xi omicron pi rho sigma tau upsilon phi chi psi omega aurora borealis cosmos nebula quasar pulsar singularity zenith planned -> .{ .length = 186, .capacity = 320, .growth_events = 1 } alpha beta gamma delta epsilon zeta eta theta iota kappa lambda mu nu xi omicron pi rho sigma tau upsilon phi chi psi omega aurora borealis cosmos nebula quasar pulsar singularity zenith ``` WARNING: Growth counts depend on allocator policy—switching to a fixed buffer or arena changes when capacity expands. Track both stats and chosen allocator when comparing profiles. ## Section: Notes & Caveats [section_id: notes-caveats] [section_url: https://zigbook.net/chapters/11__project-dynamic-string-builder#notes-caveats] - `toOwnedSlice` hands ownership to the caller; remember to free with the same allocator you passed into `StringBuilder`. - `stackFallback` zeros the scratch buffer every time you call `.get()`; if you need persistent reuse, hold on to the returned allocator instead of calling `.get()` repeatedly. - `reset()` clears contents but retains capacity, so prefer it for hot paths that rebuild strings in a tight loop. ## Section: Exercises [section_id: exercises] [section_url: https://zigbook.net/chapters/11__project-dynamic-string-builder#exercises] - Extend `StringBuilder` with an `appendFormat(comptime fmt, args)` helper powered by `std.io.Writer.Allocating`, then compare its allocations against repeated `writer.print` calls. - Build a CLI that streams JSON records into the builder, swapping between GPA and arena allocators via a command-line flag; see 05 (05__project-tempconv-cli.xml). - Emit a Markdown report to disk by piping the builder into `std.fs.File.writer()` and verifying the final slice matches the written bytes; see 06 (06__project-grep-lite.xml) and fs.zig (https://github.com/ziglang/zig/tree/master/lib/std/fs.zig). ## Section: Alternatives & Edge Cases [section_id: caveats-alternatives-edge-cases] [section_url: https://zigbook.net/chapters/11__project-dynamic-string-builder#caveats-alternatives-edge-cases] - Very large strings may allocate gigabytes—guard inputs or stream to disk once `length` crosses a safety threshold. - When composing multiple builders, share a single arena or GPA so ownership chains stay simple and leak detection remains accurate. - If latency matters more than allocations, emit straight to a buffered writer and use the builder only for sections that truly need random access edits; see 09 (09__project-hexdump.xml). # Chapter 12 — Config as Data [chapter_id: 12__config-as-data] [chapter_slug: config-as-data] [chapter_number: 12] [chapter_url: https://zigbook.net/chapters/12__config-as-data] ## Section: Overview [section_id: overview] [section_url: https://zigbook.net/chapters/12__config-as-data#overview] Configuration files eventually become ordinary data in memory. By giving that data a rich type—complete with defaults, enums, and optionals—you can reason about misconfigurations at compile time, validate invariants with determinism, and hand-tuned settings to downstream code without stringly-typed glue (see 11 (11__project-dynamic-string-builder.xml) and meta.zig (https://github.com/ziglang/zig/tree/master/lib/std/meta.zig)). This chapter establishes a playbook for struct-based configuration: start with default-heavy structs, overlay layered overrides such as environment or command-line flags, then enforce guardrails with explicit error sets so the eventual CLI in the next project can trust its inputs (see log.zig (https://github.com/ziglang/zig/tree/master/lib/std/log.zig)). ## Section: Learning Goals [section_id: learning-goals] [section_url: https://zigbook.net/chapters/12__config-as-data#learning-goals] - Model nested configuration structs with enums, optionals, and sensible defaults to capture application intent. - Layer profile, environment, and runtime overrides using reflection helpers such as `std.meta.fields` while keeping merges type-safe. - Validate configs with dedicated error sets, structured reporting, and inexpensive diagnostics so downstream systems can fail fast. 04 (04__errors-resource-cleanup.xml) ## Section: Structs as Configuration Contracts [section_id: config-structs] [section_url: https://zigbook.net/chapters/12__config-as-data#config-structs] Typed configuration mirrors the invariants you expect in production. Zig structs let you declare defaults inline, encode modes with enums, and group related knobs so callers cannot accidentally pass malformed tuples. Leaning on standard-library enums, log levels, and writers keeps the API ergonomic while honoring the I/O interface overhaul in v0.15.2 (https://ziglang.org/download/0.15.1/release-notes.html). ### Subsection: Default-rich struct definitions [section_id: config-structs-defaults] [section_url: https://zigbook.net/chapters/12__config-as-data#config-structs-defaults] The baseline configuration provides defaults for every field, including nested structs. Consumers can use designated initializers to selectively override values without losing the rest of the defaults. ```zig const std = @import("std"); /// Configuration structure for an application with sensible defaults const AppConfig = struct { /// Theme options for the application UI pub const Theme = enum { system, light, dark }; // Default configuration values are specified inline host: []const u8 = "127.0.0.1", port: u16 = 8080, log_level: std.log.Level = .info, instrumentation: bool = false, theme: Theme = .system, timeouts: Timeouts = .{}, /// Nested configuration for timeout settings pub const Timeouts = struct { connect_ms: u32 = 200, read_ms: u32 = 1200, }; }; /// Helper function to print configuration values in a human-readable format /// writer: any type implementing write() and print() methods /// label: descriptive text to identify this configuration dump /// config: the AppConfig instance to display fn dumpConfig(writer: anytype, label: []const u8, config: AppConfig) !void { // Print the label header try writer.print("{s}\n", .{label}); // Print each field with proper formatting try writer.print(" host = {s}\n", .{config.host}); try writer.print(" port = {}\n", .{config.port}); // Use @tagName to convert enum values to strings try writer.print(" log_level = {s}\n", .{@tagName(config.log_level)}); try writer.print(" instrumentation = {}\n", .{config.instrumentation}); try writer.print(" theme = {s}\n", .{@tagName(config.theme)}); // Print nested struct in single line try writer.print( " timeouts = .{{ connect_ms = {}, read_ms = {} }}\n", .{ config.timeouts.connect_ms, config.timeouts.read_ms }, ); } pub fn main() !void { // Allocate a fixed buffer for stdout operations var stdout_buffer: [2048]u8 = undefined; // Create a buffered writer for stdout to reduce syscalls var stdout_writer = std.fs.File.stdout().writer(&stdout_buffer); const stdout = &stdout_writer.interface; // Create a config using all default values (empty initializer) const defaults = AppConfig{}; try dumpConfig(stdout, "defaults ->", defaults); // Create a config with several overridden values // Fields not specified here retain their defaults from the struct definition const tuned = AppConfig{ .host = "0.0.0.0", // Bind to all interfaces .port = 9090, // Custom port .log_level = .debug, // More verbose logging .instrumentation = true, // Enable performance monitoring .theme = .dark, // Dark theme instead of system default .timeouts = .{ // Override nested timeout values .connect_ms = 75, // Faster connection timeout .read_ms = 1500, // Longer read timeout }, }; // Add blank line between the two config dumps try stdout.writeByte('\n'); // Display the customized configuration try dumpConfig(stdout, "overrides ->", tuned); // Flush the buffer to ensure all output is written to stdout try stdout.flush(); } ``` Run: ```shell $ zig run default_config.zig ``` Output: ```shell defaults -> host = 127.0.0.1 port = 8080 log_level = info instrumentation = false theme = system timeouts = .{ connect_ms = 200, read_ms = 1200 } overrides -> host = 0.0.0.0 port = 9090 log_level = debug instrumentation = true theme = dark timeouts = .{ connect_ms = 75, read_ms = 1500 } ``` ### Subsection: Optionals versus sentinel defaults [section_id: config-structs-optionals] [section_url: https://zigbook.net/chapters/12__config-as-data#config-structs-optionals] NOTE: Only fields that truly need tri-state semantics become optionals (`?[]const u8` for TLS file paths later in the chapter); everything else sticks to concrete defaults. Combining nested structs (here, `Timeouts`) with `[]const u8` strings supplies immutable references that remain valid for the lifetime of the configuration (see 03 (03__data-fundamentals.xml)). ### Subsection: Designated overrides stay readable [section_id: config-structs-initializers] [section_url: https://zigbook.net/chapters/12__config-as-data#config-structs-initializers] Since designated initializers allow you to override just the fields you care about, you can keep configuration declarations near call sites without sacrificing discoverability. Treat the struct literal as documentation: group related overrides together and lean on enums (like `Theme`) to keep magic strings out of your build. 02 (02__control-flow-essentials.xml), enums.zig (https://github.com/ziglang/zig/tree/master/lib/std/enums.zig) ### Subsection: Parsing Enum Values from Strings [section_id: parsing-enum-values] [section_url: https://zigbook.net/chapters/12__config-as-data#parsing-enum-values] When loading configuration from JSON, YAML, or environment variables, you’ll often need to convert strings to enum values. Zig’s `std.meta.stringToEnum` handles this with compile-time optimization based on enum size. ```text graph LR STRINGTOENUM["stringToEnum(T, str)"] subgraph "Small Enums" SMALL["fields.len <= 100"] MAP["StaticStringMap"] STRINGTOENUM --> SMALL SMALL --> MAP end subgraph "Large Enums" LARGE["fields.len > 100"] INLINE["inline for loop"] STRINGTOENUM --> LARGE LARGE --> INLINE end RESULT["?T"] MAP --> RESULT INLINE --> RESULT ``` For small enums (≤100 fields), `stringToEnum` builds a compile-time `StaticStringMap` for O(1) lookups. Larger enums use an inline loop to avoid compilation slowdowns from massive switch statements. The function returns `?T` (optional enum value), allowing you to handle invalid strings gracefully: ```zig const theme_str = "dark"; const theme = std.meta.stringToEnum(Theme, theme_str) orelse .system; ``` This pattern is essential for config loaders: parse the string, fall back to a sensible default if invalid. The optional return forces you to handle the error case explicitly, preventing silent failures from typos in config files (see meta.zig (https://github.com/ziglang/zig/blob/master/lib/std/meta.zig)). ## Section: Layering and Overrides [section_id: config-layering] [section_url: https://zigbook.net/chapters/12__config-as-data#config-layering] Real deployments pull configuration from multiple sources. By representing each layer as a struct of optionals, you can merge them deterministically: reflection bridges make it easy to iterate across fields without hand-writing boilerplate for every knob. 05 (05__project-tempconv-cli.xml) ### Subsection: Merging layered overrides [section_id: config-layering-example] [section_url: https://zigbook.net/chapters/12__config-as-data#config-layering-example] This program applies profile, environment, and command-line overrides where they exist, falling back to defaults otherwise. The merge order becomes explicit in `apply`, and the resulting struct stays fully typed. ```zig const std = @import("std"); /// Configuration structure for an application with sensible defaults const AppConfig = struct { /// Theme options for the application UI pub const Theme = enum { system, light, dark }; host: []const u8 = "127.0.0.1", port: u16 = 8080, log_level: std.log.Level = .info, instrumentation: bool = false, theme: Theme = .system, timeouts: Timeouts = .{}, /// Nested configuration for timeout settings pub const Timeouts = struct { connect_ms: u32 = 200, read_ms: u32 = 1200, }; }; /// Structure representing optional configuration overrides /// Each field is optional (nullable) to indicate whether it should override the base config const Overrides = struct { host: ?[]const u8 = null, port: ?u16 = null, log_level: ?std.log.Level = null, instrumentation: ?bool = null, theme: ?AppConfig.Theme = null, timeouts: ?AppConfig.Timeouts = null, }; /// Merges a single layer of overrides into a base configuration /// base: the starting configuration to modify /// overrides: optional values that should replace corresponding base fields /// Returns: a new AppConfig with overrides applied fn merge(base: AppConfig, overrides: Overrides) AppConfig { // Start with a copy of the base configuration var result = base; // Iterate over all fields in the Overrides struct at compile time inline for (std.meta.fields(Overrides)) |field| { // Check if this override field has a non-null value if (@field(overrides, field.name)) |value| { // If present, replace the corresponding field in result @field(result, field.name) = value; } } return result; } /// Applies a chain of override layers in sequence /// base: the initial configuration /// chain: slice of Overrides to apply in order (left to right) /// Returns: final configuration after all layers are merged fn apply(base: AppConfig, chain: []const Overrides) AppConfig { // Start with the base configuration var current = base; // Apply each override layer in sequence // Later layers override earlier ones for (chain) |layer| { current = merge(current, layer); } return current; } /// Helper function to print configuration values in a human-readable format /// writer: any type implementing write() and print() methods /// label: descriptive text to identify this configuration dump /// config: the AppConfig instance to display fn printSummary(writer: anytype, label: []const u8, config: AppConfig) !void { try writer.print("{s}:\n", .{label}); try writer.print(" host = {s}\n", .{config.host}); try writer.print(" port = {}\n", .{config.port}); try writer.print(" log = {s}\n", .{@tagName(config.log_level)}); try writer.print(" instrumentation = {}\n", .{config.instrumentation}); try writer.print(" theme = {s}\n", .{@tagName(config.theme)}); try writer.print(" timeouts = {any}\n", .{config.timeouts}); } pub fn main() !void { // Create base configuration with all default values const defaults = AppConfig{}; // Define a profile-level override layer (e.g., development profile) // This might come from a profile file or environment-specific settings const profile = Overrides{ .host = "0.0.0.0", .port = 9000, .log_level = .debug, .instrumentation = true, .theme = .dark, .timeouts = AppConfig.Timeouts{ .connect_ms = 100, .read_ms = 1500, }, }; // Define environment-level overrides (e.g., from environment variables) // These override profile settings const env = Overrides{ .host = "config.internal", .port = 9443, .log_level = .warn, .timeouts = AppConfig.Timeouts{ .connect_ms = 60, .read_ms = 1100, }, }; // Define command-line overrides (highest priority) // Only overrides specific fields, leaving others unchanged const command_line = Overrides{ .instrumentation = false, .theme = .light, }; // Apply all override layers in precedence order: // defaults -> profile -> env -> command_line // Later layers take precedence over earlier ones const final = apply(defaults, &[_]Overrides{ profile, env, command_line }); // Set up buffered stdout writer to reduce syscalls var stdout_buffer: [2048]u8 = undefined; var stdout_writer = std.fs.File.stdout().writer(&stdout_buffer); const stdout = &stdout_writer.interface; // Display progression of configuration through each layer try printSummary(stdout, "defaults", defaults); try printSummary(stdout, "profile", merge(defaults, profile)); try printSummary(stdout, "env", merge(defaults, env)); try printSummary(stdout, "command_line", merge(defaults, command_line)); // Add separator before showing final resolved config try stdout.writeByte('\n'); // Display the final merged configuration after all layers applied try printSummary(stdout, "resolved", final); // Ensure all buffered output is written try stdout.flush(); } ``` Run: ```shell $ zig run chapters-data/code/12__config-as-data/merge_overrides.zig ``` Output: ```shell defaults: host = 127.0.0.1 port = 8080 log = info instrumentation = false theme = system timeouts = .{ .connect_ms = 200, .read_ms = 1200 } profile: host = 0.0.0.0 port = 9000 log = debug instrumentation = true theme = dark timeouts = .{ .connect_ms = 100, .read_ms = 1500 } env: host = config.internal port = 9443 log = warn instrumentation = false theme = system timeouts = .{ .connect_ms = 60, .read_ms = 1100 } command_line: host = 127.0.0.1 port = 8080 log = info instrumentation = false theme = light timeouts = .{ .connect_ms = 200, .read_ms = 1200 } resolved: host = config.internal port = 9443 log = warn instrumentation = false theme = light timeouts = .{ .connect_ms = 60, .read_ms = 1100 } ``` See 10 (10__allocators-and-memory-management.xml) for allocator background relevant to layered configuration. ### Subsection: How Field Iteration Works Under the Hood [section_id: field-introspection] [section_url: https://zigbook.net/chapters/12__config-as-data#field-introspection] The `apply` function uses `std.meta.fields` to iterate over struct fields at compile time. Zig’s reflection API provides a rich set of introspection capabilities that make generic config merging possible without hand-written boilerplate for each field. ```text graph TB subgraph "Container Introspection" FIELDS["fields(T)"] FIELDINFO["fieldInfo(T, field)"] FIELDNAMES["fieldNames(T)"] TAGS["tags(T)"] FIELDENUM["FieldEnum(T)"] end subgraph "Declaration Introspection" DECLARATIONS["declarations(T)"] DECLINFO["declarationInfo(T, name)"] DECLENUM["DeclEnum(T)"] end subgraph "Applicable Types" STRUCT["struct"] UNION["union"] ENUMP["enum"] ERRORSET["error_set"] end STRUCT --> FIELDS UNION --> FIELDS ENUMP --> FIELDS ERRORSET --> FIELDS STRUCT --> DECLARATIONS UNION --> DECLARATIONS ENUMP --> DECLARATIONS FIELDS --> FIELDINFO FIELDS --> FIELDNAMES FIELDS --> FIELDENUM ENUMP --> TAGS ``` The introspection API provides: - `fields(T)`: Returns compile-time field information for any struct, union, enum, or error set - `fieldInfo(T, field)`: Gets detailed information for a specific field (name, type, default value, alignment) - `FieldEnum(T)`: Creates an enum with variants for each field name, useful for switch statements over fields - `declarations(T)`: Returns compile-time declaration info for functions and constants in a type When you see `inline for (std.meta.fields(Config))` in the merge logic, Zig unrolls this loop at compile time, generating specialized code for each field. This eliminates runtime overhead while maintaining type safety—the compiler verifies that all field types match between layers (see meta.zig (https://github.com/ziglang/zig/blob/master/lib/std/meta.zig)). ### Subsection: Making precedence explicit [section_id: config-layering-precedence] [section_url: https://zigbook.net/chapters/12__config-as-data#config-layering-precedence] Because `apply` copies the merged struct on each iteration, the order of the slice literal reads top-to-bottom precedence: later entries win. If you need lazy evaluation or short-circuit merging, swap `apply` for a version that stops once a field is set—just remember to keep defaults immutable so earlier layers cannot accidentally mutate shared state. 07 (07__project-safe-file-copier.xml) ### Subsection: Deep Structural Equality with std.meta.eql [section_id: deep-equality] [section_url: https://zigbook.net/chapters/12__config-as-data#deep-equality] For advanced config scenarios like detecting whether a reload is needed, `std.meta.eql(a, b)` performs deep structural comparison. This function handles nested structs, unions, error unions, and optionals recursively: ```text graph TB subgraph "Type Comparison" EQL["eql(a, b)"] STRUCT_EQL["Struct comparison"] UNION_EQL["Union comparison"] ERRORUNION_EQL["Error union comparison"] OPTIONAL_EQL["Optional comparison"] EQL --> STRUCT_EQL EQL --> UNION_EQL EQL --> ERRORUNION_EQL EQL --> OPTIONAL_EQL end ``` The `eql(a, b)` function performs deep structural equality comparison, handling nested structs, unions, and error unions recursively. This is useful for detecting "no-op" config updates: ```zig const old_config = loadedConfig; const new_config = parseConfigFile("app.conf"); if (std.meta.eql(old_config, new_config)) { // Skip reload, nothing changed return; } // Apply new config ``` The comparison works field-by-field for structs (including nested `Timeouts`), compares tags and payloads for unions, and handles error unions and optionals correctly (see meta.zig (https://github.com/ziglang/zig/blob/master/lib/std/meta.zig)). ## Section: Validation and Guardrails [section_id: config-validation] [section_url: https://zigbook.net/chapters/12__config-as-data#config-validation] Typed configs become trustworthy once you defend their invariants. Zig’s error sets turn validation failures into actionable diagnostics, and helper functions keep reporting consistent whether you’re logging or surfacing feedback to a CLI (see 04 (04__errors-resource-cleanup.xml) and debug.zig (https://github.com/ziglang/zig/tree/master/lib/std/debug.zig)). ### Subsection: Encoding invariants with error sets [section_id: config-validation-example] [section_url: https://zigbook.net/chapters/12__config-as-data#config-validation-example] This validator checks port ranges, TLS prerequisites, and timeout ordering. Each failure maps to a dedicated error tag so callers can react accordingly. ```zig const std = @import("std"); /// Environment mode for the application /// Determines security requirements and runtime behavior const Mode = enum { development, staging, production }; /// Main application configuration structure with nested settings const AppConfig = struct { host: []const u8 = "127.0.0.1", port: u16 = 8080, mode: Mode = .development, tls: Tls = .{}, timeouts: Timeouts = .{}, /// TLS/SSL configuration for secure connections pub const Tls = struct { enabled: bool = false, cert_path: ?[]const u8 = null, key_path: ?[]const u8 = null, }; /// Timeout settings for network operations pub const Timeouts = struct { connect_ms: u32 = 200, read_ms: u32 = 1200, }; }; /// Explicit error set for all configuration validation failures /// Each variant represents a specific invariant violation const ConfigError = error{ InvalidPort, InsecureProduction, MissingTlsMaterial, TimeoutOrdering, }; /// Validates configuration invariants and business rules /// config: the configuration to validate /// Returns: ConfigError if any validation rule is violated fn validate(config: AppConfig) ConfigError!void { // Port 0 is reserved and invalid for network binding if (config.port == 0) return error.InvalidPort; // Ports below 1024 require elevated privileges (except standard HTTPS) // Reject them to avoid privilege escalation requirements if (config.port < 1024 and config.port != 443) return error.InvalidPort; // Production environments must enforce TLS to protect data in transit if (config.mode == .production and !config.tls.enabled) { return error.InsecureProduction; } // When TLS is enabled, both certificate and private key must be provided if (config.tls.enabled) { if (config.tls.cert_path == null or config.tls.key_path == null) { return error.MissingTlsMaterial; } } // Read timeout must exceed connect timeout to allow data transfer // Otherwise connections would time out immediately after establishment if (config.timeouts.read_ms < config.timeouts.connect_ms) { return error.TimeoutOrdering; } } /// Reports validation result in human-readable format /// writer: output destination for the report /// label: descriptive name for this configuration test case /// config: the configuration to validate and report on fn report(writer: anytype, label: []const u8, config: AppConfig) !void { try writer.print("{s}: ", .{label}); // Attempt validation and catch any errors validate(config) catch |err| { // If validation fails, report the error name and return return try writer.print("error {s}\n", .{@errorName(err)}); }; // If validation succeeded, report success try writer.print("ok\n", .{}); } pub fn main() !void { // Test case 1: Valid production configuration // All security requirements met: TLS enabled with credentials const production = AppConfig{ .host = "example.com", .port = 8443, .mode = .production, .tls = .{ .enabled = true, .cert_path = "certs/app.pem", .key_path = "certs/app.key", }, .timeouts = .{ .connect_ms = 250, .read_ms = 1800, }, }; // Test case 2: Invalid - production mode without TLS // Should trigger InsecureProduction error const insecure = AppConfig{ .mode = .production, .tls = .{ .enabled = false }, }; // Test case 3: Invalid - read timeout less than connect timeout // Should trigger TimeoutOrdering error const misordered = AppConfig{ .timeouts = .{ .connect_ms = 700, .read_ms = 500, }, }; // Test case 4: Invalid - TLS enabled but missing certificate // Should trigger MissingTlsMaterial error const missing_tls_material = AppConfig{ .mode = .staging, .tls = .{ .enabled = true, .cert_path = null, .key_path = "certs/dev.key", }, }; // Set up buffered stdout writer to reduce syscalls var stdout_buffer: [1024]u8 = undefined; var stdout_writer = std.fs.File.stdout().writer(&stdout_buffer); const stdout = &stdout_writer.interface; // Run validation reports for all test cases // Each report will validate the config and print the result try report(stdout, "production", production); try report(stdout, "insecure", insecure); try report(stdout, "misordered", misordered); try report(stdout, "missing_tls_material", missing_tls_material); // Ensure all buffered output is written to stdout try stdout.flush(); } ``` Run: ```shell $ zig run chapters-data/code/12__config-as-data/validate_config.zig ``` Output: ```shell production: ok insecure: error InsecureProduction misordered: error TimeoutOrdering missing_tls_material: error MissingTlsMaterial ``` 04__errors-resource-cleanup.xml (04__errors-resource-cleanup.xml) ### Subsection: Reporting helpful diagnostics [section_id: config-validation-reporting] [section_url: https://zigbook.net/chapters/12__config-as-data#config-validation-reporting] Use `@errorName` (or structured enums for richer data) when printing validation errors so operators see the exact invariant that failed. Pair that with a shared reporting helper—like `report` in the example—to unify formatting across tests, logging, and CLI feedback (see 03 (03__data-fundamentals.xml) and Writer.zig (https://github.com/ziglang/zig/tree/master/lib/std/Io/Writer.zig)). ### Subsection: Error Message Formatting Standards [section_id: error-message-format] [section_url: https://zigbook.net/chapters/12__config-as-data#error-message-format] For production-grade diagnostics, follow the compiler’s error message format to provide consistent, parsable output. The standard format matches what users expect from Zig tooling: | Component | Format | Description | | --- | --- | --- | | Location | `:line:col:` | Line and column numbers (1-indexed) | | Severity | `error:` or `note:` | Message severity level | | Message | Text | The actual error or note message | Example error messages: ```text config.toml:12:8: error: port must be between 1024 and 65535 config.toml:15:1: error: TLS enabled but cert_file not specified config.toml:15:1: note: set cert_file and key_file when tls = true ``` The colon-separated format allows tools to parse error locations for IDE integration, and the severity levels (`error:` vs `note:`) help users distinguish between problems and helpful context. When validating configuration files, include the filename, line number (if available from your parser), and a clear description of the invariant violation. This consistency makes your config errors feel native to the Zig ecosystem. ### Subsection: Compile-time helpers for schema drift [section_id: config-validation-comptime] [section_url: https://zigbook.net/chapters/12__config-as-data#config-validation-comptime] For larger systems, consider wrapping your config struct in a comptime function that verifies field presence with `@hasField` or generates documentation from defaults. This keeps runtime code small while guaranteeing that evolving schemas stay in sync with generated config files (see 15 (15__comptime-and-reflection.xml)). ## Section: Notes & Caveats [section_id: notes-caveats] [section_url: https://zigbook.net/chapters/12__config-as-data#notes-caveats] - Keep immutable `[]const u8` slices for string settings so they can safely alias compile-time literals without extra copies (see mem.zig (https://github.com/ziglang/zig/tree/master/lib/std/mem.zig)). - Remember to flush buffered writers after emitting configuration diagnostics, especially when mixing stdout with process pipelines. - When layering overrides, clone mutable sub-structs (like allocator-backed lists) before mutation to avoid cross-layer aliasing. 10 (10__allocators-and-memory-management.xml) ## Section: Exercises [section_id: exercises] [section_url: https://zigbook.net/chapters/12__config-as-data#exercises] - Extend `AppConfig` with an optional telemetry endpoint (`?[]const u8`) and update the validator to ensure it is set whenever instrumentation is enabled. - Implement a `fromArgs` helper that parses key-value command-line pairs into an overrides struct, reusing the layering function to apply them. 05 (05__project-tempconv-cli.xml) - Generate a Markdown table summarizing defaults by iterating over `std.meta.fields(AppConfig)` at comptime and writing rows to a buffered writer. 11 (11__project-dynamic-string-builder.xml) ## Section: Alternatives & Edge Cases [section_id: caveats-alternatives-edge-cases] [section_url: https://zigbook.net/chapters/12__config-as-data#caveats-alternatives-edge-cases] - For massive configs, stream JSON/YAML data into arena-backed structs instead of building everything on the stack to avoid exhausting temporary buffers (see 10 (10__allocators-and-memory-management.xml)). - If you need dynamic keys, pair struct-based configs with `std.StringHashMap` lookups so you can keep typed defaults while still honoring user-provided extras (see hash_map.zig (https://github.com/ziglang/zig/tree/master/lib/std/hash_map.zig)). - Consider `std.io.Reader` pipelines when validating files uploaded over the network; this lets you short-circuit before materializing the entire config (see 28 (28__filesystem-and-io.xml)). # Chapter 13 — Testing & Leak Detection [chapter_id: 13__testing-and-leak-detection] [chapter_slug: testing-and-leak-detection] [chapter_number: 13] [chapter_url: https://zigbook.net/chapters/13__testing-and-leak-detection] ## Section: Overview [section_id: overview] [section_url: https://zigbook.net/chapters/13__testing-and-leak-detection#overview] Good tests are short, precise, and mean what they say. Zig’s `std.testing` makes this easy with small, composable assertions (`expect`, `expectEqual`, `expectError`) and a built-in testing allocator that detects leaks by default. Combined with allocation-failure injection, you can exercise error paths that would otherwise be hard to trigger, ensuring your code releases resources correctly and deterministically; see 10 (10__allocators-and-memory-management.xml) and testing.zig (https://github.com/ziglang/zig/tree/master/lib/std/testing.zig). This chapter shows how to write expressive tests, how to interpret the test runner’s leak diagnostics, and how to use `std.testing.checkAllAllocationFailures` to bulletproof code against `error.OutOfMemory` without writing hundreds of bespoke tests; see 11 (11__project-dynamic-string-builder.xml) and heap.zig (https://github.com/ziglang/zig/tree/master/lib/std/heap.zig). ## Section: Learning Goals [section_id: learning-goals] [section_url: https://zigbook.net/chapters/13__testing-and-leak-detection#learning-goals] - Write focused unit tests using `test` blocks and `std.testing` helpers. - Detect and fix memory leaks using `std.testing.allocator` and `defer` in tests; see 04 (04__errors-resource-cleanup.xml). - Use `std.testing.checkAllAllocationFailures` to systematically test OOM behavior; see 10 (10__allocators-and-memory-management.xml). ## Section: Testing basics with std.testing [section_id: testing-basics] [section_url: https://zigbook.net/chapters/13__testing-and-leak-detection#testing-basics] Zig’s test runner discovers `test` blocks in any file you pass to `zig test`. Assertions are ordinary functions that return errors, so they compose naturally with `try`/`catch`. ### Subsection: The std.testing Module Structure [section_id: testing-framework-structure] [section_url: https://zigbook.net/chapters/13__testing-and-leak-detection#testing-framework-structure] Before diving into specific assertions, it is helpful to see the complete toolkit available in `std.testing`. The module provides three categories of functionality: assertion functions, test allocators, and utilities. ```text graph TB subgraph "std.testing Module" MAIN["std.testing
(lib/std/testing.zig)"] subgraph "Assertion Functions" EXPECT["expect()"] EXPECT_EQ["expectEqual()"] EXPECT_ERR["expectError()"] EXPECT_SLICES["expectEqualSlices()"] EXPECT_STR["expectEqualStrings()"] EXPECT_FMT["expectFmt()"] end subgraph "Test Allocators" TEST_ALLOC["allocator
(GeneralPurposeAllocator)"] FAIL_ALLOC["failing_allocator
(FailingAllocator)"] end subgraph "Utilities" RAND_SEED["random_seed"] TMP_DIR["tmpDir()"] LOG_LEVEL["log_level"] end MAIN --> EXPECT MAIN --> EXPECT_EQ MAIN --> EXPECT_ERR MAIN --> EXPECT_SLICES MAIN --> EXPECT_STR MAIN --> EXPECT_FMT MAIN --> TEST_ALLOC MAIN --> FAIL_ALLOC MAIN --> RAND_SEED MAIN --> TMP_DIR MAIN --> LOG_LEVEL end ``` This chapter focuses on the core assertions (`expect`, `expectEqual`, `expectError`) and the test allocators for leak detection. Additional assertion functions like `expectEqualSlices` and `expectEqualStrings` provide specialized comparisons, while utilities like `tmpDir()` help test filesystem code; see testing.zig (https://github.com/ziglang/zig/blob/master/lib/std/testing.zig). ### Subsection: Expectations: booleans, equality, and errors [section_id: testing-basics-expect] [section_url: https://zigbook.net/chapters/13__testing-and-leak-detection#testing-basics-expect] This example covers boolean assertions, value equality, string equality, and expecting an error from a function under test. ```zig const std = @import("std"); /// Performs exact integer division, returning an error if the divisor is zero. /// This function demonstrates error handling in a testable way. fn divExact(a: i32, b: i32) !i32 { // Guard clause: check for division by zero before attempting division if (b == 0) return error.DivideByZero; // Safe to divide: use @divTrunc for truncating integer division return @divTrunc(a, b); } test "boolean and equality expectations" { // Test basic boolean expression using expect // expect() returns an error if the condition is false try std.testing.expect(2 + 2 == 4); // Test type-safe equality with expectEqual // Both arguments must be the same type; here we explicitly cast to u8 try std.testing.expectEqual(@as(u8, 42), @as(u8, 42)); } test "string equality (bytes)" { // Define expected string as a slice of const bytes const expected: []const u8 = "hello"; // Create actual string via compile-time concatenation // The ++ operator concatenates string literals at compile time const actual: []const u8 = "he" ++ "llo"; // Use expectEqualStrings for slice comparison // This compares the content of the slices, not just the pointer addresses try std.testing.expectEqualStrings(expected, actual); } test "expecting an error" { // Test that divExact returns the expected error when dividing by zero // expectError() succeeds if the function returns the specified error try std.testing.expectError(error.DivideByZero, divExact(1, 0)); // Test successful division path // We use 'try' to unwrap the success value, then expectEqual to verify it // If divExact returns an error here, the test will fail try std.testing.expectEqual(@as(i32, 3), try divExact(9, 3)); } ``` Run: ```shell $ zig test basic_tests.zig ``` Output: ```shell All 3 tests passed. ``` ## Section: Leak detection by construction [section_id: leak-detection] [section_url: https://zigbook.net/chapters/13__testing-and-leak-detection#leak-detection] The testing allocator (`std.testing.allocator`) is a `GeneralPurposeAllocator` configured to track allocations and report leaks when a test finishes. That means your tests fail if they forget to free; see 10 (10__allocators-and-memory-management.xml). ### Subsection: How Test Allocators Work [section_id: test-allocator-architecture] [section_url: https://zigbook.net/chapters/13__testing-and-leak-detection#test-allocator-architecture] The testing module provides two allocators: `allocator` for general testing with leak detection, and `failing_allocator` for simulating allocation failures. Understanding their architecture helps explain their different behaviors. ```text graph TB subgraph "Test Allocators in lib/std/testing.zig" ALLOC_INST["allocator_instance
GeneralPurposeAllocator"] ALLOC["allocator
Allocator interface"] BASE_INST["base_allocator_instance
FixedBufferAllocator"] FAIL_INST["failing_allocator_instance
FailingAllocator"] FAIL["failing_allocator
Allocator interface"] ALLOC_INST -->|"allocator()"| ALLOC BASE_INST -->|"provides base"| FAIL_INST FAIL_INST -->|"allocator()"| FAIL end subgraph "Usage in Tests" TEST["test block"] ALLOC_CALL["std.testing.allocator.alloc()"] FAIL_CALL["std.testing.failing_allocator.alloc()"] TEST --> ALLOC_CALL TEST --> FAIL_CALL end ALLOC --> ALLOC_CALL FAIL --> FAIL_CALL ``` The `testing.allocator` wraps a `GeneralPurposeAllocator` configured with stack traces and leak detection. The `failing_allocator` uses a `FixedBufferAllocator` as its base, then wraps it with failure injection logic. Both expose the standard `Allocator` interface, making them drop-in replacements for production allocators in tests; see testing.zig (https://github.com/ziglang/zig/blob/master/lib/std/testing.zig). ### Subsection: What a leak looks like [section_id: leak-detection-fail] [section_url: https://zigbook.net/chapters/13__testing-and-leak-detection#leak-detection-fail] The test below intentionally forgets to `free`. The runner reports a leaked address, a stack trace to the allocating callsite, and exits with a non-zero status. ```zig const std = @import("std"); // This test intentionally leaks to demonstrate the testing allocator's leak detection. // Do NOT copy this pattern into real code; see leak_demo_fix.zig for the fix. test "leak detection catches a missing free" { const allocator = std.testing.allocator; // Intentionally leak this allocation by not freeing it. const buf = try allocator.alloc(u8, 64); // Touch the memory so optimizers can't elide the allocation. for (buf) |*b| b.* = 0xAA; // No free on purpose: // allocator.free(buf); } ``` Run: ```shell $ zig test leak_demo_fail.zig ``` Output: ```shell [gpa] (err): memory address 0x… leaked: … leak_demo_fail.zig:1:36: … in test.leak detection catches a missing free (leak_demo_fail.zig) All 1 tests passed. 1 errors were logged. 1 tests leaked memory. error: the following test command failed with exit code 1: …/test --seed=0x… ``` IMPORTANT: The "All N tests passed." line only asserts test logic; the leak report still causes the overall run to fail. Fix the leak to make the suite green. 04 (04__errors-resource-cleanup.xml) ### Subsection: Fixing leaks with defer [section_id: leak-detection-fix] [section_url: https://zigbook.net/chapters/13__testing-and-leak-detection#leak-detection-fix] Use `defer allocator.free(buf)` immediately after a successful allocation to guarantee release along all paths. ```zig const std = @import("std"); test "no leak when freeing properly" { // Use the testing allocator, which tracks allocations and detects leaks const allocator = std.testing.allocator; // Allocate a 64-byte buffer on the heap const buf = try allocator.alloc(u8, 64); // Schedule deallocation to happen at scope exit (ensures cleanup) defer allocator.free(buf); // Fill the buffer with 0xAA pattern to demonstrate usage for (buf) |*b| b.* = 0xAA; // When the test exits, defer runs allocator.free(buf) // The testing allocator verifies all allocations were freed } ``` Run: ```shell $ zig test leak_demo_fix.zig ``` Output: ```shell All 1 tests passed. ``` 04 (04__errors-resource-cleanup.xml), mem.zig (https://github.com/ziglang/zig/tree/master/lib/std/mem.zig) ### Subsection: The Leak Detection Lifecycle [section_id: leak-detection-flow] [section_url: https://zigbook.net/chapters/13__testing-and-leak-detection#leak-detection-flow] Leak detection happens automatically at the end of each test. Understanding this timeline helps explain why `defer` must execute before the test completes and why leak reports appear even when test assertions pass. ```text graph TB TEST_START["Test Start"] ALLOC_MEM["Allocate Memory
const data = try testing.allocator.alloc(T, n);"] USE_MEM["Use Memory"] FREE_MEM["Free Memory
defer testing.allocator.free(data);"] TEST_END["Test End
Allocator checks for leaks"] TEST_START --> ALLOC_MEM ALLOC_MEM --> USE_MEM USE_MEM --> FREE_MEM FREE_MEM --> TEST_END LEAK_CHECK["If leaked: Test fails with
stack trace of allocation"] TEST_END -.->|"Memory not freed"| LEAK_CHECK ``` When a test ends, the `GeneralPurposeAllocator` verifies that all allocated memory has been freed. If any allocations remain, it prints the stack trace showing where the leaked memory was allocated (not where it should have been freed). This automatic checking eliminates entire categories of bugs without requiring manual tracking. The key is placing `defer allocator.free(…​)` immediately after successful allocation so it executes on all code paths, including early returns and error propagation; see heap.zig (https://github.com/ziglang/zig/blob/master/lib/std/heap.zig). ## Section: Allocation-failure injection [section_id: oom-injection] [section_url: https://zigbook.net/chapters/13__testing-and-leak-detection#oom-injection] Code that allocates memory must be correct even when allocations fail. `std.testing.checkAllAllocationFailures` reruns your function with a failing allocator at each allocation site, verifying you clean up partially-initialized state and propagate `error.OutOfMemory` properly; see 10 (10__allocators-and-memory-management.xml). ### Subsection: Systematically testing for OOM safety [section_id: oom-injection-good] [section_url: https://zigbook.net/chapters/13__testing-and-leak-detection#oom-injection-good] This example uses `checkAllAllocationFailures` with a small function that performs two allocations and frees both with `defer`. The helper simulates failure at each allocation point; the test passes only if no leaks occur and `error.OutOfMemory` is forwarded correctly. ```zig const std = @import("std"); fn testImplGood(allocator: std.mem.Allocator, length: usize) !void { const a = try allocator.alloc(u8, length); defer allocator.free(a); const b = try allocator.alloc(u8, length); defer allocator.free(b); } // No "bad" implementation here; see leak_demo_fail.zig for a dedicated failing example. test "OOM injection: good implementation is leak-free" { const allocator = std.testing.allocator; try std.testing.checkAllAllocationFailures(allocator, testImplGood, .{32}); } // Intentionally not included: a "bad" implementation under checkAllAllocationFailures // will cause the test runner to fail due to leak logging, even if you expect the error. // See leak_demo_fail.zig for a dedicated failing example. ``` Run: ```shell $ zig test oom_injection.zig ``` Output: ```shell All 1 tests passed. ``` NOTE: A deliberately "bad" implementation under `checkAllAllocationFailures` will cause the test runner to record leaked allocations and fail the overall run, even if you `expectError(error.MemoryLeakDetected, …)`. Keep failing examples isolated when teaching or debugging; see 10 (10__allocators-and-memory-management.xml). ## Section: Notes & Caveats [section_id: notes-caveats] [section_url: https://zigbook.net/chapters/13__testing-and-leak-detection#notes-caveats] - The testing allocator is only available when compiling tests. Attempting to use it in non-test code triggers a compile error. - Leak detection relies on deterministic deallocation. Prefer `defer` directly after allocation; avoid hidden control flow that skips frees; see 04 (04__errors-resource-cleanup.xml). - For integration tests that need lots of allocations, wrap with an arena allocator for speed, but still route ultimate backing through the testing allocator to preserve leak checks; see 10 (10__allocators-and-memory-management.xml). ## Section: Exercises [section_id: exercises] [section_url: https://zigbook.net/chapters/13__testing-and-leak-detection#exercises] - Write a function that builds a `std.ArrayList(u8)` from input bytes, then clears it. Use `checkAllAllocationFailures` to verify OOM safety; see 11 (11__project-dynamic-string-builder.xml). - Introduce a deliberate early return after the first allocation and watch the leak detector catch a missing `free`; then fix it with `defer`. - Add `expectError` tests for a function that returns an error on invalid input; include both the erroring and the successful path. ## Section: Alternatives & Edge Cases [section_id: caveats-alternatives-edge-cases] [section_url: https://zigbook.net/chapters/13__testing-and-leak-detection#caveats-alternatives-edge-cases] - If you need to run a suite that intentionally demonstrates leaks, keep those files separate from your passing tests to avoid failing CI runs. Alternatively, gate them behind a build flag and only opt in locally; see 20 (20__concept-primer-modules-vs-programs-vs-packages-vs-libraries.xml). - Outside of tests, you can enable `std.heap.GeneralPurposeAllocator` leak detection in debug builds to catch leaks in manual runs, but production builds should disable expensive checks for performance. - Allocation-failure injection is most effective on small, self-contained helpers. For higher-level workflows, test critical components in isolation to keep the induced failure space manageable; see 37 (36__style-and-best-practices.xml). # Chapter 14 — Project [chapter_id: 14__project-path-utility] [chapter_slug: project-path-utility] [chapter_number: 14] [chapter_url: https://zigbook.net/chapters/14__project-path-utility] ## Section: Overview [section_id: overview] [section_url: https://zigbook.net/chapters/14__project-path-utility#overview] In this hands-on chapter, we build a tiny, allocator-friendly path helper that plays nicely with Zig’s standard library and works across platforms. We’ll develop it test-first—then also provide a small CLI demo so you can see actual output without a test harness. Along the way, we deliberately introduce a leak and watch Zig’s testing allocator catch it, then fix it and verify. The goal is not to replace `std.fs.path`, but to practice API design, test-driven development (TDD), and leak-safe allocation in a realistic, bite-sized utility. See 13__testing-and-leak-detection.xml (13__testing-and-leak-detection.xml) and path.zig (https://github.com/ziglang/zig/tree/master/lib/std/fs/path.zig). ## Section: Learning Goals [section_id: learning-goals] [section_url: https://zigbook.net/chapters/14__project-path-utility#learning-goals] - Design a small, composable API: join, basename/dirpath, extension, and change extension. - Use allocators correctly and avoid leaks under success and failure paths. 10__allocators-and-memory-management.xml (10__allocators-and-memory-management.xml) - Practice TDD with `std.testing`, and pair TDD with a `zig run` demo for visible output. 13__testing-and-leak-detection.xml (13__testing-and-leak-detection.xml) ## Section: A small API surface [section_id: api-sketch] [section_url: https://zigbook.net/chapters/14__project-path-utility#api-sketch] We’ll implement four helpers in the `pathutil` namespace: - `joinAlloc(allocator, parts)` → `[]u8`: join components with a single separator, preserving an absolute root - `basename(path)` → `[]const u8`: last component, ignoring trailing separators - `dirpath(path)` → `[]const u8`: directory part, no trailing separators ("." for bare names, "/" for root) - `extname(path)` → `[]const u8` and `changeExtAlloc(allocator, path, new_ext)` → `[]u8` These functions emphasize predictable, teaching-friendly behavior; for production-grade edge cases, prefer `std.fs.path`. ```zig const std = @import("std"); /// Tiny, allocator-friendly path utilities for didactic purposes. /// Note: These do not attempt full platform semantics; they aim to be predictable /// and portable for teaching. Prefer std.fs.path for production code. pub const pathutil = struct { /// Join parts with exactly one separator between components. /// - Collapses duplicate separators at boundaries /// - Preserves a leading root (e.g. "/" on POSIX) if the first non-empty part starts with a separator /// - Does not resolve dot segments or drive letters pub fn joinAlloc(allocator: std.mem.Allocator, parts: []const []const u8) ![]u8 { var list: std.ArrayListUnmanaged(u8) = .{}; defer list.deinit(allocator); const sep: u8 = std.fs.path.sep; var has_any: bool = false; for (parts) |raw| { if (raw.len == 0) continue; // Trim leading/trailing separators from this component var start: usize = 0; var end: usize = raw.len; while (start < end and isSep(raw[start])) start += 1; while (end > start and isSep(raw[end - 1])) end -= 1; const had_leading_sep = start > 0; const core = raw[start..end]; if (!has_any) { if (had_leading_sep) { // Preserve absolute root try list.append(allocator, sep); has_any = true; } } else { // Ensure exactly one separator between components if we have content already if (list.items.len == 0 or list.items[list.items.len - 1] != sep) { try list.append(allocator, sep); } } if (core.len != 0) { try list.appendSlice(allocator, core); has_any = true; } } return list.toOwnedSlice(allocator); } /// Return the last path component. Trailing separators are ignored. /// Examples: "a/b/c" -> "c", "/a/b/" -> "b", "/" -> "/", "" -> "". pub fn basename(path: []const u8) []const u8 { if (path.len == 0) return path; // Skip trailing separators var end = path.len; while (end > 0 and isSep(path[end - 1])) end -= 1; if (end == 0) { // path was all separators; treat it as root return path[0..1]; } // Find previous separator var i: isize = @intCast(end); while (i > 0) : (i -= 1) { if (isSep(path[@intCast(i - 1)])) break; } const start: usize = @intCast(i); return path[start..end]; } /// Return the directory portion (without trailing separators). /// Examples: "a/b/c" -> "a/b", "a" -> ".", "/" -> "/". pub fn dirpath(path: []const u8) []const u8 { if (path.len == 0) return "."; // Skip trailing separators var end = path.len; while (end > 0 and isSep(path[end - 1])) end -= 1; if (end == 0) return path[0..1]; // all separators -> root // Find previous separator var i: isize = @intCast(end); while (i > 0) : (i -= 1) { const ch = path[@intCast(i - 1)]; if (isSep(ch)) break; } if (i == 0) return "."; // Skip any trailing separators in the dir portion var d_end: usize = @intCast(i); while (d_end > 1 and isSep(path[d_end - 1])) d_end -= 1; if (d_end == 0) return path[0..1]; return path[0..d_end]; } /// Return the extension (without dot) of the last component or "" if none. /// Examples: "file.txt" -> "txt", "a.tar.gz" -> "gz", ".gitignore" -> "". pub fn extname(path: []const u8) []const u8 { const base = basename(path); if (base.len == 0) return base; if (base[0] == '.') { // Hidden file as first character '.' does not count as extension if there is no other dot if (std.mem.indexOfScalar(u8, base[1..], '.')) |idx2| { const idx = 1 + idx2; if (idx + 1 < base.len) return base[(idx + 1)..]; return ""; } else return ""; } if (std.mem.lastIndexOfScalar(u8, base, '.')) |idx| { if (idx + 1 < base.len) return base[(idx + 1)..]; } return ""; } /// Return a newly-allocated path with the extension replaced by `new_ext` (no dot). /// If there is no existing extension, appends one if `new_ext` is non-empty. pub fn changeExtAlloc(allocator: std.mem.Allocator, path: []const u8, new_ext: []const u8) ![]u8 { const base = basename(path); const dir = dirpath(path); const sep: u8 = std.fs.path.sep; var base_core = base; if (std.mem.lastIndexOfScalar(u8, base, '.')) |idx| { if (!(idx == 0 and base[0] == '.')) { base_core = base[0..idx]; } } const need_dot = new_ext.len != 0; const dir_has = dir.len != 0 and !(dir.len == 1 and dir[0] == '.' and base.len == path.len); // Compute length at runtime to avoid comptime_int dependency var new_len: usize = 0; if (dir_has) new_len += dir.len + 1; new_len += base_core.len; if (need_dot) new_len += 1 + new_ext.len; var out = try allocator.alloc(u8, new_len); errdefer allocator.free(out); var w: usize = 0; if (dir_has) { @memcpy(out[w..][0..dir.len], dir); w += dir.len; out[w] = sep; w += 1; } @memcpy(out[w..][0..base_core.len], base_core); w += base_core.len; if (need_dot) { out[w] = '.'; w += 1; @memcpy(out[w..][0..new_ext.len], new_ext); w += new_ext.len; } return out; } }; inline fn isSep(ch: u8) bool { return ch == std.fs.path.sep or isOtherSep(ch); } inline fn isOtherSep(ch: u8) bool { // Be forgiving in parsing: treat both '/' and '\\' as separators on any platform // but only emit std.fs.path.sep when joining. return ch == '/' or ch == '\\'; } ``` NOTE: For pedagogy, we accept either `'/'` or `'\\'` as separators on any platform when parsing, but we always emit the local separator (`std.fs.path.sep`) when joining. ## Section: Try it: run the demo (visible output) [section_id: demo-run] [section_url: https://zigbook.net/chapters/14__project-path-utility#demo-run] To keep output visible outside the test runner, here’s a tiny CLI that calls our helpers and prints the results. ```zig const std = @import("std"); const pathutil = @import("path_util.zig").pathutil; pub fn main() !void { var out_buf: [2048]u8 = undefined; var out_writer = std.fs.File.stdout().writer(&out_buf); const out = &out_writer.interface; // Demonstrate join const j1 = try pathutil.joinAlloc(std.heap.page_allocator, &.{ "a", "b", "c" }); defer std.heap.page_allocator.free(j1); try out.print("join a,b,c => {s}\n", .{j1}); const j2 = try pathutil.joinAlloc(std.heap.page_allocator, &.{ "/", "usr/", "/bin" }); defer std.heap.page_allocator.free(j2); try out.print("join /,usr/,/bin => {s}\n", .{j2}); // Demonstrate basename/dirpath const p = "/home/user/docs/report.txt"; try out.print("basename({s}) => {s}\n", .{ p, pathutil.basename(p) }); try out.print("dirpath({s}) => {s}\n", .{ p, pathutil.dirpath(p) }); // Extension helpers try out.print("extname({s}) => {s}\n", .{ p, pathutil.extname(p) }); const changed = try pathutil.changeExtAlloc(std.heap.page_allocator, p, "md"); defer std.heap.page_allocator.free(changed); try out.print("changeExt({s}, md) => {s}\n", .{ p, changed }); try out.flush(); } ``` Run: ```shell $ zig run chapters-data/code/14__project-path-utility-tdd/path_util_demo.zig ``` Output: ```shell join a,b,c => a/b/c join /,usr/,/bin => /usr/bin basename(/home/user/docs/report.txt) => report.txt dirpath(/home/user/docs/report.txt) => /home/user/docs extname(/home/user/docs/report.txt) => txt changeExt(/home/user/docs/report.txt, md) => /home/user/docs/report.md ``` ## Section: Test-first: codify behavior and edge cases [section_id: tdd-loop] [section_url: https://zigbook.net/chapters/14__project-path-utility#tdd-loop] TDD helps clarify intent and lock down edge cases. We keep tests small and fast; they run with Zig’s testing allocator, which catches leaks by default. This chapter includes tests because the content plan calls for TDD; elsewhere we’ll favor `zig run`-style demos for visible output. See 13__testing-and-leak-detection.xml (13__testing-and-leak-detection.xml) and testing.zig (https://github.com/ziglang/zig/tree/master/lib/std/testing.zig). ```zig const std = @import("std"); const testing = std.testing; const pathutil = @import("path_util.zig").pathutil; fn ajoin(parts: []const []const u8) ![]u8 { return try pathutil.joinAlloc(testing.allocator, parts); } test "joinAlloc basic and absolute" { const p1 = try ajoin(&.{ "a", "b", "c" }); defer testing.allocator.free(p1); try testing.expectEqualStrings("a" ++ [1]u8{std.fs.path.sep} ++ "b" ++ [1]u8{std.fs.path.sep} ++ "c", p1); const p2 = try ajoin(&.{ "/", "usr/", "/bin" }); defer testing.allocator.free(p2); try testing.expectEqualStrings("/usr/bin", p2); const p3 = try ajoin(&.{ "", "a", "", "b" }); defer testing.allocator.free(p3); try testing.expectEqualStrings("a" ++ [1]u8{std.fs.path.sep} ++ "b", p3); const p4 = try ajoin(&.{ "a/", "/b/" }); defer testing.allocator.free(p4); try testing.expectEqualStrings("a" ++ [1]u8{std.fs.path.sep} ++ "b", p4); } test "basename and dirpath edges" { try testing.expectEqualStrings("c", pathutil.basename("a/b/c")); try testing.expectEqualStrings("b", pathutil.basename("/a/b/")); try testing.expectEqualStrings("/", pathutil.basename("////")); try testing.expectEqualStrings("", pathutil.basename("")); try testing.expectEqualStrings("a/b", pathutil.dirpath("a/b/c")); try testing.expectEqualStrings(".", pathutil.dirpath("a")); try testing.expectEqualStrings("/", pathutil.dirpath("////")); } test "extension and changeExtAlloc" { try testing.expectEqualStrings("txt", pathutil.extname("file.txt")); try testing.expectEqualStrings("gz", pathutil.extname("a.tar.gz")); try testing.expectEqualStrings("", pathutil.extname(".gitignore")); try testing.expectEqualStrings("", pathutil.extname("noext")); const changed1 = try pathutil.changeExtAlloc(testing.allocator, "a/b/file.txt", "md"); defer testing.allocator.free(changed1); try testing.expectEqualStrings("a/b/file.md", changed1); const changed2 = try pathutil.changeExtAlloc(testing.allocator, "a/b/file", "md"); defer testing.allocator.free(changed2); try testing.expectEqualStrings("a/b/file.md", changed2); const changed3 = try pathutil.changeExtAlloc(testing.allocator, "a/b/.profile", "txt"); defer testing.allocator.free(changed3); try testing.expectEqualStrings("a/b/.profile.txt", changed3); } ``` Run: ```shell $ zig test chapters-data/code/14__project-path-utility-tdd/path_util_test.zig ``` Output: ```shell All 3 tests passed. ``` ## Section: Catch a deliberate leak → fix it [section_id: leak-catch-fix] [section_url: https://zigbook.net/chapters/14__project-path-utility#leak-catch-fix] The testing allocator flags leaks at the end of a test. First, a failing example that forgets to `free`: ```zig const std = @import("std"); const testing = std.testing; const pathutil = @import("path_util.zig").pathutil; test "deliberate leak caught by testing allocator" { const joined = try pathutil.joinAlloc(testing.allocator, &.{ "/", "tmp", "demo" }); // Intentionally forget to free: allocator leak should be detected by the runner // defer testing.allocator.free(joined); try testing.expect(std.mem.endsWith(u8, joined, "demo")); } ``` Run (expect failure): ```shell $ zig test chapters-data/code/14__project-path-utility-tdd/leak_demo_fail.zig ``` Output (excerpt): ```text [gpa] (err): memory address 0x… leaked: … path_util.zig:49:33: … in joinAlloc … leak_demo_fail.zig:6:42: … in test.deliberate leak caught by testing allocator All 1 tests passed. 1 errors were logged. 1 tests leaked memory. error: the following test command failed with exit code 1: …/test --seed=0x… ``` Then fix it with `defer` and watch the suite go green: ```zig const std = @import("std"); const testing = std.testing; const pathutil = @import("path_util.zig").pathutil; test "fixed: no leak after adding defer free" { const joined = try pathutil.joinAlloc(testing.allocator, &.{ "/", "tmp", "demo" }); defer testing.allocator.free(joined); try testing.expect(std.mem.endsWith(u8, joined, "demo")); } ``` Run: ```shell $ zig test chapters-data/code/14__project-path-utility-tdd/leak_demo_fix.zig ``` Output: ```shell All 1 tests passed. ``` 10__allocators-and-memory-management.xml (10__allocators-and-memory-management.xml), heap.zig (https://github.com/ziglang/zig/tree/master/lib/std/heap.zig) ## Section: Notes & Caveats [section_id: notes-caveats] [section_url: https://zigbook.net/chapters/14__project-path-utility#notes-caveats] - For production path handling, consult `std.fs.path` for platform nuances (UNC paths, drive letters, special roots). - Prefer `defer allocator.free(buf)` immediately after successful allocations; it makes success and error paths correct by construction. 04__errors-resource-cleanup.xml (04__errors-resource-cleanup.xml) - When you need visible output (tutorials, demos), prefer `zig run` examples; when you need guarantees (CI), prefer `zig test`. This chapter demonstrates both because it’s explicitly TDD-focused. 13__testing-and-leak-detection.xml (13__testing-and-leak-detection.xml) ## Section: Exercises [section_id: exercises] [section_url: https://zigbook.net/chapters/14__project-path-utility#exercises] - Extend `joinAlloc` to elide `.` segments and collapse `..` pairs in the middle (be careful near the root). Add tests for edge cases, then demo with `zig run`. - Add `stem(path)` that returns the basename without extension; verify behavior for `.gitignore`, multi-dot names, and trailing dots. - Write a tiny CLI that takes `--change-ext md file1 file2 …` and prints the results, using the page allocator and a buffered writer. 28__filesystem-and-io.xml (28__filesystem-and-io.xml) ## Section: Alternatives & Edge Cases [section_id: caveats-alternatives-edge-cases] [section_url: https://zigbook.net/chapters/14__project-path-utility#caveats-alternatives-edge-cases] - On Windows, this teaching utility treats both `'/'` and `'\\'` as separators for input, but always prints the local separator. `std.fs.path` has richer semantics if you need exact Windows behavior. - Allocation failure handling: the demo uses `std.heap.page_allocator` and would abort on OOM; the tests use `std.testing.allocator` to systematically catch leaks. 10__allocators-and-memory-management.xml (10__allocators-and-memory-management.xml) - If you embed these helpers into larger tools, thread the allocator through your APIs and keep ownership rules explicit; avoid global state. 36__style-and-best-practices.xml (36__style-and-best-practices.xml) # Chapter 15 — Comptime & Reflection [chapter_id: 15__comptime-and-reflection] [chapter_slug: comptime-and-reflection] [chapter_number: 15] [chapter_url: https://zigbook.net/chapters/15__comptime-and-reflection] ## Section: Overview [section_id: overview] [section_url: https://zigbook.net/chapters/15__comptime-and-reflection#overview] Zig lets you execute plain Zig at compile time. That single, quiet idea unlocks a lot: generate lookup tables, specialize code based on types or values, validate invariants before the program is ever run, and write generic utilities without macros or a separate metaprogramming language. Reflection completes the picture: with `@TypeOf`, `@typeInfo`, and friends, code can inspect types and construct behavior adaptively. This chapter is a practitioner’s tour of compile-time execution and reflection in Zig 0.15.2. We’ll build small, self-contained examples you can run directly. Along the way, we’ll discuss what runs when (compile vs. run), how to keep code readable and fast, and when to prefer explicit parameters over clever reflection. For more detail, see meta.zig (https://github.com/ziglang/zig/tree/master/lib/std/meta.zig). ## Section: Learning Goals [section_id: learning-goals] [section_url: https://zigbook.net/chapters/15__comptime-and-reflection#learning-goals] - Use `comptime` expressions and blocks to compute data at build time and surface it at run time. - Introspect types using `@TypeOf`, `@typeInfo`, and `@typeName` to implement robust, generic helpers. - Apply `inline fn` and `inline for/while` judiciously, understanding code-size and performance trade-offs. 37 (37__illegal-behavior-and-safety-modes.xml) - Detect declarations and fields with `@hasDecl`, `@hasField`, and embed assets with `@embedFile`. 19 (19__modules-and-imports-root-builtin-discovery.xml) ## Section: Compile-time basics: data now, print later [section_id: comptime-basics] [section_url: https://zigbook.net/chapters/15__comptime-and-reflection#comptime-basics] Compile-time work is just ordinary Zig evaluated earlier. The example below: - Evaluates an expression at compile time. - Checks `@inComptime()` during runtime (it’s `false`). - Builds a small squares lookup table at compile time using an `inline while` and a comptime index. ```zig const std = @import("std"); fn stdout() *std.Io.Writer { // Buffered stdout writer per Zig 0.15.2 (Writergate) // We keep the buffer static so it survives for main's duration. const g = struct { var buf: [1024]u8 = undefined; var w = std.fs.File.stdout().writer(&buf); }; return &g.w.interface; } // Compute a tiny lookup table at compile time; print at runtime. fn squaresTable(comptime N: usize) [N]u64 { var out: [N]u64 = undefined; comptime var i: usize = 0; inline while (i < N) : (i += 1) { out[i] = @as(u64, i) * @as(u64, i); } return out; } pub fn main() !void { const out = stdout(); // Basic comptime evaluation const a = comptime 2 + 3; // evaluated at compile time try out.print("a (comptime 2+3) = {}\n", .{a}); // @inComptime reports whether we are currently executing at compile-time const during_runtime = @inComptime(); try out.print("@inComptime() during runtime: {}\n", .{during_runtime}); // Generate a squares table at compile time const table = squaresTable(8); try out.print("squares[0..8): ", .{}); var i: usize = 0; while (i < table.len) : (i += 1) { if (i != 0) try out.print(",", .{}); try out.print("{}", .{table[i]}); } try out.print("\n", .{}); try out.flush(); } ``` Run: ```shell $ zig run chapters-data/code/15__comptime-and-reflection/comptime_basics.zig ``` Output: ```shell a (comptime 2+3) = 5 @inComptime() during runtime: false squares[0..8): 0,1,4,9,16,25,36,49 ``` TIP: `inline while` requires the condition to be known at compile time. Use a `comptime var` index for unrolled loops. Prefer ordinary loops unless you have a measured reason to unroll. ### Subsection: How the compiler tracks comptime values [section_id: comptime-tracking] [section_url: https://zigbook.net/chapters/15__comptime-and-reflection#comptime-tracking] When you write comptime code, the compiler must determine which allocations and values are fully known at compile time. This tracking uses a mechanism in semantic analysis (Sema) that monitors all stores to allocated memory. ```text graph TB subgraph "Key Structures" COMPTIMEALLOC["ComptimeAlloc
val, is_const, alignment"] MAYBECOMPTIMEALLOC["MaybeComptimeAlloc
runtime_index, stores[]"] BASEALLOC["base_allocs map
derived ptr → base alloc"] end subgraph "Lifecycle" RUNTIMEALLOC["Runtime alloc instruction"] STORES["Store operations tracked"] MAKEPTRCONST["make_ptr_const instruction"] COMPTIMEVALUE["Determine comptime value"] end subgraph "MaybeComptimeAlloc Tracking" STORELIST["stores: MultiArrayList
inst, src"] RUNTIMEINDEXFIELD["runtime_index
Allocation point"] end subgraph "ComptimeAlloc Fields" VAL["val: MutableValue
Current value"] ISCONST["is_const: bool
Immutable after init"] ALIGNMENT["alignment
Pointer alignment"] RUNTIMEINDEXALLOC["runtime_index
Creation point"] end RUNTIMEALLOC --> MAYBECOMPTIMEALLOC MAYBECOMPTIMEALLOC --> STORELIST STORELIST --> STORES STORES --> MAKEPTRCONST MAKEPTRCONST --> COMPTIMEVALUE COMPTIMEVALUE --> COMPTIMEALLOC COMPTIMEALLOC --> VAL COMPTIMEALLOC --> ISCONST COMPTIMEALLOC --> ALIGNMENT COMPTIMEALLOC --> RUNTIMEINDEXALLOC BASEALLOC -.->|"tracks"| RUNTIMEALLOC ``` When the compiler encounters an allocation during semantic analysis, it creates a `MaybeComptimeAlloc` entry to track all stores. If any store depends on runtime values or conditions, the allocation cannot be known at comptime and the entry is discarded. If all stores are known at comptime when the pointer becomes const, the compiler applies all stores at compile time and creates a `ComptimeAlloc` with the final value. This mechanism enables the compiler to evaluate complex initialization patterns at compile time while ensuring correctness. For implementation details, see Sema.zig (https://github.com/ziglang/zig/blob/master/src/Sema.zig). ## Section: Reflection: , , and friends [section_id: reflection-typeinfo] [section_url: https://zigbook.net/chapters/15__comptime-and-reflection#reflection-typeinfo] Reflection lets you write “generic-but-precise” code. Here we examine a `struct` and print its fields and their types, then construct a value in the usual way. ```zig const std = @import("std"); fn stdout() *std.Io.Writer { const g = struct { var buf: [2048]u8 = undefined; var w = std.fs.File.stdout().writer(&buf); }; return &g.w.interface; } const Person = struct { id: u32, name: []const u8, active: bool = true, }; pub fn main() !void { const out = stdout(); // Reflect over Person using @TypeOf and @typeInfo const T = Person; try out.print("type name: {s}\n", .{@typeName(T)}); const info = @typeInfo(T); switch (info) { .@"struct" => |s| { try out.print("fields: {d}\n", .{s.fields.len}); inline for (s.fields, 0..) |f, idx| { try out.print(" {d}. {s}: {s}\n", .{ idx, f.name, @typeName(f.type) }); } }, else => try out.print("not a struct\n", .{}), } // Use reflection to initialize a default instance (here trivial) const p = Person{ .id = 42, .name = "Zig" }; try out.print("example: id={} name={s} active={}\n", .{ p.id, p.name, p.active }); try out.flush(); } ``` Run: ```shell $ zig run chapters-data/code/15__comptime-and-reflection/type_info_introspect.zig ``` Output: ```shell type name: type_info_introspect.Person fields: 3 0. id: u32 1. name: []const u8 2. active: bool example: id=42 name=Zig active=true ``` NOTE: Use `@typeInfo(T)` at compile time to derive implementations (formatters, serializers, adapters). Keep the result in a local `const` for readability. ### Subsection: Type decomposition with [section_id: type-decomposition] [section_url: https://zigbook.net/chapters/15__comptime-and-reflection#type-decomposition] Beyond `@typeInfo`, the `std.meta` module provides specialized functions for extracting component types from composite types. These utilities make generic code cleaner by avoiding manual `@typeInfo` inspection. ```text graph TB subgraph "Type Extractors" CHILD["Child(T)"] ELEM["Elem(T)"] SENTINEL["sentinel(T)"] TAG["Tag(T)"] ACTIVETAG["activeTag(union)"] end subgraph "Input Types" ARRAY["array"] VECTOR["vector"] POINTER["pointer"] OPTIONAL["optional"] UNION["union"] ENUM["enum"] end ARRAY --> CHILD VECTOR --> CHILD POINTER --> CHILD OPTIONAL --> CHILD ARRAY --> ELEM VECTOR --> ELEM POINTER --> ELEM ARRAY --> SENTINEL POINTER --> SENTINEL UNION --> TAG ENUM --> TAG UNION --> ACTIVETAG ``` Key type extraction functions: - `Child(T)`: Extracts the child type from arrays, vectors, pointers, and optionals—useful for generic functions operating on containers. - `Elem(T)`: Gets the element type from memory span types (arrays, slices, pointers)—cleaner than manual `@typeInfo` field access. - `sentinel(T)`: Returns the sentinel value, if present, enabling generic handling of null-terminated data. - `Tag(T)`: Gets the tag type from enums and unions for switch-based dispatch. - `activeTag(u)`: Returns the active tag of a union value at runtime. These functions compose well: `std.meta.Child(std.meta.Child(T))` extracts the element type from `[][]u8`. Use them to write generic algorithms that adapt to type structure without verbose `switch (@typeInfo(T))` blocks. meta.zig (https://github.com/ziglang/zig/blob/master/lib/std/meta.zig) ### Subsection: Field and declaration introspection [section_id: field-declaration-introspection] [section_url: https://zigbook.net/chapters/15__comptime-and-reflection#field-declaration-introspection] For structured access to container internals, `std.meta` provides higher-level alternatives to manual `@typeInfo` navigation: ```text graph TB subgraph "Container Introspection" FIELDS["fields(T)"] FIELDINFO["fieldInfo(T, field)"] FIELDNAMES["fieldNames(T)"] TAGS["tags(T)"] FIELDENUM["FieldEnum(T)"] end subgraph "Declaration Introspection" DECLARATIONS["declarations(T)"] DECLINFO["declarationInfo(T, name)"] DECLENUM["DeclEnum(T)"] end subgraph "Applicable Types" STRUCT["struct"] UNION["union"] ENUMP["enum"] ERRORSET["error_set"] end STRUCT --> FIELDS UNION --> FIELDS ENUMP --> FIELDS ERRORSET --> FIELDS STRUCT --> DECLARATIONS UNION --> DECLARATIONS ENUMP --> DECLARATIONS FIELDS --> FIELDINFO FIELDS --> FIELDNAMES FIELDS --> FIELDENUM ENUMP --> TAGS ``` The introspection API provides: - `fields(T)`: Returns compile-time field information for any struct, union, enum, or error set—iterate with `inline for` to process each field. - `fieldInfo(T, field)`: Gets detailed information (name, type, default value, alignment) for a specific field. - `FieldEnum(T)`: Creates an enum with variants for each field name, enabling switch-based field dispatch. - `declarations(T)`: Returns compile-time declaration info for functions and constants in a type—useful for finding optional interface methods. Example pattern: `inline for (std.meta.fields(MyStruct)) |field| { …​ }` lets you write generic serialization, formatting, or comparison functions without hand-coding field access. The `FieldEnum(T)` helper is particularly useful for switch statements over field names. meta.zig (https://github.com/ziglang/zig/blob/master/lib/std/meta.zig) ## Section: Inline functions and inline loops: power and cost [section_id: inline-and-unrolling] [section_url: https://zigbook.net/chapters/15__comptime-and-reflection#inline-and-unrolling] `inline fn` forces inlining, and `inline for` unrolls compile-time-known iterations. Both increase code size. Use them when you’ve profiled and determined a hot path benefits from unrolling or call-overhead elimination. ```zig const std = @import("std"); fn stdout() *std.Io.Writer { const g = struct { var buf: [1024]u8 = undefined; var w = std.fs.File.stdout().writer(&buf); }; return &g.w.interface; } // An inline function; the compiler is allowed to inline automatically too, // but `inline` forces it (use sparingly—can increase code size). inline fn mulAdd(a: u64, b: u64, c: u64) u64 { return a * b + c; } pub fn main() !void { const out = stdout(); // inline for: unroll a small loop at compile time var acc: u64 = 0; inline for (.{ 1, 2, 3, 4 }) |v| { acc = mulAdd(acc, 2, v); // (((0*2+1)*2+2)*2+3)*2+4 } try out.print("acc={}\n", .{acc}); // demonstrate that `inline` is not magic; it's a trade-off // prefer profiling for hot paths before forcing inline. try out.flush(); } ``` Run: ```shell $ zig run chapters-data/code/15__comptime-and-reflection/inline_for_inline_fn.zig ``` Output: ```shell acc=26 ``` CAUTION: Inline is not a performance cheat code. It trades instruction cache and binary size for potential speed. Measure before and after. 39 (39__performance-and-inlining.xml) ## Section: Capabilities: , , and [section_id: decl-field-embedfile] [section_url: https://zigbook.net/chapters/15__comptime-and-reflection#decl-field-embedfile] Compile-time capability tests let you adapt to types without overfitting APIs. Asset embedding keeps small resources close to the code with no runtime I/O. ```zig const std = @import("std"); fn stdout() *std.Io.Writer { const g = struct { var buf: [1024]u8 = undefined; var w = std.fs.File.stdout().writer(&buf); }; return &g.w.interface; } const WithStuff = struct { x: u32, pub const message: []const u8 = "compile-time constant"; pub fn greet() []const u8 { return "hello"; } }; pub fn main() !void { const out = stdout(); // Detect declarations and fields at comptime comptime { if (!@hasDecl(WithStuff, "greet")) { @compileError("missing greet decl"); } if (!@hasField(WithStuff, "x")) { @compileError("missing field x"); } } // @embedFile: include file contents in the binary at build time const embedded = @embedFile("hello.txt"); try out.print("has greet: {}\n", .{@hasDecl(WithStuff, "greet")}); try out.print("has field x: {}\n", .{@hasField(WithStuff, "x")}); try out.print("message: {s}\n", .{WithStuff.message}); try out.print("embedded:\n{s}", .{embedded}); try out.flush(); } ``` Run: ```shell $ zig run chapters-data/code/15__comptime-and-reflection/has_decl_field_embedfile.zig ``` Output: ```shell has greet: true has field x: true message: compile-time constant embedded: Hello from @embedFile! This text is compiled into the binary at build time. ``` TIP: Place assets next to the source that uses them and reference with a relative path in `@embedFile`. For larger assets or user-supplied data, prefer runtime I/O. 28 (28__filesystem-and-io.xml) ## Section: and explicit type parameters: pragmatic generics [section_id: anytype-generics] [section_url: https://zigbook.net/chapters/15__comptime-and-reflection#anytype-generics] Zig’s generics are just functions with `comptime` parameters. Use explicit type parameters for clarity; use `anytype` in leaf helpers that forward types. Reflection (`@TypeOf`, `@typeName`) helps with diagnostics when you accept flexible inputs. ```zig const std = @import("std"); fn stdout() *std.Io.Writer { const g = struct { var buf: [2048]u8 = undefined; var w = std.fs.File.stdout().writer(&buf); }; return &g.w.interface; } // A generic function that accepts any element type and sums a slice. // We use reflection to print type info at runtime. pub fn sum(comptime T: type, slice: []const T) T { var s: T = 0; var i: usize = 0; while (i < slice.len) : (i += 1) s += slice[i]; return s; } pub fn describeAny(x: anytype) void { const T = @TypeOf(x); const out = stdout(); out.print("value of type {s}: ", .{@typeName(T)}) catch {}; // best-effort print out.print("{any}\n", .{x}) catch {}; } pub fn main() !void { const out = stdout(); // Explicit type parameter const a = [_]u32{ 1, 2, 3, 4 }; const s1 = sum(u32, &a); try out.print("sum(u32,[1,2,3,4]) = {}\n", .{s1}); // Inferred by helper that forwards T const b = [_]u64{ 10, 20 }; const s2 = sum(u64, &b); try out.print("sum(u64,[10,20]) = {}\n", .{s2}); // anytype descriptor describeAny(@as(u8, 42)); describeAny("hello"); try out.flush(); } ``` Run: ```shell $ zig run chapters-data/code/15__comptime-and-reflection/anytype_and_generics.zig ``` Output: ```shell sum(u32,[1,2,3,4]) = 10 sum(u64,[10,20]) = 30 value of type u8: 42 value of type *const [5:0]u8: { 104, 101, 108, 108, 111 } ``` IMPORTANT: Prefer explicit `comptime T: type` parameters for public APIs; restrict `anytype` to helpers that transparently forward the concrete type and don’t constrain semantics. ## Section: Notes & Caveats [section_id: notes-caveats] [section_url: https://zigbook.net/chapters/15__comptime-and-reflection#notes-caveats] - Compile-time execution runs in the compiler; be mindful of complexity. Keep heavy work out of tight incremental loops to preserve fast rebuilds. 38 (38__zig-cli-deep-dive.xml) - Inline loops require compile-time-known bounds. When in doubt, use runtime loops and let the optimizer do its job. 39 (39__performance-and-inlining.xml) - Reflection is powerful but can obscure control flow. Prefer straightforward parameters for clarity, and reflect only where ergonomics justify it. 36 (36__style-and-best-practices.xml) ## Section: Exercises [section_id: exercises] [section_url: https://zigbook.net/chapters/15__comptime-and-reflection#exercises] - Write a `formatFields` helper that uses `@typeInfo` to print any struct’s field names and values. Try it with nested structs and slices. 47 (45__text-formatting-and-unicode.xml) - Build a compile-time computed `sin`/`cos` lookup table for integer angles and benchmark against `std.math` calls in a tight loop. Measure code size and runtime. 50 (50__random-and-math.xml) - Add a `hasToString` check: if a type `T` has a `format` method, print with `{f}`, otherwise print with `{any}`. Clarify behavior in a short doc comment. ## Section: Alternatives & Edge Cases [section_id: caveats-alternatives-edge-cases] [section_url: https://zigbook.net/chapters/15__comptime-and-reflection#caveats-alternatives-edge-cases] - `@inComptime()` is `true` in comptime contexts only; don’t rely on it for runtime behavior switches. Keep such switches in values/parameters. - `@embedFile` increases binary size; avoid embedding large resources. For configs/logos, it’s great. For datasets, stream from disk or network. 28 (28__filesystem-and-io.xml) - Avoid `inline fn` on large functions; it can balloon code. Use it on leaf arithmetic helpers or very small combinators where profiling shows wins. 39 (39__performance-and-inlining.xml) # Chapter 16 — Project [chapter_id: 16__project-table-generator] [chapter_slug: project-table-generator] [chapter_number: 16] [chapter_url: https://zigbook.net/chapters/16__project-table-generator] ## Section: Overview [section_id: overview] [section_url: https://zigbook.net/chapters/16__project-table-generator#overview] In this project, we turn the ideas from 15 (15__comptime-and-reflection.xml) into a practical workflow: generate small lookup tables at compile time and use them at runtime with zero overhead. This technique removes branches from hot loops, replaces repeated work with constant data, and keeps code simple. We’ll take a “measure-first” mindset and show when a table helps and when it’s not worth the binary size. We’ll implement three self-contained demos: - ASCII classification table: constant-time character categorization (digit/alpha/space/punct) - Popcount table: fast bit counting for bytes, composable for larger aggregates - Multiplication table: a parameterized N×N matrix rendered compactly Each example uses Zig’s modern stdout writer (see the Writergate changes) and prints visible results when run directly. See v0.15.2 (https://ziglang.org/download/0.15.1/release-notes.html) and ascii.zig (https://github.com/ziglang/zig/tree/master/lib/std/ascii.zig). ## Section: Learning Goals [section_id: learning-goals] [section_url: https://zigbook.net/chapters/16__project-table-generator#learning-goals] - Design compile-time table builders that are simple, readable, and fast. 15 (15__comptime-and-reflection.xml) - Weigh trade-offs: code size vs speed, flexibility vs “baked-in” constants. 41 (39__performance-and-inlining.xml) - Format and present tables cleanly using `std.Io.Writer` with minimal allocation. 47 (45__text-formatting-and-unicode.xml) ## Section: ASCII classification table [section_id: ascii-class-table] [section_url: https://zigbook.net/chapters/16__project-table-generator#ascii-class-table] We construct a 256-entry table mapping bytes to bitmasks for digit/alpha/space/punct. At runtime, we summarize an input string. The “punctuation” set is derived from `isPrint && !isAlphanumeric && !isWhitespace` (sufficient for ASCII). ```zig const std = @import("std"); /// Helper function to obtain a buffered standard output writer. /// Uses a static buffer to avoid repeated allocations. fn stdout() *std.Io.Writer { const g = struct { var buf: [4096]u8 = undefined; var w = std.fs.File.stdout().writer(&buf); }; return &g.w.interface; } /// Bit flags representing ASCII character classes. /// Multiple flags can be combined using bitwise OR. const Class = struct { pub const digit: u8 = 0x01; // 0-9 pub const alpha: u8 = 0x02; // A-Z, a-z pub const space: u8 = 0x04; // space, newline, tab, carriage return pub const punct: u8 = 0x08; // punctuation characters }; /// Builds a lookup table mapping each byte (0-255) to its character class flags. /// This function runs at compile time, producing a constant table embedded in the binary. fn buildAsciiClassTable() [256]u8 { // Initialize all entries to 0 (no class flags set) var t: [256]u8 = .{0} ** 256; // Iterate over all possible byte values at compile time comptime var b: usize = 0; inline while (b < 256) : (b += 1) { const ch: u8 = @intCast(b); var m: u8 = 0; // Accumulator for class flags // Check if character is a digit (0-9) if (ch >= '0' and ch <= '9') m |= Class.digit; // Check if character is alphabetic (A-Z or a-z) if ((ch >= 'A' and ch <= 'Z') or (ch >= 'a' and ch <= 'z')) m |= Class.alpha; // Check if character is whitespace (space, newline, tab, carriage return) if (ch == ' ' or ch == '\n' or ch == '\t' or ch == '\r') m |= Class.space; // Check if character is punctuation (printable, non-alphanumeric, non-whitespace) if (std.ascii.isPrint(ch) and !std.ascii.isAlphanumeric(ch) and !std.ascii.isWhitespace(ch)) m |= Class.punct; // Store the computed flags for this byte value t[b] = m; } return t; } /// Counts occurrences of each character class in the input string. /// Uses the precomputed lookup table for O(1) classification per character. fn countKinds(s: []const u8) struct { digits: usize, letters: usize, spaces: usize, punct: usize } { // Build the classification table (happens at compile time) const T = buildAsciiClassTable(); // Initialize counters for each character class var c = struct { digits: usize = 0, letters: usize = 0, spaces: usize = 0, punct: usize = 0 }{}; // Iterate through each byte in the input string var i: usize = 0; while (i < s.len) : (i += 1) { // Look up the class flags for the current byte const m = T[s[i]]; // Test each flag and increment the corresponding counter if ((m & Class.digit) != 0) c.digits += 1; if ((m & Class.alpha) != 0) c.letters += 1; if ((m & Class.space) != 0) c.spaces += 1; if ((m & Class.punct) != 0) c.punct += 1; } // Return the counts as an anonymous struct return .{ .digits = c.digits, .letters = c.letters, .spaces = c.spaces, .punct = c.punct }; } pub fn main() !void { // Get buffered output writer const out = stdout(); // Define test string containing various character classes const s = "Hello, Zig 0.15.2! \t\n"; // Count each character class in the test string const c = countKinds(s); // Print the input string try out.print("input: {s}\n", .{s}); // Print the computed counts for each character class try out.print("digits={} letters={} spaces={} punct={}\n", .{ c.digits, c.letters, c.spaces, c.punct }); // Ensure buffered output is written to stdout try out.flush(); } ``` Run: ```shell $ zig run ascii_class_table.zig ``` Output: ```shell input: Hello, Zig 0.15.2! digits=4 letters=8 spaces=6 punct=4 ``` TIP: Tables like this remove repeated branching in inner loops. Keep the derivation logic easy to audit, and prefer `std.ascii` helpers where possible. 15 (15__comptime-and-reflection.xml) ## Section: Popcount table for bytes [section_id: popcount-table] [section_url: https://zigbook.net/chapters/16__project-table-generator#popcount-table] Rather than call a bit-twiddling routine per byte, we bake a 256-entry popcount table and reduce across inputs. This scales from toy examples to “count set bits in a buffer” primitives. ```zig const std = @import("std"); /// Returns a reference to a buffered stdout writer. /// The buffer and writer are stored in a private struct to persist across calls. fn stdout() *std.Io.Writer { const g = struct { // Static buffer for stdout writes—survives function returns var buf: [4096]u8 = undefined; // Writer wraps stdout with the buffer; created once var w = std.fs.File.stdout().writer(&buf); }; // Return pointer to the writer's generic interface return &g.w.interface; } /// Counts the number of set bits (1s) in a single byte using bit manipulation. /// Uses a well-known parallel popcount algorithm that avoids branches. fn popcountByte(x: u8) u8 { var v = x; // Step 1: Count bits in pairs (2-bit groups) // Subtracts neighbor bit from each 2-bit group to get counts 0-2 v = v - ((v >> 1) & 0x55); // Step 2: Count bits in nibbles (4-bit groups) // Adds adjacent 2-bit counts to get nibble counts 0-4 v = (v & 0x33) + ((v >> 2) & 0x33); // Step 3: Combine nibbles and mask low 4 bits (result 0-8) // Adding the two nibbles gives total count, truncate to u8 return @truncate(((v + (v >> 4)) & 0x0F)); } /// Builds a 256-entry lookup table at compile time. /// Each entry [i] holds the number of set bits in byte value i. fn buildPopcountTable() [256]u8 { // Initialize table with zeros (all 256 entries) var t: [256]u8 = .{0} ** 256; // Compile-time loop index (required for inline while) comptime var i: usize = 0; // Unrolled loop: compute popcount for each possible byte value inline while (i < 256) : (i += 1) { // Store the bit count for byte value i t[i] = popcountByte(@intCast(i)); } // Return the fully populated table as a compile-time constant return t; } pub fn main() !void { // Acquire the buffered stdout writer const out = stdout(); // Generate the popcount lookup table at compile time const T = buildPopcountTable(); // Test data: array of bytes to analyze const bytes = [_]u8{ 0x00, 0x0F, 0xF0, 0xAA, 0xFF }; // Accumulator for total set bits across all test bytes var sum: usize = 0; // Sum up set bits by indexing into the precomputed table for (bytes) |b| sum += T[b]; // Print label for the output try out.print("bytes: ", .{}); // Print each byte in hex format with spacing for (bytes, 0..) |b, idx| { // Add space separator between bytes (not before first) if (idx != 0) try out.print(" ", .{}); // Format as 0x-prefixed 2-digit hex (e.g., 0x0F) try out.print("0x{X:0>2}", .{b}); } // Print the final sum of all set bits try out.print(" -> total set bits = {}\n", .{sum}); // Flush the buffered writer to ensure all output appears try out.flush(); } ``` Run: ```shell $ zig run popcount_table.zig ``` Output: ```shell bytes: 0x00 0x0F 0xF0 0xAA 0xFF -> total set bits = 20 ``` NOTE: In many workloads, the CPU’s POPCNT instruction (or `std.math.popCount`) is already fast. Prefer a table only when your profile shows that it helps for your data access pattern and platform. 52 (50__random-and-math.xml) ## Section: Parameterized multiplication table (N×N) [section_id: times-table] [section_url: https://zigbook.net/chapters/16__project-table-generator#times-table] Here the table dimension is a `comptime` parameter, so the compiler unrolls generation and stores a compact `[N][N]u16`. We format a 12×12 “times table” and only print a subset to keep output readable. ```zig const std = @import("std"); /// Returns a reference to a buffered stdout writer. /// The buffer and writer are stored in a private struct to persist across calls. fn stdout() *std.Io.Writer { const g = struct { // Static buffer for stdout writes—survives function returns var buf: [8192]u8 = undefined; // Writer wraps stdout with the buffer; created once var w = std.fs.File.stdout().writer(&buf); }; // Return pointer to the writer's generic interface return &g.w.interface; } /// Builds an N×N multiplication table at compile time. /// Each cell [i][j] holds (i+1) * (j+1) (1-indexed). fn buildMulTable(comptime N: usize) [N][N]u16 { // Declare the result table; will be computed entirely at compile time var t: [N][N]u16 = undefined; // Outer loop: row index (compile-time variable required for inline while) comptime var i: usize = 0; inline while (i < N) : (i += 1) { // Inner loop: column index comptime var j: usize = 0; inline while (j < N) : (j += 1) { // Store (row+1) * (col+1) in the table t[i][j] = @intCast((i + 1) * (j + 1)); } } // Return the fully populated table as a compile-time constant return t; } pub fn main() !void { // Acquire the buffered stdout writer const out = stdout(); // Table dimension (classic 12×12 times table) const N = 12; // Generate the multiplication table at compile time const T = buildMulTable(N); // Print header line try out.print("{s}x{s} multiplication table (partial):\n", .{ "12", "12" }); // Print only first 6 rows to keep output concise (runtime loop) var i: usize = 0; while (i < 6) : (i += 1) { // Print all 12 columns for this row var j: usize = 0; while (j < N) : (j += 1) { // Format each cell right-aligned in a 4-character field try out.print("{d: >4}", .{T[i][j]}); } // End the row with a newline try out.print("\n", .{}); } // Flush the buffered writer to ensure all output appears try out.flush(); } ``` Run: ```shell $ zig run mult_table.zig ``` Output: ```shell 12x12 multiplication table (partial): 1 2 3 4 5 6 7 8 9 10 11 12 2 4 6 8 10 12 14 16 18 20 22 24 3 6 9 12 15 18 21 24 27 30 33 36 4 8 12 16 20 24 28 32 36 40 44 48 5 10 15 20 25 30 35 40 45 50 55 60 6 12 18 24 30 36 42 48 54 60 66 72 ``` IMPORTANT: `inline while/for` constructs need compile-time-known bounds; pairing them with `comptime var` indices makes intent explicit. Choose ordinary loops unless you have a reason to unroll. 15 (15__comptime-and-reflection.xml) ## Section: Notes & Caveats [section_id: notes-caveats] [section_url: https://zigbook.net/chapters/16__project-table-generator#notes-caveats] - Binary size vs speed: tables cost memory. Use them when they remove meaningful work from a hot path and your binary budget allows it. 41 (39__performance-and-inlining.xml) - Portability: ASCII classification is straightforward; Unicode requires a different strategy (tables of ranges/pages or a library). 47 (45__text-formatting-and-unicode.xml) - I/O: The examples use the Zig 0.15.2 `std.Io.Writer` interface with a buffer in the interface—don’t forget to call `flush()`. ## Section: Exercises [section_id: exercises] [section_url: https://zigbook.net/chapters/16__project-table-generator#exercises] - Extend the ASCII table with additional classes (hex digits, control) and print a histogram for arbitrary input files. - Generate a `crc32` or `crc16` table at compile time and validate against a known test vector at runtime (as a small end-to-end demo). 15 (15__comptime-and-reflection.xml) - Parameterize the multiplication table’s cell formatter to align at different widths; measure the impact on readability and code size. 47 (45__text-formatting-and-unicode.xml) ## Section: Alternatives & Edge Cases [section_id: caveats-alternatives-edge-cases] [section_url: https://zigbook.net/chapters/16__project-table-generator#caveats-alternatives-edge-cases] - Table invalidation: if inputs change shape (e.g., switching from ASCII to UTF-8 code points), document assumptions prominently and introduce compile-time assertions to catch misuse early. 37 (36__style-and-best-practices.xml) - Micro-architectural effects: depending on cache behavior, a branchy routine can outperform a table walk; profile with realistic data. 42 (40__profiling-optimization-hardening.xml) - For tables much larger than CPU caches, consider on-demand generation, chunking, or precomputed assets loaded from disk rather than embedding in the binary. 28 (28__filesystem-and-io.xml) # Chapter 17 — Generic APIs & Type Erasure [chapter_id: 17__generic-apis-and-type-erasure] [chapter_slug: generic-apis-and-type-erasure] [chapter_number: 17] [chapter_url: https://zigbook.net/chapters/17__generic-apis-and-type-erasure] ## Section: Overview [section_id: overview] [section_url: https://zigbook.net/chapters/17__generic-apis-and-type-erasure#overview] Generics in Zig are nothing more than regular functions parameterized by `comptime` values, yet this simplicity hides a remarkable amount of expressive power. In this chapter, we turn the reflective techniques from 15 (15__comptime-and-reflection.xml) into disciplined API design patterns: structuring capability contracts, forwarding concrete types with `anytype`, and keeping the call sites ergonomic without sacrificing correctness. We also cover the opposite end of the spectrum—runtime type erasure—where opaque pointers and handwritten vtables let you store heterogeneous behavior in uniform containers. These techniques complement the lookup-table generation from 16 (16__project-table-generator.xml) and prepare us for the fully generic priority queue project that follows. For release notes, see v0.15.2 (https://ziglang.org/download/0.15.1/release-notes.html). ## Section: Learning Goals [section_id: learning-goals] [section_url: https://zigbook.net/chapters/17__generic-apis-and-type-erasure#learning-goals] - Build compile-time contracts that validate user-supplied types before code generation, delivering clear diagnostics. - Wrap arbitrary writers and strategies with `anytype`, preserving zero-cost abstractions while keeping call sites tidy. See Writer.zig (https://github.com/ziglang/zig/tree/master/lib/std/io/Writer.zig). - Apply `anyopaque` pointers and explicit vtables to erase types safely, aligning state and handling lifetimes without undefined behavior. ## Section: Comptime contracts as interfaces [section_id: comptime-contracts] [section_url: https://zigbook.net/chapters/17__generic-apis-and-type-erasure#comptime-contracts] A Zig function becomes generic the moment it accepts a `comptime` parameter. By pairing that flexibility with capability checks—`@hasDecl`, `@TypeOf`, or even custom predicates—you can encode rich structural interfaces without heavyweight trait systems. 15 (15__comptime-and-reflection.xml) We start by seeing how a metric aggregator contract pushes errors to compile time instead of relying on runtime assertions. ### Subsection: Validating structural requirements [section_id: contracts-validate] [section_url: https://zigbook.net/chapters/17__generic-apis-and-type-erasure#contracts-validate] `computeReport` below accepts an analyzer type that must expose `State`, `Summary`, `init`, `observe`, and `summarize`. The `validateAnalyzer` helper makes these requirements explicit; forgetting a method gives a precise `@compileError` instead of a mysterious instantiation failure. We demonstrate the pattern with a `RangeAnalyzer` and a `MeanVarianceAnalyzer`. ```zig const std = @import("std"); fn validateAnalyzer(comptime Analyzer: type) void { if (!@hasDecl(Analyzer, "State")) @compileError("Analyzer must define `pub const State`."); const state_alias = @field(Analyzer, "State"); if (@TypeOf(state_alias) != type) @compileError("Analyzer.State must be a type."); if (!@hasDecl(Analyzer, "Summary")) @compileError("Analyzer must define `pub const Summary`."); const summary_alias = @field(Analyzer, "Summary"); if (@TypeOf(summary_alias) != type) @compileError("Analyzer.Summary must be a type."); if (!@hasDecl(Analyzer, "init")) @compileError("Analyzer missing `pub fn init`."); if (!@hasDecl(Analyzer, "observe")) @compileError("Analyzer missing `pub fn observe`."); if (!@hasDecl(Analyzer, "summarize")) @compileError("Analyzer missing `pub fn summarize`."); } fn computeReport(comptime Analyzer: type, readings: []const f64) Analyzer.Summary { comptime validateAnalyzer(Analyzer); var state = Analyzer.init(readings.len); for (readings) |value| { Analyzer.observe(&state, value); } return Analyzer.summarize(state); } const RangeAnalyzer = struct { pub const State = struct { min: f64, max: f64, seen: usize, }; pub const Summary = struct { min: f64, max: f64, spread: f64, }; pub fn init(_: usize) State { return .{ .min = std.math.inf(f64), .max = -std.math.inf(f64), .seen = 0, }; } pub fn observe(state: *State, value: f64) void { state.seen += 1; state.min = @min(state.min, value); state.max = @max(state.max, value); } pub fn summarize(state: State) Summary { if (state.seen == 0) { return .{ .min = 0, .max = 0, .spread = 0 }; } return .{ .min = state.min, .max = state.max, .spread = state.max - state.min, }; } }; const MeanVarianceAnalyzer = struct { pub const State = struct { count: usize, sum: f64, sum_sq: f64, }; pub const Summary = struct { mean: f64, variance: f64, }; pub fn init(_: usize) State { return .{ .count = 0, .sum = 0, .sum_sq = 0 }; } pub fn observe(state: *State, value: f64) void { state.count += 1; state.sum += value; state.sum_sq += value * value; } pub fn summarize(state: State) Summary { if (state.count == 0) { return .{ .mean = 0, .variance = 0 }; } const n = @as(f64, @floatFromInt(state.count)); const mean = state.sum / n; const variance = @max(0.0, state.sum_sq / n - mean * mean); return .{ .mean = mean, .variance = variance }; } }; pub fn main() !void { const readings = [_]f64{ 21.0, 23.5, 22.1, 24.0, 22.9 }; const range = computeReport(RangeAnalyzer, readings[0..]); const stats = computeReport(MeanVarianceAnalyzer, readings[0..]); std.debug.print( "Range -> min={d:.2} max={d:.2} spread={d:.2}\n", .{ range.min, range.max, range.spread }, ); std.debug.print( "Mean/variance -> mean={d:.2} variance={d:.3}\n", .{ stats.mean, stats.variance }, ); } ``` Run: ```shell $ zig run chapters-data/code/17__generic-apis-and-type-erasure/comptime_contract.zig ``` Output: ```shell Range -> min=21.00 max=24.00 spread=3.00 Mean/variance -> mean=22.70 variance=1.124 ``` TIP: The contract remains zero-cost: once validated, the analyzer methods inline as if you had written specialized code, while still surfacing readable diagnostics for downstream users. ### Subsection: Diagnosing capability gaps [section_id: contracts-diagnostics] [section_url: https://zigbook.net/chapters/17__generic-apis-and-type-erasure#contracts-diagnostics] Because `validateAnalyzer` centralizes the checks, you can extend the interface over time—by requiring `pub const SummaryFmt = []const u8`, for instance—without touching every call site. When an adopter upgrades and misses a new declaration, the compiler reports exactly which requirement is absent. This “fail fast, fail specific” strategy scales especially well for internal frameworks and prevents silent drift between modules. 37 (36__style-and-best-practices.xml) ### Subsection: Trade-offs and batching considerations [section_id: contracts-tradeoffs] [section_url: https://zigbook.net/chapters/17__generic-apis-and-type-erasure#contracts-tradeoffs] Keep contract predicates cheap. Anything more than a handful of `@hasDecl` checks or straightforward type comparisons should be factored behind an opt-in feature flag or cached in a `comptime var`. Heavy analysis in a widely-instantiated helper quickly balloons compile times—profile with `zig build --verbose-cc` if a generic takes longer than expected. 40 (38__zig-cli-deep-dive.xml) ### Subsection: Under the hood: InternPool and generic instances [section_id: contracts-internpool] [section_url: https://zigbook.net/chapters/17__generic-apis-and-type-erasure#contracts-internpool] When `computeReport` is instantiated with a concrete analyzer, the compiler resolves all of the involved types and values through a shared `InternPool`. This structure guarantees that each unique analyzer `State`, `Summary`, and function type has a single canonical identity before code generation. ```text graph TB IP["InternPool"] subgraph "Threading" LOCALS["locals: []Local
(one per thread)"] SHARDS["shards: []Shard
(concurrent writes)"] TIDWIDTH["tid_width / tid_shift_*"] end subgraph "Core Storage" ITEMS["items: []Item"] EXTRADATA["extra_data: []u32"] STRINGS["string_bytes"] LIMBS["limbs: []Limb"] end subgraph "Dependency Tracking" SRCHASHDEPS["src_hash_deps"] NAVVALDEPS["nav_val_deps"] NAVTYDEPS["nav_ty_deps"] INTERNEDDEPS["interned_deps"] end subgraph "Symbol Tables" NAVS["navs: []Nav"] NAMESPACES["namespaces: []Namespace"] CAUS["caus: []Cau"] end subgraph "Special Indices" NONE["Index.none"] UNREACHABLE["Index.unreachable_value"] TYPEINFO["Index.type_info_type"] ANYERROR["Index.anyerror_type"] end IP --> LOCALS IP --> SHARDS IP --> TIDWIDTH IP --> ITEMS IP --> EXTRADATA IP --> STRINGS IP --> LIMBS IP --> SRCHASHDEPS IP --> NAVVALDEPS IP --> NAVTYDEPS IP --> INTERNEDDEPS IP --> NAVS IP --> NAMESPACES IP --> CAUS IP --> NONE IP --> UNREACHABLE IP --> TYPEINFO IP --> ANYERROR ``` Key properties: - Content-addressed storage: Each unique type/value is stored once, identified by an `Index`. - Thread-safe: `shards` allow concurrent writes via fine-grained locking. - Dependency tracking: Maps from source hashes, Navs, and interned values to dependent analysis units. - Special values: Pre-allocated indices for common types like `anyerror_type`, `type_info_type`, etc. ## Section: Forwarding with wrappers [section_id: anytype-forwarding] [section_url: https://zigbook.net/chapters/17__generic-apis-and-type-erasure#anytype-forwarding] Once you trust the capabilities of a concrete type, you often want to wrap or adapt it without reifying a trait object. `anytype` is the perfect tool: it copies the concrete type into the wrapper’s signature, preserving monomorphized performance while allowing you to build chains of decorators. 15 (15__comptime-and-reflection.xml) The next example shows a reusable “prefixed writer” that works equally well for fixed buffers and growable lists. ### Subsection: A reusable prefixed writer [section_id: anytype-wrapper] [section_url: https://zigbook.net/chapters/17__generic-apis-and-type-erasure#anytype-wrapper] We fabricate two sinks: a fixed-buffer stream from the reorganized `std.Io` namespace and a heap-backed `ArrayList` wrapper with its own `GenericWriter`. `withPrefix` captures their concrete writer types via `@TypeOf`, returning a struct whose `print` method prepends a label before forwarding to the inner writer. ```zig const std = @import("std"); fn PrefixedWriter(comptime Writer: type) type { return struct { inner: Writer, prefix: []const u8, pub fn print(self: *@This(), comptime fmt: []const u8, args: anytype) !void { try self.inner.print("[{s}] ", .{self.prefix}); try self.inner.print(fmt, args); } }; } fn withPrefix(writer: anytype, prefix: []const u8) PrefixedWriter(@TypeOf(writer)) { return .{ .inner = writer, .prefix = prefix, }; } const ListSink = struct { allocator: std.mem.Allocator, list: std.ArrayList(u8) = std.ArrayList(u8).empty, const Writer = std.io.GenericWriter(*ListSink, std.mem.Allocator.Error, writeFn); fn writeFn(self: *ListSink, chunk: []const u8) std.mem.Allocator.Error!usize { try self.list.appendSlice(self.allocator, chunk); return chunk.len; } pub fn writer(self: *ListSink) Writer { return .{ .context = self }; } pub fn print(self: *ListSink, comptime fmt: []const u8, args: anytype) !void { try self.writer().print(fmt, args); } pub fn deinit(self: *ListSink) void { self.list.deinit(self.allocator); } }; pub fn main() !void { var stream_storage: [256]u8 = undefined; var fixed_stream = std.Io.fixedBufferStream(&stream_storage); var pref_stream = withPrefix(fixed_stream.writer(), "stream"); try pref_stream.print("value = {d}\n", .{42}); try pref_stream.print("tuple = {any}\n", .{.{ 1, 2, 3 }}); var gpa = std.heap.GeneralPurposeAllocator(.{}){}; defer _ = gpa.deinit(); const allocator = gpa.allocator(); var sink = ListSink{ .allocator = allocator }; defer sink.deinit(); var pref_array = withPrefix(sink.writer(), "array"); try pref_array.print("flags = {any}\n", .{.{ true, false }}); try pref_array.print("label = {s}\n", .{"generic"}); std.debug.print("Fixed buffer stream captured:\n{s}", .{fixed_stream.getWritten()}); std.debug.print("ArrayList writer captured:\n{s}", .{sink.list.items}); } ``` Run: ```shell $ zig run chapters-data/code/17__generic-apis-and-type-erasure/prefixed_writer.zig ``` Output: ```shell Fixed buffer stream captured: [stream] value = 42 [stream] tuple = .{ 1, 2, 3 } ArrayList writer captured: [array] flags = .{ true, false } [array] label = generic ``` NOTE: `std.Io.fixedBufferStream` and `std.io.GenericWriter` were both polished in Zig 0.15.2 to emphasize explicit writer contexts, which is why we pass the allocator into `ListSink.writer()` each time. fixed_buffer_stream.zig (https://github.com/ziglang/zig/tree/master/lib/std/Io/fixed_buffer_stream.zig) ### Subsection: Guardrails for [section_id: anytype-guardrails] [section_url: https://zigbook.net/chapters/17__generic-apis-and-type-erasure#anytype-guardrails] Prefer `anytype` in helpers that merely forward calls; export public APIs with explicit `comptime T: type` parameters so that documentation and tooling stay honest. If a wrapper accepts `anytype` but inspects `@TypeInfo` deeply, document the expectation and consider moving the predicate into a reusable validator like we did with analyzers. That way a future refactor can upgrade the constraint without rewriting the wrapper. 37 (36__style-and-best-practices.xml) ### Subsection: helpers for structural contracts [section_id: anytype-meta] [section_url: https://zigbook.net/chapters/17__generic-apis-and-type-erasure#anytype-meta] When an `anytype` wrapper needs to understand the shape of the value it is forwarding, `std.meta` offers small, composable "view" functions. They are used pervasively in the standard library to implement generic helpers that adapt to arrays, slices, optionals, and unions at compile time. ```text graph TB subgraph "Type Extractors" CHILD["Child(T)"] ELEM["Elem(T)"] SENTINEL["sentinel(T)"] TAG["Tag(T)"] ACTIVETAG["activeTag(union)"] end subgraph "Input Types" ARRAY["array"] VECTOR["vector"] POINTER["pointer"] OPTIONAL["optional"] UNION["union"] ENUM["enum"] end ARRAY --> CHILD VECTOR --> CHILD POINTER --> CHILD OPTIONAL --> CHILD ARRAY --> ELEM VECTOR --> ELEM POINTER --> ELEM ARRAY --> SENTINEL POINTER --> SENTINEL UNION --> TAG ENUM --> TAG UNION --> ACTIVETAG ``` Key type extraction functions: - `Child(T)`: Extracts the child type from arrays, vectors, pointers, and optionals (see meta.zig:83-91 (https://github.com/ziglang/zig/tree/master/lib/std/meta.zig#L83-91)). - `Elem(T)`: Gets the element type from memory span types (see meta.zig:102-118 (https://github.com/ziglang/zig/tree/master/lib/std/meta.zig#L102-118)). - `sentinel(T)`: Returns the sentinel value, if present (see meta.zig:134-150 (https://github.com/ziglang/zig/tree/master/lib/std/meta.zig#L134-150)). - `Tag(T)`: Gets the tag type from enums and unions (see meta.zig:628-634 (https://github.com/ziglang/zig/tree/master/lib/std/meta.zig#L628-634)). - `activeTag(u)`: Returns the active tag of a union value (see meta.zig:651-654 (https://github.com/ziglang/zig/tree/master/lib/std/meta.zig#L651-654)). ### Subsection: Inline costs and specialization [section_id: anytype-inline] [section_url: https://zigbook.net/chapters/17__generic-apis-and-type-erasure#anytype-inline] Each distinct concrete writer instantiates a fresh copy of the wrapper. Use this to your advantage—attach comptime-known prefixes, bake in field offsets, or gate an `inline for` that only triggers for tiny objects. If the wrapper might be applied to dozens of types, double-check code size with `zig build-exe -femit-bin=` to avoid bloating binaries. 41 (39__performance-and-inlining.xml) ## Section: Runtime type erasure with vtables [section_id: type-erasure] [section_url: https://zigbook.net/chapters/17__generic-apis-and-type-erasure#type-erasure] Sometimes you need to hold a heterogeneous set of strategies at runtime: logging backends, diagnostics passes, or data sinks discovered via configuration. Zig’s answer is explicit vtables containing function pointers plus `*anyopaque` state that you allocate yourself. The compiler stops enforcing structure, so it becomes your responsibility to maintain alignment, lifetime, and error propagation. ### Subsection: Typed state, erased handles [section_id: erasure-demo] [section_url: https://zigbook.net/chapters/17__generic-apis-and-type-erasure#erasure-demo] The registry below manages two text processors. Each factory allocates a strongly-typed state, casts it to `*anyopaque`, and stores it alongside a vtable of function pointers. Helper functions `statePtr` and `stateConstPtr` recover the original types with `@alignCast`, ensuring we never violate alignment requirements. ```zig const std = @import("std"); const VTable = struct { name: []const u8, process: *const fn (*anyopaque, []const u8) void, finish: *const fn (*anyopaque) anyerror!void, }; fn statePtr(comptime T: type, ptr: *anyopaque) *T { const aligned = @as(*align(@alignOf(T)) anyopaque, @alignCast(ptr)); return @as(*T, @ptrCast(aligned)); } fn stateConstPtr(comptime T: type, ptr: *anyopaque) *const T { const aligned = @as(*align(@alignOf(T)) anyopaque, @alignCast(ptr)); return @as(*const T, @ptrCast(aligned)); } const Processor = struct { state: *anyopaque, vtable: *const VTable, pub fn name(self: *const Processor) []const u8 { return self.vtable.name; } pub fn process(self: *Processor, text: []const u8) void { _ = @call(.auto, self.vtable.process, .{ self.state, text }); } pub fn finish(self: *Processor) !void { try @call(.auto, self.vtable.finish, .{self.state}); } }; const CharTallyState = struct { vowels: usize, digits: usize, }; fn charTallyProcess(state_ptr: *anyopaque, text: []const u8) void { const state = statePtr(CharTallyState, state_ptr); for (text) |byte| { if (std.ascii.isAlphabetic(byte)) { const lower = std.ascii.toLower(byte); switch (lower) { 'a', 'e', 'i', 'o', 'u' => state.vowels += 1, else => {}, } } if (std.ascii.isDigit(byte)) { state.digits += 1; } } } fn charTallyFinish(state_ptr: *anyopaque) !void { const state = stateConstPtr(CharTallyState, state_ptr); std.debug.print( "[{s}] vowels={d} digits={d}\n", .{ char_tally_vtable.name, state.vowels, state.digits }, ); } const char_tally_vtable = VTable{ .name = "char-tally", .process = &charTallyProcess, .finish = &charTallyFinish, }; fn makeCharTally(allocator: std.mem.Allocator) !Processor { const state = try allocator.create(CharTallyState); state.* = .{ .vowels = 0, .digits = 0 }; return .{ .state = state, .vtable = &char_tally_vtable }; } const WordStatsState = struct { total_chars: usize, sentences: usize, longest_word: usize, current_word: usize, }; fn wordStatsProcess(state_ptr: *anyopaque, text: []const u8) void { const state = statePtr(WordStatsState, state_ptr); for (text) |byte| { state.total_chars += 1; if (byte == '.' or byte == '!' or byte == '?') { state.sentences += 1; } if (std.ascii.isAlphanumeric(byte)) { state.current_word += 1; if (state.current_word > state.longest_word) { state.longest_word = state.current_word; } } else if (state.current_word != 0) { state.current_word = 0; } } } fn wordStatsFinish(state_ptr: *anyopaque) !void { const state = statePtr(WordStatsState, state_ptr); if (state.current_word > state.longest_word) { state.longest_word = state.current_word; } std.debug.print( "[{s}] chars={d} sentences={d} longest-word={d}\n", .{ word_stats_vtable.name, state.total_chars, state.sentences, state.longest_word }, ); } const word_stats_vtable = VTable{ .name = "word-stats", .process = &wordStatsProcess, .finish = &wordStatsFinish, }; fn makeWordStats(allocator: std.mem.Allocator) !Processor { const state = try allocator.create(WordStatsState); state.* = .{ .total_chars = 0, .sentences = 0, .longest_word = 0, .current_word = 0 }; return .{ .state = state, .vtable = &word_stats_vtable }; } pub fn main() !void { var gpa = std.heap.GeneralPurposeAllocator(.{}){}; defer _ = gpa.deinit(); var arena = std.heap.ArenaAllocator.init(gpa.allocator()); defer arena.deinit(); const allocator = arena.allocator(); var processors = [_]Processor{ try makeCharTally(allocator), try makeWordStats(allocator), }; const samples = [_][]const u8{ "Generic APIs feel like contracts.", "Type erasure lets us pass handles without templating everything.", }; for (samples) |line| { for (&processors) |*processor| { processor.process(line); } } for (&processors) |*processor| { try processor.finish(); } } ``` Run: ```shell $ zig run chapters-data/code/17__generic-apis-and-type-erasure/type_erasure_registry.zig ``` Output: ```shell [char-tally] vowels=30 digits=0 [word-stats] chars=97 sentences=2 longest-word=10 ``` IMPORTANT: Keep track of lifetimes—the arena allocator outlives the processors, so the erased pointers stay valid. Switching to a scoped allocator would require a matching `destroy` hook in the vtable to avoid dangling pointers. 10 (10__allocators-and-memory-management.xml), Allocator.zig (https://github.com/ziglang/zig/tree/master/lib/std/mem/Allocator.zig) ### Subsection: Standard allocator as a vtable case study [section_id: erasure-allocator-vtable] [section_url: https://zigbook.net/chapters/17__generic-apis-and-type-erasure#erasure-allocator-vtable] The standard library’s `std.mem.Allocator` is itself a type-erased interface: every allocator implementation provides a concrete state pointer plus a vtable of function pointers. This mirrors the registry pattern above but in a form that the entire ecosystem relies on. ```text graph TB ALLOC["Allocator"] PTR["ptr: *anyopaque"] VTABLE["vtable: *VTable"] ALLOC --> PTR ALLOC --> VTABLE subgraph "VTable Functions" ALLOCFN["alloc(*anyopaque, len, alignment, ret_addr)"] RESIZEFN["resize(*anyopaque, memory, alignment, new_len, ret_addr)"] REMAPFN["remap(*anyopaque, memory, alignment, new_len, ret_addr)"] FREEFN["free(*anyopaque, memory, alignment, ret_addr)"] end VTABLE --> ALLOCFN VTABLE --> RESIZEFN VTABLE --> REMAPFN VTABLE --> FREEFN subgraph "High-Level API" CREATE["create(T)"] DESTROY["destroy(ptr)"] ALLOCAPI["alloc(T, n)"] FREE["free(slice)"] REALLOC["realloc(slice, new_len)"] end ALLOC --> CREATE ALLOC --> DESTROY ALLOC --> ALLOCAPI ALLOC --> FREE ALLOC --> REALLOC ``` The `Allocator` type is defined in Allocator.zig:7-20 (https://github.com/ziglang/zig/tree/master/lib/std/mem/Allocator.zig#L7-20) as a type-erased interface with a pointer and vtable. The vtable contains four fundamental operations: - `alloc`: Returns a pointer to `len` bytes with the specified alignment, or null on failure (see Allocator.zig:29 (https://github.com/ziglang/zig/tree/master/lib/std/mem/Allocator.zig#L29)). - `resize`: Attempts to expand or shrink memory in place (see Allocator.zig:48 (https://github.com/ziglang/zig/tree/master/lib/std/mem/Allocator.zig#L48)). - `remap`: Attempts to expand or shrink memory, allowing relocation (see Allocator.zig:69 (https://github.com/ziglang/zig/tree/master/lib/std/mem/Allocator.zig#L69)). - `free`: Frees and invalidates a region of memory (see Allocator.zig:81 (https://github.com/ziglang/zig/tree/master/lib/std/mem/Allocator.zig#L81)). ### Subsection: Safety notes for [section_id: erasure-safety] [section_url: https://zigbook.net/chapters/17__generic-apis-and-type-erasure#erasure-safety] `anyopaque` has a declared alignment of one, so every downcast must assert the true alignment with `@alignCast`. Skipping that assertion is illegal behavior even if the pointer happens to be properly aligned at runtime. Consider stashing the allocator and a cleanup function inside the vtable when ownership spans multiple modules. ### Subsection: When to graduate to modules or packages [section_id: erasure-interop] [section_url: https://zigbook.net/chapters/17__generic-apis-and-type-erasure#erasure-interop] Manual vtables shine for small, closed sets of behaviors. As soon as the surface area grows, migrate to a module-level registry that exposes constructors returning typed handles. Consumers still receive erased pointers, but the module can enforce invariants and share helper code for alignment, cleanup, and panic diagnostics. 19 (19__modules-and-imports-root-builtin-discovery.xml) ## Section: Notes & Caveats [section_id: notes-caveats] [section_url: https://zigbook.net/chapters/17__generic-apis-and-type-erasure#notes-caveats] - Favor small, intention-revealing validator helpers—long `validateX` functions are ripe for extraction into reusable comptime utilities. 15 (15__comptime-and-reflection.xml) - `anytype` wrappers generate one instantiation per concrete type. Profile binary size when exposing them in widely-used libraries. 41 (39__performance-and-inlining.xml) - Type erasure pushes correctness to the programmer. Add assertions, logging, or debug toggles in development builds to prove that downcasts and lifetimes remain valid. 39 (37__illegal-behavior-and-safety-modes.xml) ## Section: Exercises [section_id: exercises] [section_url: https://zigbook.net/chapters/17__generic-apis-and-type-erasure#exercises] - Extend `validateAnalyzer` to require an optional `summarizeError` function and demonstrate custom error sets in a test. 13 (13__testing-and-leak-detection.xml) - Add a `flush` capability to `PrefixedWriter`, detecting at comptime whether the inner writer exposes the method and adapting accordingly. meta.zig (https://github.com/ziglang/zig/tree/master/lib/std/meta.zig) - Introduce a third processor that streams hashes into a `std.crypto.hash.sha2.Sha256` context, then prints the digest in hex when finished. 52 (50__random-and-math.xml), sha2.zig (https://github.com/ziglang/zig/tree/master/lib/std/crypto/sha2.zig) ## Section: Alternatives & Edge Cases [section_id: caveats-alternatives-edge-cases] [section_url: https://zigbook.net/chapters/17__generic-apis-and-type-erasure#caveats-alternatives-edge-cases] - If compile-time validation depends on user-supplied types from other packages, add smoke tests so regressions surface before integration builds. 22 (22__build-system-deep-dive.xml) - Prefer `union(enum)` with payloaded variants when only a handful of strategies exist; vtables pay off once you cross from “few” to “many.” 08 (08__user-types-structs-enums-unions.xml) - For plug-in systems loaded from shared objects, pair erased state with explicit ABI-safe trampolines to keep portability manageable. 33 (33__c-interop-import-export-abi.xml) # Chapter 18 — Project [chapter_id: 18__project-generic-priority-queue] [chapter_slug: project-generic-priority-queue] [chapter_number: 18] [chapter_url: https://zigbook.net/chapters/18__project-generic-priority-queue] ## Section: Overview [section_id: overview] [section_url: https://zigbook.net/chapters/18__project-generic-priority-queue#overview] Generic APIs let us describe capabilities at compile time; priority queues are where those capabilities meet the realities of time-sensitive scheduling. In this project, we wrap `std.PriorityQueue` with rich comparators and context-aware policies that can be tested and tuned without sacrificing zero-cost abstractions. See 17 (17__generic-apis-and-type-erasure.xml) and priority_queue.zig (https://github.com/ziglang/zig/tree/master/lib/std/priority_queue.zig). We’ll build three artefacts: a foundational dispatcher that encodes ordering rules in a comparator, a fairness simulator that reuses the same queue while changing policy context, and an analytics wrapper that tracks the top offenders in a stream. Along the way, we revisit allocator choices, weighing strategies for draining, retuning, and introspecting heaps. See 10 (10__allocators-and-memory-management.xml) and sort.zig (https://github.com/ziglang/zig/tree/master/lib/std/sort.zig). ## Section: Learning Goals [section_id: learning-goals] [section_url: https://zigbook.net/chapters/18__project-generic-priority-queue#learning-goals] - Translate business rules into compile-time comparator contracts that drive `std.PriorityQueue` ordering. - Model dynamic scheduling heuristics using the queue’s `Context` parameter while keeping memory churn predictable. 10 (10__allocators-and-memory-management.xml) - Derive streaming analytics (top-K, rolling statistics) from the same heap without copy-pasting logic or sacrificing stability. 47 (45__text-formatting-and-unicode.xml) ## Section: Architecting a reusable queue core [section_id: architect-core] [section_url: https://zigbook.net/chapters/18__project-generic-priority-queue#architect-core] The priority queue API accepts a value type, a user-defined context, and a comparator that returns `std.math.Order`. That one function decides which element is bubbled to the front, so we’ll treat it as a contract backed by tests. ### Subsection: Comparator design as API surface [section_id: core-comparator-contract] [section_url: https://zigbook.net/chapters/18__project-generic-priority-queue#core-comparator-contract] Our first example builds a simple build-and-release dispatcher. Urgency is the primary key; submission time breaks ties so that we avoid starving older tasks. The comparator is a pure function, invoked entirely at compile time when the queue type is instantiated, yet it is expressive enough to capture nuanced ordering logic. See math.zig (https://github.com/ziglang/zig/tree/master/lib/std/math.zig). ```zig /// Demo: Using std.PriorityQueue to dispatch tasks by priority. /// Lower urgency values mean higher priority; ties are broken by earlier submission time. /// This example prints the order in which tasks would be processed. /// /// Notes: /// - The comparator returns `.lt` when `a` should be dispatched before `b`. /// - We also order by `submitted_at_ms` to ensure deterministic order among equal urgencies. const std = @import("std"); const Order = std.math.Order; /// A single work item to schedule. const Task = struct { /// Display name for the task. name: []const u8, /// Priority indicator: lower value = more urgent. urgency: u8, /// Monotonic timestamp in milliseconds used to break ties (earlier wins). submitted_at_ms: u64, }; /// Comparator for the priority queue: /// - Primary key: urgency (lower is dispatched first) /// - Secondary key: submitted_at_ms (earlier is dispatched first) fn taskOrder(_: void, a: Task, b: Task) Order { // Compare by urgency first. if (a.urgency < b.urgency) return .lt; if (a.urgency > b.urgency) return .gt; // Tie-breaker: earlier submission is higher priority. return std.math.order(a.submitted_at_ms, b.submitted_at_ms); } /// Program entry: builds a priority queue and prints dispatch order. pub fn main() !void { // Use the General Purpose Allocator (GPA) for simplicity in examples. var gpa = std.heap.GeneralPurposeAllocator(.{}){}; defer _ = gpa.deinit(); const allocator = gpa.allocator(); // Instantiate a priority queue of Task: // - Context type is `void` (no extra state needed by the comparator) // - `taskOrder` defines the ordering. var queue = std.PriorityQueue(Task, void, taskOrder).init(allocator, {}); defer queue.deinit(); // Enqueue tasks with varying urgency and submission times. // Expectation (by our ordering): lower urgency processed first; // within same urgency, earlier submitted_at_ms processed first. try queue.add(.{ .name = "compile pointer.zig", .urgency = 0, .submitted_at_ms = 1 }); try queue.add(.{ .name = "run tests", .urgency = 1, .submitted_at_ms = 2 }); try queue.add(.{ .name = "deploy preview", .urgency = 2, .submitted_at_ms = 3 }); try queue.add(.{ .name = "prepare changelog", .urgency = 1, .submitted_at_ms = 4 }); std.debug.print("Dispatch order:\n", .{}); // Remove tasks in priority order until the queue is empty. // removeOrNull() yields the next Task or null when empty. while (queue.removeOrNull()) |task| { std.debug.print(" - {s} (urgency {d})\n", .{ task.name, task.urgency }); } } ``` Run: ```shell $ zig run task_queue_basics.zig ``` Output: ```shell Dispatch order: - compile pointer.zig (urgency 0) - run tests (urgency 1) - prepare changelog (urgency 1) - deploy preview (urgency 2) ``` TIP: Because the comparator returns `std.math.Order`, we can layer in secondary keys without changing the queue type; the heap simply obeys the contract you encode. ### Subsection: Growth and allocation strategy [section_id: core-growth-allocation] [section_url: https://zigbook.net/chapters/18__project-generic-priority-queue#core-growth-allocation] Every call to `add` may reallocate if the underlying slice needs more capacity. For hot paths, reserve with `ensureUnusedCapacity` or initialize from a pre-sized slice, then drain to amortize allocations. The queue’s `deinit` is cheap so long as you make allocator lifetimes explicit, mirroring the memory hygiene practices from our allocator deep dive. 10 (10__allocators-and-memory-management.xml) ## Section: Policy-driven reprioritization [section_id: policy-driven] [section_url: https://zigbook.net/chapters/18__project-generic-priority-queue#policy-driven] Next, we feed richer data into the same queue: service requests with SLAs, time-of-day context, and VIP hints. The queue itself is agnostic; all nuance lives in the policy structure and comparator. This design keeps the heap reusable even as we layer on fairness rules. 17 (17__generic-apis-and-type-erasure.xml) ### Subsection: Aging and VIP weighting [section_id: policy-aging] [section_url: https://zigbook.net/chapters/18__project-generic-priority-queue#policy-aging] The comparator computes a scalar “score” by measuring slack (time remaining until deadline), multiplying overdue requests to escalate them, and subtracting a VIP bonus. Because `Context` is just a struct, the policy is compiled into the queue and can be swapped by constructing a new instance with different weights. We forward-declare helper functions to keep the comparator readable and testable. ### Subsection: Simulating operating modes [section_id: policy-scenarios] [section_url: https://zigbook.net/chapters/18__project-generic-priority-queue#policy-scenarios] We run two scenarios: mid-shift triage and late escalation. The only difference is the policy struct we pass to `init`; everything else (tasks, queue type) stays the same. The printed order shows how overdue multiplication and VIP boosts change the pop sequence. ```zig const std = @import("std"); const Order = std.math.Order; /// Represents an incoming support request with SLA constraints. const Request = struct { ticket: []const u8, submitted_at_ms: u64, sla_ms: u32, work_estimate_ms: u32, vip: bool, }; /// Scheduling policy parameters that influence prioritization. const Policy = struct { now_ms: u64, // Current time reference for slack calculation vip_boost: i64, // Score reduction (boost) for VIP requests overdue_multiplier: i64, // Penalty multiplier for overdue requests }; /// Computes the time slack for a request: positive means time remaining, negative means overdue. /// Overdue requests are amplified by the policy's overdue_multiplier to increase urgency. fn slack(policy: Policy, request: Request) i64 { // Calculate absolute deadline from submission time + SLA window const deadline = request.submitted_at_ms + request.sla_ms; // Compute slack as deadline - now; use i128 to prevent overflow on subtraction const slack_signed = @as(i64, @intCast(@as(i128, deadline) - @as(i128, policy.now_ms))); if (slack_signed >= 0) { // Positive slack: request is still within SLA return slack_signed; } // Negative slack: request is overdue; amplify urgency by multiplying return slack_signed * policy.overdue_multiplier; } /// Computes a weighted score for prioritization. /// Lower scores = higher priority (processed first by min-heap). fn weightedScore(policy: Policy, request: Request) i64 { // Start with slack: negative (overdue) or positive (time remaining) var score = slack(policy, request); // Add work estimate: longer tasks get slightly lower priority (higher score) score += @as(i64, @intCast(request.work_estimate_ms)); // VIP boost: reduce score to increase priority if (request.vip) score -= policy.vip_boost; return score; } /// Comparison function for the priority queue. /// Returns Order.lt if 'a' should be processed before 'b' (lower score = higher priority). fn requestOrder(policy: Policy, a: Request, b: Request) Order { const score_a = weightedScore(policy, a); const score_b = weightedScore(policy, b); return std.math.order(score_a, score_b); } /// Simulates a scheduling scenario by inserting all tasks into a priority queue, /// then dequeuing and printing them in priority order. fn simulateScenario(allocator: std.mem.Allocator, policy: Policy, label: []const u8) !void { // Define a set of incoming requests with varying SLA constraints and characteristics const tasks = [_]Request{ .{ .ticket = "INC-482", .submitted_at_ms = 0, .sla_ms = 500, .work_estimate_ms = 120, .vip = false }, .{ .ticket = "INC-993", .submitted_at_ms = 120, .sla_ms = 400, .work_estimate_ms = 60, .vip = true }, .{ .ticket = "INC-511", .submitted_at_ms = 200, .sla_ms = 200, .work_estimate_ms = 45, .vip = false }, .{ .ticket = "INC-742", .submitted_at_ms = 340, .sla_ms = 120, .work_estimate_ms = 30, .vip = false }, }; // Initialize priority queue with the given policy as context for comparison var queue = std.PriorityQueue(Request, Policy, requestOrder).init(allocator, policy); defer queue.deinit(); // Add all tasks to the queue; they will be heap-ordered automatically try queue.addSlice(&tasks); // Print scenario header std.debug.print("{s} (now={d}ms)\n", .{ label, policy.now_ms }); // Dequeue and print requests in priority order (lowest score first) while (queue.removeOrNull()) |request| { // Recalculate score and deadline for display const score = weightedScore(policy, request); const deadline = request.submitted_at_ms + request.sla_ms; std.debug.print( " -> {s} score={d} deadline={d} vip={}\n", .{ request.ticket, score, deadline, request.vip }, ); } std.debug.print("\n", .{}); } pub fn main() !void { // Set up general-purpose allocator with leak detection var gpa = std.heap.GeneralPurposeAllocator(.{}){}; defer _ = gpa.deinit(); const allocator = gpa.allocator(); // Scenario 1: Mid-shift with moderate VIP boost and overdue penalty try simulateScenario( allocator, .{ .now_ms = 350, .vip_boost = 250, .overdue_multiplier = 2 }, "Mid-shift triage" ); // Scenario 2: Escalation window with reduced VIP boost but higher overdue penalty try simulateScenario( allocator, .{ .now_ms = 520, .vip_boost = 100, .overdue_multiplier = 4 }, "Escalation window" ); } ``` Run: ```shell $ zig run sla_fairness.zig ``` Output: ```shell Mid-shift triage (now=350ms) -> INC-993 score=-20 deadline=520 vip=true -> INC-511 score=95 deadline=400 vip=false -> INC-742 score=140 deadline=460 vip=false -> INC-482 score=270 deadline=500 vip=false Escalation window (now=520ms) -> INC-511 score=-435 deadline=400 vip=false -> INC-742 score=-210 deadline=460 vip=false -> INC-993 score=-40 deadline=520 vip=true -> INC-482 score=40 deadline=500 vip=false ``` IMPORTANT: Changing policy after enqueuing existing items requires rebuilding the heap—drain into a slice, mutate the policy, then reinsert or call `fromOwnedSlice` to re-heapify under the new comparator. 10 (10__allocators-and-memory-management.xml) ## Section: Analytics and top-K reporting [section_id: analytics-topk] [section_url: https://zigbook.net/chapters/18__project-generic-priority-queue#analytics-topk] Priority queues are also excellent rolling aggregates. By keeping the “worst” elements in the heap and trimming aggressively, we can maintain a top-K view of latency spikes with minimal overhead. Sorting the current heap snapshot lets us render results directly for dashboards or logs. 47 (45__text-formatting-and-unicode.xml) ### Subsection: A composable wrapper [section_id: analytics-wrapper] [section_url: https://zigbook.net/chapters/18__project-generic-priority-queue#analytics-wrapper] `TopK` wraps `std.PriorityQueue` and uses the comparator to form a min-heap of scores. Every insert calls `remove` when the heap exceeds the limit, ensuring we keep only the highest scorers. The `snapshotDescending` helper copies the heap into a scratch buffer and sorts it with `std.sort.heap`, leaving the queue ready for further inserts. 17 (17__generic-apis-and-type-erasure.xml) ```zig // Import the Zig standard library for allocator, sorting, debugging, etc. const std = @import("std"); const Order = std.math.Order; // A single latency measurement for an endpoint. // Fields: // - endpoint: UTF-8 byte slice identifying the endpoint. // - duration_ms: observed latency in milliseconds. // - payload_bytes: size of the request/response payload in bytes. const LatencySample = struct { endpoint: []const u8, duration_ms: u32, payload_bytes: u32, }; // Compute a score for a latency sample. // Higher scores represent more severe (worse) samples. The formula favors // larger durations and applies a small penalty for larger payloads to reduce // noisy high-latency large-payload samples. // // Returns an f64 so scores can be compared with fractional penalties. fn score(sample: LatencySample) f64 { // Convert integers to floating point explicitly to avoid implicit casts. // The penalty factor 0.005 was chosen empirically to be small. return @as(f64, @floatFromInt(sample.duration_ms)) - (@as(f64, @floatFromInt(sample.payload_bytes)) * 0.005); } // TopK is a compile-time generic producer that returns a fixed-capacity, // score-driven top-K tracker for items of type T. // // Parameters: // - T: the element type stored in the tracker. // - scoreFn: a compile-time function that maps T -> f64 used to rank elements. fn TopK(comptime T: type, comptime scoreFn: fn (T) f64) type { const Error = error{InvalidLimit}; // Comparator helpers used by the PriorityQueue and for sorting snapshots. const Comparators = struct { // Comparator used by the PriorityQueue. The first parameter is the // user-provided context (unused here), hence the underscore name. // Returns an Order (Less/Equal/Greater) based on the score function. fn heap(_: void, a: T, b: T) Order { return std.math.order(scoreFn(a), scoreFn(b)); } // Boolean comparator used by the heap sort to produce descending order. // Returns true when `a` should come before `b` (i.e., a has higher score). fn desc(_: void, a: T, b: T) bool { return scoreFn(a) > scoreFn(b); } }; return struct { // A priority queue specialized for T using our heap comparator. const Heap = std.PriorityQueue(T, void, Comparators.heap); const Self = @This(); heap: Heap, limit: usize, // Initialize a TopK tracker with the provided allocator and positive limit. // Returns Error.InvalidLimit when limit == 0. pub fn init(allocator: std.mem.Allocator, limit: usize) Error!Self { if (limit == 0) return Error.InvalidLimit; return .{ .heap = Heap.init(allocator, {}), .limit = limit }; } // Deinitialize the underlying heap and free its resources. pub fn deinit(self: *Self) void { self.heap.deinit(); } // Add a single value into the tracker. If adding causes the internal // count to exceed `limit`, the priority queue will evict the item it // considers lowest priority according to our comparator, keeping the // top-K scored items. pub fn add(self: *Self, value: T) !void { try self.heap.add(value); if (self.heap.count() > self.limit) { // Evict the lowest-priority element (as defined by Comparators.heap). _ = self.heap.remove(); } } // Add multiple values from a slice into the tracker. // This simply forwards each element to `add`. pub fn addSlice(self: *Self, values: []const T) !void { for (values) |value| try self.add(value); } // Produce a snapshot of the current tracked items in descending score order. // // The snapshot allocates a new array via `allocator` and copies the // internal heap's item storage into it. The result is then sorted // descending (highest score first) using Comparators.desc. // // Caller is responsible for freeing the returned slice. pub fn snapshotDescending(self: *Self, allocator: std.mem.Allocator) ![]T { const count = self.heap.count(); const out = try allocator.alloc(T, count); // Copy the underlying items buffer into the newly allocated array. // This creates an independent snapshot so we can sort without mutating the heap. @memcpy(out, self.heap.items[0..count]); // Sort in-place so the highest-scored items appear first. std.sort.heap(T, out, @as(void, {}), Comparators.desc); return out; } }; } // Example program demonstrating TopK usage with LatencySample. pub fn main() !void { // Create a general-purpose allocator for example allocations. var gpa = std.heap.GeneralPurposeAllocator(.{}){}; defer _ = gpa.deinit(); const allocator = gpa.allocator(); // Track the top 5 latency samples by computed score. var tracker = try TopK(LatencySample, score).init(allocator, 5); defer tracker.deinit(); // Example samples. These are small, stack-allocated literal records. const samples = [_]LatencySample{ .{ .endpoint = "/v1/users", .duration_ms = 122, .payload_bytes = 850 }, .{ .endpoint = "/v1/orders", .duration_ms = 210, .payload_bytes = 1200 }, .{ .endpoint = "/v1/users", .duration_ms = 188, .payload_bytes = 640 }, .{ .endpoint = "/v1/payments", .duration_ms = 305, .payload_bytes = 1500 }, .{ .endpoint = "/v1/orders", .duration_ms = 154, .payload_bytes = 700 }, .{ .endpoint = "/v1/ledger", .duration_ms = 420, .payload_bytes = 540 }, .{ .endpoint = "/v1/users", .duration_ms = 275, .payload_bytes = 980 }, .{ .endpoint = "/v1/health", .duration_ms = 34, .payload_bytes = 64 }, .{ .endpoint = "/v1/ledger", .duration_ms = 362, .payload_bytes = 480 }, }; // Bulk-add the sample slice into the tracker. try tracker.addSlice(&samples); // Capture the current top-K samples in descending order and print them. const worst = try tracker.snapshotDescending(allocator); defer allocator.free(worst); std.debug.print("Top latency offenders (descending by score):\n", .{}); for (worst, 0..) |sample, idx| { // Compute the score again for display purposes (identical to the ordering key). const computed_score = score(sample); std.debug.print( " {d:>2}. {s: <12} latency={d}ms payload={d}B score={d:.2}\n", .{ idx + 1, sample.endpoint, sample.duration_ms, sample.payload_bytes, computed_score }, ); } } ``` Run: ```shell $ zig run topk_latency.zig ``` Output: ```shell Top latency offenders (descending by score): 1. /v1/ledger latency=420ms payload=540B score=417.30 2. /v1/ledger latency=362ms payload=480B score=359.60 3. /v1/payments latency=305ms payload=1500B score=297.50 4. /v1/users latency=275ms payload=980B score=270.10 5. /v1/orders latency=210ms payload=1200B score=204.00 ``` NOTE: Snapshotting copies the heap so that future inserts remain cheap; reuse a scratch allocator or arena for high-volume telemetry jobs to avoid fragmenting long-lived heaps. 10 (10__allocators-and-memory-management.xml) ### Subsection: From queues to module boundaries [section_id: closing-loop] [section_url: https://zigbook.net/chapters/18__project-generic-priority-queue#closing-loop] We now have reusable queue wrappers that can live in their own module. The next chapter formalizes that step, showing how to surface the queue as a package-level module and expose policies through `@import` boundaries. 19 (19__modules-and-imports-root-builtin-discovery.xml) ## Section: Notes & Caveats [section_id: notes-caveats] [section_url: https://zigbook.net/chapters/18__project-generic-priority-queue#notes-caveats] - Define comparators in a dedicated helper so they can be unit-tested independently and reused across queue instances. 13 (13__testing-and-leak-detection.xml) - Policy structs are value types—change detection means rebuilding the heap or creating a new queue; otherwise, your ordering no longer matches the comparator’s assumptions. - Copying heap contents for reporting allocates memory; recycle buffers or use arenas when integrating with telemetry services to keep GC-less Zig code predictable. 10 (10__allocators-and-memory-management.xml) ## Section: Exercises [section_id: exercises] [section_url: https://zigbook.net/chapters/18__project-generic-priority-queue#exercises] - Extend the dispatcher to respect “batch size” hints by tallying cumulative runtime in the comparator; add a test that asserts fairness across mixed priorities. 13 (13__testing-and-leak-detection.xml) - Modify the SLA simulator to write audit entries using `std.log` and compare the output against expectations under multiple policies. log.zig (https://github.com/ziglang/zig/tree/master/lib/std/log.zig) - Teach the `TopK` wrapper to return both the snapshot and the aggregate average; consider how you would expose that through an asynchronous metrics hook. 47 (45__text-formatting-and-unicode.xml) ## Section: Alternatives & Edge Cases [section_id: caveats-alternatives-edge-cases] [section_url: https://zigbook.net/chapters/18__project-generic-priority-queue#caveats-alternatives-edge-cases] - If you need stable ordering for items with identical scores, wrap the payload in a struct that stores a monotonically increasing sequence number and include it in the comparator. - For extremely large queues, consider chunking into buckets or using a pairing heap—`std.PriorityQueue` is binary and may incur cache misses for million-item heaps. - When exposing queue factories across module boundaries, document allocator ownership and provide explicit `destroy` helpers to prevent leaks when callers change policies at runtime. 19 (19__modules-and-imports-root-builtin-discovery.xml) # Chapter 19 — Modules & Imports [chapter_id: 19__modules-and-imports-root-builtin-discovery] [chapter_slug: modules-and-imports-root-builtin-discovery] [chapter_number: 19] [chapter_url: https://zigbook.net/chapters/19__modules-and-imports-root-builtin-discovery] ## Section: Overview [section_id: overview] [section_url: https://zigbook.net/chapters/19__modules-and-imports-root-builtin-discovery#overview] Chapter 18 wrapped a generic priority queue in reusable modules; now we widen the aperture to the compiler’s full module graph. We will draw clear lines between the root module, the standard library, and the special `builtin` namespace that surfaces compilation metadata. Along the way, we will embrace Zig 0.15.2’s I/O revamp, practice discovery of optional helpers, and preview how custom entry points hook into `std.start` for programs that need to bypass the default runtime prelude. For more detail, see 18 (18__project-generic-priority-queue.xml), start.zig (https://github.com/ziglang/zig/tree/master/lib/std/start.zig), and v0.15.2 (https://ziglang.org/download/0.15.1/release-notes.html). ## Section: Learning Goals [section_id: learning-goals] [section_url: https://zigbook.net/chapters/19__modules-and-imports-root-builtin-discovery#learning-goals] - Map how root, `std`, and `builtin` interact to form the compile-time module graph and share declarations safely. See std.zig (https://github.com/ziglang/zig/tree/master/lib/std/std.zig). - Harvest target, optimization, and build-mode metadata from `builtin` to steer configuration and diagnostics. See builtin.zig (https://github.com/ziglang/zig/tree/master/lib/std/builtin.zig). - Gate optional helpers with `@import` and `@hasDecl`, keeping discoveries explicit while supporting policy-driven modules. ## Section: Walking the module graph [section_id: module-graph] [section_url: https://zigbook.net/chapters/19__modules-and-imports-root-builtin-discovery#module-graph] The compiler perceives every source file as a namespaced struct. When you `@import` a path, the returned struct exposes any `pub` declarations for downstream use. The root module simply corresponds to your top-level file; anything it exports is immediately reachable through `@import("root")`, whether the caller is another module or a test block. We will inspect that relationship with a small constellation of files to show value sharing across modules while capturing build metadata. See File.zig (https://github.com/ziglang/zig/tree/master/lib/std/fs/File.zig). ### Subsection: Sharing root exports across helper modules [section_id: module-graph-example] [section_url: https://zigbook.net/chapters/19__modules-and-imports-root-builtin-discovery#module-graph-example] `module_graph_report.zig` instantiates a queue-like report across three files: the root exports a `Features` array, a `build_config.zig` helper formats metadata, and a `service/metrics.zig` module consumes the root exports to build a catalog. The example also demonstrates the new writer API introduced in 0.15.2, where we borrow a stack buffer and flush through the `std.fs.File.stdout().writer` interface. See Io.zig (https://github.com/ziglang/zig/tree/master/lib/std/Io.zig). ```zig // Import the standard library for I/O and basic functionality const std = @import("std"); // Import a custom module from the project to access build configuration utilities const config = @import("build_config.zig"); // Import a nested module demonstrating hierarchical module organization // This path uses a directory structure: service/metrics.zig const metrics = @import("service/metrics.zig"); /// Version string exported by the root module. /// This demonstrates how the root module can expose public constants /// that are accessible to other modules via @import("root"). pub const Version = "0.15.2"; /// Feature flags exported by the root module. /// This array of string literals showcases a typical pattern for documenting /// and advertising capabilities or experimental features in a Zig project. pub const Features = [_][]const u8{ "root-module-export", "builtin-introspection", "module-catalogue", }; /// Entry point for the module graph report utility. /// Demonstrates a practical use case for @import: composing functionality /// from multiple modules (std, custom build_config, nested service/metrics) /// and orchestrating their output to produce a unified report. pub fn main() !void { // Allocate a buffer for stdout buffering to reduce system calls var stdout_buffer: [1024]u8 = undefined; // Create a buffered writer for stdout to improve I/O performance var file_writer = std.fs.File.stdout().writer(&stdout_buffer); // Obtain the generic writer interface for formatted output const stdout = &file_writer.interface; // Print a header to introduce the report try stdout.print("== Module graph walkthrough ==\n", .{}); // Display the version constant defined in this root module // This shows how modules can export and reference their own public declarations try stdout.print("root.Version -> {s}\n", .{Version}); // Invoke a function from the imported build_config module // This demonstrates cross-module function calls and how modules // encapsulate and expose behavior through their public API try config.printSummary(stdout); // Invoke a function from the nested metrics module // This illustrates hierarchical module organization and the ability // to compose deeply nested modules into a coherent application try metrics.printCatalog(stdout); // Flush the buffered writer to ensure all output is written to stdout try stdout.flush(); } ``` Run: ```shell $ zig run module_graph_report.zig ``` Output: ```shell == Module graph walkthrough == root.Version -> 1.4.0 mode=Debug target=x86_64-linux features: root-module-export builtin-introspection module-catalogue Features exported by root (3): 1. root-module-export 2. builtin-introspection 3. module-catalogue ``` The helper modules reference `@import("root")` to read `Features`, and they format `builtin.target` information to prove the metadata flows correctly. Treat this pattern as your baseline for sharing config without reaching for globals or singleton state. ### Subsection: How calls are tracked internally [section_id: module-graph-import-tracking] [section_url: https://zigbook.net/chapters/19__modules-and-imports-root-builtin-discovery#module-graph-import-tracking] At the compiler level, each `@import("path")` expression becomes an entry in an imports map during AST-to-ZIR lowering. This map deduplicates paths, preserves token locations for diagnostics, and ultimately feeds a packed `Imports` payload in the ZIR extra data. ```text graph TB ImportExpr["@import("path")"] --> CheckImports["Check imports map"] CheckImports -->|Exists| UseExisting["Reuse existing import"] CheckImports -->|Not exists| AddImport["Add to imports map"] AddImport --> StoreToken["Map string_index -> token"] StoreToken --> GenerateInst["Generate import instruction"] GenerateInst --> Finalize["At end of AstGen"] Finalize --> StoreImports["Store Imports payload
in extra array"] ``` ## Section: Inspecting build metadata via [section_id: builtin-metadata] [section_url: https://zigbook.net/chapters/19__modules-and-imports-root-builtin-discovery#builtin-metadata] The `builtin` namespace is assembled by the compiler for every translation unit. It exposes fields such as `mode`, `target`, `single_threaded`, and `link_libc`, allowing you to tailor diagnostics or guard costly features behind compile-time switches. The next example exercises these fields and shows how to keep optional imports quarantined behind `comptime` checks so they never trigger in release builds. ```zig // Import the standard library for I/O and basic functionality const std = @import("std"); // Import the builtin module to access compile-time build information const builtin = @import("builtin"); // Compute a human-readable hint about the current optimization mode at compile time. // This block evaluates once during compilation and embeds the result as a constant string. const optimize_hint = blk: { break :blk switch (builtin.mode) { .Debug => "debug symbols and runtime safety checks enabled", .ReleaseSafe => "runtime checks on, optimized for safety", .ReleaseFast => "optimizations prioritized for speed", .ReleaseSmall => "optimizations prioritized for size", }; }; /// Entry point for the builtin probe utility. /// Demonstrates how to query and display compile-time build configuration /// from the `builtin` module, including Zig version, optimization mode, /// target platform details, and linking options. pub fn main() !void { // Allocate a buffer for stdout buffering to reduce system calls var stdout_buffer: [1024]u8 = undefined; // Create a buffered writer for stdout to improve I/O performance var file_writer = std.fs.File.stdout().writer(&stdout_buffer); // Obtain the generic writer interface for formatted output const out = &file_writer.interface; // Print the Zig compiler version string embedded at compile time try out.print("zig version (compiler): {s}\n", .{builtin.zig_version_string}); // Print the optimization mode and its corresponding description try out.print("optimize mode: {s} — {s}\n", .{ @tagName(builtin.mode), optimize_hint }); // Print the target triple: architecture, OS, and ABI // These values reflect the platform for which the binary was compiled try out.print( "target triple: {s}-{s}-{s}\n", .{ @tagName(builtin.target.cpu.arch), @tagName(builtin.target.os.tag), @tagName(builtin.target.abi), }, ); // Indicate whether the binary was built in single-threaded mode try out.print("single-threaded build: {}\n", .{builtin.single_threaded}); // Indicate whether the standard C library (libc) is linked try out.print("linking libc: {}\n", .{builtin.link_libc}); // Compile-time block to conditionally import test helpers when running tests. // This demonstrates using `builtin.is_test` to enable test-only code paths. comptime { if (builtin.is_test) { // The root module could enable test-only helpers using this hook. _ = @import("test_helpers.zig"); } } // Flush the buffered writer to ensure all output is written to stdout try out.flush(); } ``` Run: ```shell $ zig run builtin_probe.zig ``` Output: ```shell zig version (compiler): 0.15.2 optimize mode: Debug — debug symbols and runtime safety checks enabled target triple: x86_64-linux-gnu single-threaded build: false linking libc: false ``` Key takeaways: - `std.fs.File.stdout().writer(&buffer)` supplies a buffered writer compatible with the new `std.Io.Writer` API; always flush before exit to avoid truncated output. - `builtin.is_test` is a comptime constant. Gating `@import("test_helpers.zig")` behind that flag ensures test-only helpers disappear from release builds while keeping coverage instrumentation centralized. - Using `@tagName` on enum-like fields (`mode`, `target.cpu.arch`) yields strings without heap allocation, making them ideal for banner messages or feature toggles. ### Subsection: Optimization modes in practice [section_id: builtin-optimization-modes] [section_url: https://zigbook.net/chapters/19__modules-and-imports-root-builtin-discovery#builtin-optimization-modes] The `builtin.mode` field observed in the probe corresponds to the optimizer configuration for the current module. Each mode trades off safety checks, debug information, speed, and binary size; understanding these trade-offs helps you decide when to enable discovery hooks or expensive diagnostics. | Mode | Priority | Safety Checks | Speed | Binary Size | Use Case | | --- | --- | --- | --- | --- | --- | | `Debug` | Safety + Debug Info | All enabled | Slowest | Largest | Development and debugging | | `ReleaseSafe` | Speed + Safety | All enabled | Fast | Large | Production with safety | | `ReleaseFast` | Maximum Speed | Disabled | Fastest | Medium | Performance-critical production | | `ReleaseSmall` | Minimum Size | Disabled | Fast | Smallest | Embedded systems, size-constrained | The optimization mode is specified per-module and affects: - Runtime safety checks (overflow, bounds checking, null checks) - Stack traces and debug information generation - LLVM optimization level (when using LLVM backend) - Inlining heuristics and code generation strategies ```text graph TB subgraph "Optimization Mode Effects" OptMode["optimize_mode: OptimizeMode"] OptMode --> SafetyChecks["Runtime Safety Checks"] OptMode --> DebugInfo["Debug Information"] OptMode --> CodegenStrategy["Codegen Strategy"] OptMode --> LLVMOpt["LLVM Optimization Level"] SafetyChecks --> Overflow["Integer overflow checks"] SafetyChecks --> Bounds["Bounds checking"] SafetyChecks --> Null["Null pointer checks"] SafetyChecks --> Unreachable["Unreachable assertions"] DebugInfo --> StackTraces["Stack traces"] DebugInfo --> DWARF["DWARF debug info"] DebugInfo --> LineInfo["Source line information"] CodegenStrategy --> Inlining["Inline heuristics"] CodegenStrategy --> Unrolling["Loop unrolling"] CodegenStrategy --> Vectorization["SIMD vectorization"] LLVMOpt --> O0["Debug: -O0"] LLVMOpt --> O2Safe["ReleaseSafe: -O2 + safety"] LLVMOpt --> O3["ReleaseFast: -O3"] LLVMOpt --> Oz["ReleaseSmall: -Oz"] end ``` ### Subsection: Case study: -driven test configuration [section_id: builtin-test-config] [section_url: https://zigbook.net/chapters/19__modules-and-imports-root-builtin-discovery#builtin-test-config] The standard library’s test framework uses `builtin` fields extensively to decide when to skip tests for unsupported backends, platforms, or optimization modes. The flow below mirrors the conditional patterns you can adopt in your own modules when wiring optional helpers. ```text graph TB subgraph "Conditional Execution" BACKEND_CHECK["Backend Check
if (builtin.zig_backend == .stage2_X)
return error.SkipZigTest;"] PLATFORM_CHECK["Platform Check
if (builtin.os.tag == .X)
return error.SkipZigTest;"] MODE_CHECK["Mode Check
if (builtin.mode == .ReleaseFast)
return error.SkipZigTest;"] end subgraph "Test Types" RUNTIME["Runtime Test
var x = computeValue();"] COMPTIME["Comptime Test
try comptime testFunction();"] MIXED["Mixed Test
try testFn();
try comptime testFn();"] end BODY --> BACKEND_CHECK BODY --> PLATFORM_CHECK BODY --> MODE_CHECK BODY --> RUNTIME BODY --> COMPTIME BODY --> MIXED ``` ## Section: Optional discovery with and [section_id: optional-discovery] [section_url: https://zigbook.net/chapters/19__modules-and-imports-root-builtin-discovery#optional-discovery] Large systems frequently ship debug-only tooling or experimental adapters. Rather than silently probing the filesystem, Zig encourages explicit discovery: import the helper module at comptime when a policy is enabled, then interrogate its exported API with `@hasDecl`. The following sample does just that by conditionally wiring `tools/dev_probe.zig` into the build when running in Debug mode. ```zig //! Discovery probe utility demonstrating conditional imports and runtime introspection. //! This module showcases how to use compile-time conditionals to optionally load //! development tools and query their capabilities at runtime using reflection. const std = @import("std"); const builtin = @import("builtin"); /// Conditionally import development hooks based on build mode. /// In Debug mode, imports the full dev_probe module with diagnostic capabilities. /// In other modes (ReleaseSafe, ReleaseFast, ReleaseSmall), provides a minimal /// stub implementation to avoid loading unnecessary development tooling. /// /// This pattern enables zero-cost abstractions where development features are /// completely elided from release builds while maintaining a consistent API. pub const DevHooks = if (builtin.mode == .Debug) @import("tools/dev_probe.zig") else struct { /// Minimal stub implementation for non-debug builds. /// Returns a static message indicating development hooks are disabled. pub fn banner() []const u8 { return "dev hooks disabled"; } }; /// Entry point demonstrating module discovery and conditional feature detection. /// This function showcases: /// 1. The new Zig 0.15.2 buffered writer API for stdout /// 2. Compile-time conditional imports (DevHooks) /// 3. Runtime introspection using @hasDecl to probe for optional functions pub fn main() !void { // Create a stack-allocated buffer for stdout operations var stdout_buffer: [512]u8 = undefined; // Initialize a file writer with our buffer. This is part of the Zig 0.15.2 // I/O revamp where writers now require explicit buffer management. var file_writer = std.fs.File.stdout().writer(&stdout_buffer); // Obtain the generic writer interface for formatted output const stdout = &file_writer.interface; // Report the current build mode (Debug, ReleaseSafe, ReleaseFast, ReleaseSmall) try stdout.print("discovery mode: {s}\n", .{@tagName(builtin.mode)}); // Call the always-available banner() function from DevHooks. // The implementation varies based on whether we're in Debug mode or not. try stdout.print("dev hooks: {s}\n", .{DevHooks.banner()}); // Use @hasDecl to check if the buildSession() function exists in DevHooks. // This demonstrates runtime discovery of optional capabilities without // requiring all implementations to provide every function. if (@hasDecl(DevHooks, "buildSession")) { // buildSession() is only available in the full dev_probe module (Debug builds) try stdout.print("built with zig {s}\n", .{DevHooks.buildSession()}); } else { // In release builds, the stub DevHooks doesn't provide buildSession() try stdout.print("no buildSession() exported\n", .{}); } // Flush the buffered output to ensure all content is written to stdout try stdout.flush(); } ``` Run: ```shell $ zig run discovery_probe.zig ``` Output: ```shell discovery mode: Debug dev hooks: debug-only instrumentation active built with zig 0.15.2 ``` Because `DevHooks` is itself a comptime `if`, Release builds replace the import with a stub struct whose API documents the absence of dev features. Combined with `@hasDecl`, the root module can emit a summary without enumerating every optional hook manually, keeping compile-time discovery explicit and reproducible. ## Section: Entry points and [section_id: entrypoints] [section_url: https://zigbook.net/chapters/19__modules-and-imports-root-builtin-discovery#entrypoints] `std.start` inspects the root module to decide whether to export `main`, `_start`, or platform-specific entry symbols. If you provide `pub fn _start() noreturn`, the default start shim stands aside, letting you wire syscalls or a bespoke runtime manually. ### Subsection: Entry point symbol table [section_id: entrypoints-symbol-table] [section_url: https://zigbook.net/chapters/19__modules-and-imports-root-builtin-discovery#entrypoints-symbol-table] The exported symbol chosen by `std.start` depends on the platform, link mode, and configuration flags such as `link_libc`. The table below summarizes the most important combinations. | Platform | Link Mode | Conditions | Exported Symbol | Handler Function | | --- | --- | --- | --- | --- | | POSIX/Linux | Executable | Default | `_start` | `_start()` | | POSIX/Linux | Executable | Linking libc | `main` | `main()` | | Windows | Executable | Default | `wWinMainCRTStartup` | `WinStartup()` / `wWinMainCRTStartup()` | | Windows | Dynamic Library | Default | `_DllMainCRTStartup` | `_DllMainCRTStartup()` | | UEFI | Executable | Default | `EfiMain` | `EfiMain()` | | WASI | Executable (command) | Default | `_start` | `wasi_start()` | | WASI | Executable (reactor) | Default | `_initialize` | `wasi_start()` | | WebAssembly | Freestanding | Default | `_start` | `wasm_freestanding_start()` | | WebAssembly | Linking libc | Default | `__main_argc_argv` | `mainWithoutEnv()` | | OpenCL/Vulkan | Kernel | Default | `main` | `spirvMain2()` | | MIPS | Any | Default | `__start` | (same as `_start`) | ### Subsection: Compile-time entry point logic [section_id: entrypoints-logic] [section_url: https://zigbook.net/chapters/19__modules-and-imports-root-builtin-discovery#entrypoints-logic] Internally, `std.start` uses `builtin` fields such as `output_mode`, `os`, `link_libc`, and the target architecture to decide which symbol to export. The compile-time flow mirrors the cases in the symbol table. ```text graph TB Start["comptime block
(start.zig:28)"] CheckMode["Check builtin.output_mode"] CheckSimplified["simplified_logic?
(stage2 backends)"] CheckLinkC["link_libc or
object_format == .c?"] CheckWindows["builtin.os == .windows?"] CheckUEFI["builtin.os == .uefi?"] CheckWASI["builtin.os == .wasi?"] CheckWasm["arch.isWasm() &&
os == .freestanding?"] ExportMain["@export(&main, 'main')"] ExportWinMain["@export(&WinStartup,
'wWinMainCRTStartup')"] ExportStart["@export(&_start, '_start')"] ExportEfi["@export(&EfiMain, 'EfiMain')"] ExportWasi["@export(&wasi_start,
wasm_start_sym)"] ExportWasmStart["@export(&wasm_freestanding_start,
'_start')"] Start --> CheckMode CheckMode -->|".Exe or has main"| CheckSimplified CheckSimplified -->|"true"| Simple["Simplified logic
(lines 33-51)"] CheckSimplified -->|"false"| CheckLinkC CheckLinkC -->|"yes"| ExportMain CheckLinkC -->|"no"| CheckWindows CheckWindows -->|"yes"| ExportWinMain CheckWindows -->|"no"| CheckUEFI CheckUEFI -->|"yes"| ExportEfi CheckUEFI -->|"no"| CheckWASI CheckWASI -->|"yes"| ExportWasi CheckWASI -->|"no"| CheckWasm CheckWasm -->|"yes"| ExportWasmStart CheckWasm -->|"no"| ExportStart ``` `std.start` inspects the root module to decide whether to export `main`, `_start`, or platform-specific entry symbols. If you provide `pub fn _start() noreturn`, the default start shim stands aside, letting you wire syscalls or a bespoke runtime manually. To keep the toolchain happy: - Build with `-fno-entry` so the linker does not expect the C runtime’s `main`. - Emit diagnostics via syscalls or lightweight wrappers; the standard I/O stack assumes `std.start` performed its initialization. See linux.zig (https://github.com/ziglang/zig/tree/master/lib/std/os/linux.zig). - Optionally wrap the low-level entry point with a thin compat shim that calls into a higher-level Zig function so your business logic still lives in ergonomically testable code. In the next chapter we will generalize these ideas into a vocabulary for differentiating modules, programs, packages, and libraries, preparing us to scale compile-time configuration without muddling namespace boundaries. 20 (20__concept-primer-modules-vs-programs-vs-packages-vs-libraries.xml) ## Section: Notes & Caveats [section_id: notes-caveats] [section_url: https://zigbook.net/chapters/19__modules-and-imports-root-builtin-discovery#notes-caveats] - Prefer `@import("root")` over global singletons when sharing configuration structs; it keeps dependencies explicit and plays nicely with Zig’s compile-time evaluation. - The 0.15.2 writer API requires explicit buffers; adjust buffer sizes to match your output volume and always flush before returning. - Optional imports should live behind policy-enforcing declarations so production artifacts do not accidentally drag dev-only code into release builds. ## Section: Exercises [section_id: exercises] [section_url: https://zigbook.net/chapters/19__modules-and-imports-root-builtin-discovery#exercises] - Extend `module_graph_report.zig` so the `Features` array becomes a struct of structs, then update the catalog printer to format nested capabilities with indentation. 13 (13__testing-and-leak-detection.xml) - Modify `builtin_probe.zig` to emit a JSON fragment describing the target; use `std.json.stringify` and verify the output under each optimization mode. See json.zig (https://github.com/ziglang/zig/tree/master/lib/std/json.zig). - Add a ReleaseFast-only helper module for `discovery_probe.zig` that tracks build timestamps; guard it with `if (builtin.mode == .ReleaseFast)` and prove via tests that ReleaseSafe builds never import it. 13 (13__testing-and-leak-detection.xml) ## Section: Caveats, alternatives, edge cases [section_id: caveats-alternatives-edge-cases] [section_url: https://zigbook.net/chapters/19__modules-and-imports-root-builtin-discovery#caveats-alternatives-edge-cases] - When combining `@import("root")` with `@This()` from inside the same file, beware of circular references; forward declarations or intermediate helper structs can break the cycle. - On cross-compilation targets where `std.fs.File.stdout()` may not exist (e.g. freestanding WASM), fall back to target-specific writers or telemetry buffers before flushing. See wasi.zig (https://github.com/ziglang/zig/tree/master/lib/std/os/wasi.zig). - If you disable `std.start`, you also opt out of Zig’s automatic panic handler and argument parsing helpers; reintroduce equivalents explicitly or document the new contract for consumers. # Chapter 20 — Concept Primer [chapter_id: 20__concept-primer-modules-vs-programs-vs-packages-vs-libraries] [chapter_slug: concept-primer-modules-vs-programs-vs-packages-vs-libraries] [chapter_number: 20] [chapter_url: https://zigbook.net/chapters/20__concept-primer-modules-vs-programs-vs-packages-vs-libraries] ## Section: Overview [section_id: overview] [section_url: https://zigbook.net/chapters/20__concept-primer-modules-vs-programs-vs-packages-vs-libraries#overview] Chapter 19 (19__modules-and-imports-root-builtin-discovery.xml) mapped the compiler’s module graph; this chapter names the roles those modules can play so you know when a file is merely a helper, when it graduates to a program, and when it becomes the nucleus of a reusable package or library. We will also preview how the Zig CLI registers modules for consumers, setting the stage for build graph authoring in Chapter 21 (21__zig-init-and-package-metadata.xml) and in build.zig (https://github.com/ziglang/zig/tree/master/lib/std/build.zig). ## Section: Learning Goals [section_id: learning-goals] [section_url: https://zigbook.net/chapters/20__concept-primer-modules-vs-programs-vs-packages-vs-libraries#learning-goals] - Distinguish modules, programs, packages, and libraries, and explain how Zig treats each during compilation. - Use the `--dep` and `-M` flags (and their build graph equivalents) to register named modules for consumers. - Apply a practical checklist for picking the right unit when starting a new artifact or refactoring an existing one. 19 (19__modules-and-imports-root-builtin-discovery.xml) ## Section: Building a shared vocabulary [section_id: vocabulary] [section_url: https://zigbook.net/chapters/20__concept-primer-modules-vs-programs-vs-packages-vs-libraries#vocabulary] Before you wire build scripts or register dependencies, settle on consistent language: In Zig, a module is any compilation unit returned by `@import`, a program is a module graph with an entry point, a package bundles modules plus metadata, and a library is a package intended for reuse without a root `main`. start.zig (https://github.com/ziglang/zig/tree/master/lib/std/start.zig) ### Subsection: Modules and programs in practice [section_id: modules-programs] [section_url: https://zigbook.net/chapters/20__concept-primer-modules-vs-programs-vs-packages-vs-libraries#modules-programs] This demo starts with a root module that exports a manifest for a library but also declares `main`, so the runtime treats the graph as a program while the helper module introspects public symbols to keep terminology honest. 19 (19__modules-and-imports-root-builtin-discovery.xml) ```zig // This module demonstrates how Zig's module system distinguishes between different roles: // programs (with main), libraries (exposing public APIs), and hybrid modules. // It showcases introspection of module characteristics and role-based decision making. const std = @import("std"); const roles = @import("role_checks.zig"); const manifest_pkg = @import("pkg/manifest.zig"); /// List of public declarations intentionally exported by the root module. /// This array defines the public API surface that other modules can rely on. /// It serves as documentation and can be used for validation or tooling. pub const PublicSurface = [_][]const u8{ "main", "libraryManifest", "PublicSurface", }; /// Provide a canonical manifest describing the library surface that this module exposes. /// Other modules import this helper to reason about the package-level API. /// Returns a Manifest struct containing metadata about the library's public interface. pub fn libraryManifest() manifest_pkg.Manifest { // Delegate to the manifest package to construct a sample library descriptor return manifest_pkg.sampleLibrary(); } /// Entry point demonstrating module role classification and vocabulary. /// Analyzes both the root module and a library module, printing their characteristics: /// - Whether they export a main function (indicating program vs library intent) /// - Public symbol counts (API surface area) /// - Role recommendations based on module structure pub fn main() !void { // Use a fixed-size stack buffer for stdout to avoid heap allocation var stdout_buffer: [768]u8 = undefined; var file_writer = std.fs.File.stdout().writer(&stdout_buffer); const stdout = &file_writer.interface; // Capture snapshots of module characteristics for analysis const root_snapshot = roles.rootSnapshot(); const library_snapshot = roles.librarySnapshot(); // Retrieve role-based decision guidance const decisions = roles.decisions(); try stdout.print("== Module vocabulary demo ==\n", .{}); // Display root module role determination based on main export try stdout.print( "root exports main? {s} → treat as {s}\n", .{ if (root_snapshot.exports_main) "yes" else "no", root_snapshot.role, }, ); // Show the number of public declarations in the root module try stdout.print( "root public surface: {d} declarations\n", .{root_snapshot.public_symbol_count}, ); // Display library module metadata: name, version, and main export status try stdout.print( "library '{s}' v{s} exports main? {s}\n", .{ library_snapshot.name, library_snapshot.version, if (library_snapshot.exports_main) "yes" else "no", }, ); // Show the count of public modules or symbols in the library try stdout.print( "library modules listed: {d}\n", .{library_snapshot.public_symbol_count}, ); // Print architectural guidance for different module design goals try stdout.print("intent cheat sheet:\n", .{}); for (decisions) |entry| { try stdout.print(" - {s} → {s}\n", .{ entry.goal, entry.recommendation }); } // Flush buffered output to ensure all content is written try stdout.flush(); } ``` Run: ```shell $ zig run module_role_map.zig ``` Output: ```shell == Module vocabulary demo == root exports main? yes → treat as program root public surface: 3 declarations library 'widgetlib' v0.1.0 exports main? no library modules listed: 2 intent cheat sheet: - ship a CLI entry point → program - publish reusable code → package + library - share type definitions inside a workspace → module ``` TIP: Keep root exports minimal and document them in one place (`PublicSurface` here) so helper modules can reason about intent without relying on undocumented globals. ### Subsection: Under the hood: entry points and programs [section_id: modules-programs-entrypoints] [section_url: https://zigbook.net/chapters/20__concept-primer-modules-vs-programs-vs-packages-vs-libraries#modules-programs-entrypoints] Whether a module graph behaves as a program or a library depends on whether it ultimately exports an entry point symbol. `std.start` decides which symbol to export based on platform, link mode, and a few `builtin` fields, so the presence of `main` is only part of the story. ### Subsection: Entry point symbol table [section_id: _entry_point_symbol_table] [section_url: https://zigbook.net/chapters/20__concept-primer-modules-vs-programs-vs-packages-vs-libraries#_entry_point_symbol_table] | Platform | Link Mode | Conditions | Exported Symbol | Handler Function | | --- | --- | --- | --- | --- | | POSIX/Linux | Executable | Default | `_start` | `_start()` | | POSIX/Linux | Executable | Linking libc | `main` | `main()` | | Windows | Executable | Default | `wWinMainCRTStartup` | `WinStartup()` / `wWinMainCRTStartup()` | | Windows | Dynamic Library | Default | `_DllMainCRTStartup` | `_DllMainCRTStartup()` | | UEFI | Executable | Default | `EfiMain` | `EfiMain()` | | WASI | Executable (command) | Default | `_start` | `wasi_start()` | | WASI | Executable (reactor) | Default | `_initialize` | `wasi_start()` | | WebAssembly | Freestanding | Default | `_start` | `wasm_freestanding_start()` | | WebAssembly | Linking libc | Default | `__main_argc_argv` | `mainWithoutEnv()` | | OpenCL/Vulkan | Kernel | Default | `main` | `spirvMain2()` | | MIPS | Any | Default | `__start` | (same as `_start`) | Sources:start.zig (https://github.com/ziglang/zig/tree/master/lib/std/start.zig) ### Subsection: Compile-time entry point logic [section_id: _compile_time_entry_point_logic] [section_url: https://zigbook.net/chapters/20__concept-primer-modules-vs-programs-vs-packages-vs-libraries#_compile_time_entry_point_logic] At compile time, `std.start` runs a small decision tree over `builtin.output_mode`, `builtin.os`, `link_libc`, and the target architecture to export exactly one of the symbols above: ```text graph TB Start["comptime block
(start.zig:28)"] CheckMode["Check builtin.output_mode"] CheckSimplified["simplified_logic?
(stage2 backends)"] CheckLinkC["link_libc or
object_format == .c?"] CheckWindows["builtin.os == .windows?"] CheckUEFI["builtin.os == .uefi?"] CheckWASI["builtin.os == .wasi?"] CheckWasm["arch.isWasm() &&
os == .freestanding?"] ExportMain["@export(&main, 'main')"] ExportWinMain["@export(&WinStartup,
'wWinMainCRTStartup')"] ExportStart["@export(&_start, '_start')"] ExportEfi["@export(&EfiMain, 'EfiMain')"] ExportWasi["@export(&wasi_start,
wasm_start_sym)"] ExportWasmStart["@export(&wasm_freestanding_start,
'_start')"] Start --> CheckMode CheckMode -->|".Exe or has main"| CheckSimplified CheckSimplified -->|"true"| Simple["Simplified logic
(lines 33-51)"] CheckSimplified -->|"false"| CheckLinkC CheckLinkC -->|"yes"| ExportMain CheckLinkC -->|"no"| CheckWindows CheckWindows -->|"yes"| ExportWinMain CheckWindows -->|"no"| CheckUEFI CheckUEFI -->|"yes"| ExportEfi CheckUEFI -->|"no"| CheckWASI CheckWASI -->|"yes"| ExportWasi CheckWASI -->|"no"| CheckWasm CheckWasm -->|"yes"| ExportWasmStart CheckWasm -->|"no"| ExportStart ``` Sources:lib/std/start.zig:28-100 (https://github.com/ziglang/zig/tree/master/lib/std/start.zig) ### Subsection: Library manifests and internal reuse [section_id: library-manifests] [section_url: https://zigbook.net/chapters/20__concept-primer-modules-vs-programs-vs-packages-vs-libraries#library-manifests] The manifest recorded in `pkg/manifest.zig` models what eventually becomes package metadata: a name, semantic version, a list of modules, and an explicit statement that no entry point is exported. ## Section: Packages as distribution contracts [section_id: packages] [section_url: https://zigbook.net/chapters/20__concept-primer-modules-vs-programs-vs-packages-vs-libraries#packages] Packages are agreements between producers and consumers: producers register module names and expose metadata; consumers import those names without touching filesystem paths, trusting the build graph to supply the right code. ### Subsection: Registering modules with -M and --dep [section_id: registering-modules] [section_url: https://zigbook.net/chapters/20__concept-primer-modules-vs-programs-vs-packages-vs-libraries#registering-modules] Zig 0.15.2 replaces legacy `--pkg-begin/--pkg-end` syntax with `-M` (module definition) and `--dep` (import table entry), mirroring what `std.build` does when it wires workspaces (see Build.zig (https://github.com/ziglang/zig/tree/master/lib/std/Build.zig)). ```zig // Import the standard library for common utilities and types const std = @import("std"); // Import builtin module to access compile-time information about the build const builtin = @import("builtin"); // Import the overlay module by name as it will be registered via --dep/-M on the CLI const overlay = @import("overlay"); /// Entry point for the package overlay demonstration program. /// Demonstrates how to use the overlay_widget library to display package information /// including build mode and target operating system details. pub fn main() !void { // Allocate a fixed-size buffer on the stack for stdout operations // This avoids heap allocation for simple output scenarios var stdout_buffer: [512]u8 = undefined; // Create a buffered writer for stdout to improve performance by batching writes var file_writer = std.fs.File.stdout().writer(&stdout_buffer); const stdout = &file_writer.interface; // Populate package details structure with information about the current package // This includes compile-time information like optimization mode and target OS const details = overlay.PackageDetails{ .package_name = "overlay", .role = "library package", // Extract the optimization mode name (e.g., Debug, ReleaseFast) at compile time .optimize_mode = @tagName(builtin.mode), // Extract the target OS name (e.g., linux, windows) at compile time .target_os = @tagName(builtin.target.os.tag), }; // Render the package summary to stdout using the overlay library try overlay.renderSummary(stdout, details); // Ensure all buffered output is written to the terminal try stdout.flush(); } ``` ```zig const std = @import("std"); /// Summary of a package registration as seen from the consumer invoking `--pkg-begin`. pub const PackageDetails = struct { package_name: []const u8, role: []const u8, optimize_mode: []const u8, target_os: []const u8, }; /// Render a formatted summary that demonstrates how package registration exposes modules by name. pub fn renderSummary(writer: anytype, details: PackageDetails) !void { try writer.print("registered package: {s}\n", .{details.package_name}); try writer.print("role advertised: {s}\n", .{details.role}); try writer.print("optimize mode: {s}\n", .{details.optimize_mode}); try writer.print("target os: {s}\n", .{details.target_os}); try writer.print( "resolved module namespace: overlay → pub decls: {d}\n", .{moduleDeclCount()}, ); } fn moduleDeclCount() usize { // Enumerate the declarations exported by this module to simulate API surface reporting. return std.meta.declarations(@This()).len; } ``` Run: ```shell $ zig build-exe --dep overlay -Mroot=package_overlay_demo.zig -Moverlay=overlay_widget.zig -femit-bin=overlay_demo && ./overlay_demo ``` Output: ```shell registered package: overlay role advertised: library package optimize mode: Debug target os: linux resolved module namespace: overlay → pub decls: 2 ``` IMPORTANT: `--dep overlay` must precede the module declaration that consumes it; otherwise the import table stays empty and the compiler cannot resolve `@import("overlay")`.`--dep` writes the dependency list for the next `-M` entry, mirroring how `std.build.Module.addImport` queues dependencies before the module materializes. ### Subsection: Case study: compiler bootstrap command [section_id: _case_study_compiler_bootstrap_command] [section_url: https://zigbook.net/chapters/20__concept-primer-modules-vs-programs-vs-packages-vs-libraries#_case_study_compiler_bootstrap_command] The Zig compiler itself is built using the same `-M`/`--dep` machinery. During the bootstrap from `zig1` to `zig2`, the command line wires multiple named modules and their dependencies: ```text zig1 build-exe -ofmt=c -lc -OReleaseSmall \ --name zig2 \ -femit-bin=zig2.c \ -target \ --dep build_options \ --dep aro \ -Mroot=src/main.zig \ -Mbuild_options=config.zig \ -Maro=lib/compiler/aro/aro.zig ``` Here, each `--dep` line queues a dependency for the next `-M` module declaration, just like in the small overlay demo but at compiler scale. ### Subsection: From CLI flags to build graph [section_id: _from_cli_flags_to_build_graph] [section_url: https://zigbook.net/chapters/20__concept-primer-modules-vs-programs-vs-packages-vs-libraries#_from_cli_flags_to_build_graph] Once you move from ad-hoc `zig build-exe` commands to a `build.zig` file, the same concepts reappear as `std.Build` and `std.Build.Module` nodes in a build graph. The diagram below summarizes how the native build system’s entry point wires compiler compilation, tests, docs, and installation. ```text graph TB subgraph "Build Entry Point" BUILD_FN["build(b: *std.Build)"] --> OPTIONS["Parse Build Options"] OPTIONS --> COMPILER["addCompilerStep()"] OPTIONS --> TEST_SETUP["Test Suite Setup"] OPTIONS --> DOCS["Documentation Steps"] end subgraph "Compiler Compilation" COMPILER --> EXE["std.Build.CompileStep
(zig executable)"] EXE --> COMPILER_MOD["addCompilerMod()"] EXE --> BUILD_OPTIONS["build_options
(generated config)"] EXE --> LLVM_INTEGRATION["LLVM/Clang/LLD
linking"] end subgraph "Test Steps" TEST_SETUP --> TEST_CASES["test-cases
tests.addCases()"] TEST_SETUP --> TEST_MODULES["test-modules
tests.addModuleTests()"] TEST_SETUP --> TEST_UNIT["test-unit
compiler unit tests"] TEST_SETUP --> TEST_STANDALONE["test-standalone"] TEST_SETUP --> TEST_CLI["test-cli"] end subgraph "Documentation" DOCS --> LANGREF_GEN["generateLangRef()
(tools/docgen.zig)"] DOCS --> STD_DOCS["autodoc_test
(lib/std/std.zig)"] end subgraph "Installation" EXE --> INSTALL_BIN["stage3/bin/zig"] INSTALL_LIB_DIR["lib/ directory"] --> INSTALL_LIB_TARGET["stage3/lib/zig/"] LANGREF_GEN --> INSTALL_LANGREF["stage3/doc/langref.html"] STD_DOCS --> INSTALL_STD_DOCS["stage3/doc/std/"] end ``` ### Subsection: Documenting package intent [section_id: package-intent] [section_url: https://zigbook.net/chapters/20__concept-primer-modules-vs-programs-vs-packages-vs-libraries#package-intent] Beyond the CLI flags, intent lives in documentation: describe which modules are public, whether you expect downstream entry points, and how the package should be consumed by other build graphs (see Module.zig (https://github.com/ziglang/zig/tree/master/lib/std/Build/Module.zig)). ## Section: Choosing the right unit fast [section_id: choosing-unit] [section_url: https://zigbook.net/chapters/20__concept-primer-modules-vs-programs-vs-packages-vs-libraries#choosing-unit] Use the cheat sheet below when deciding what to create next; it is intentionally opinionated so teams develop shared defaults. 19 (19__modules-and-imports-root-builtin-discovery.xml) | You want to… | Prefer | Rationale | | --- | --- | --- | | Publish reusable algorithms with no entry point | Package + library | Bundle modules with metadata so consumers can import by name and stay decoupled from paths. | | Ship a command-line tool | Program | Export a `main` (or `_start`) and keep helper modules private unless you intend to share them. | | Share types across files inside one repo | Module | Use plain `@import` to expose namespaces without coupling build metadata prematurely. 19 (19__modules-and-imports-root-builtin-discovery.xml) | ### Subsection: Artifact types at a glance [section_id: _artifact_types_at_a_glance] [section_url: https://zigbook.net/chapters/20__concept-primer-modules-vs-programs-vs-packages-vs-libraries#_artifact_types_at_a_glance] The compiler’s `output_mode` and `link_mode` choices determine the concrete artifact form that backs each conceptual role. Programs usually build as executables, while libraries use `Lib` outputs that can be static or dynamic. ```text graph LR subgraph "Output Mode + Link Mode = Artifact Type" Exe_static["output_mode: Exe
link_mode: static"] --> ExeStatic["Static executable"] Exe_dynamic["output_mode: Exe
link_mode: dynamic"] --> ExeDynamic["Dynamic executable"] Lib_static["output_mode: Lib
link_mode: static"] --> LibStatic["Static library (.a)"] Lib_dynamic["output_mode: Lib
link_mode: dynamic"] --> LibDynamic["Shared library (.so/.dll)"] Obj["output_mode: Obj
link_mode: N/A"] --> ObjFile["Object file (.o)"] end ``` Sources:Config.zig (https://github.com/ziglang/zig/tree/master/src/Compilation/Config.zig), main.zig (https://github.com/ziglang/zig/tree/master/src/main.zig), builtin.zig (https://github.com/ziglang/zig/tree/master/lib/std/builtin.zig) You can combine the vocabulary from this chapter with these artifact types using a simple mapping: | Role | Typical artifact | Notes | | --- | --- | --- | | Program | `output_mode: Exe` (static or dynamic) | Exposes an entry point; may also export helper modules internally. | | Library package | `output_mode: Lib` (static or shared) | Intended for reuse; no root `main`, consumers import modules by name. | | Internal module | Depends on context | Often compiled as part of an executable or library; exposed via `@import` rather than a standalone artifact. | ## Section: Notes & Caveats [section_id: notes-caveats] [section_url: https://zigbook.net/chapters/20__concept-primer-modules-vs-programs-vs-packages-vs-libraries#notes-caveats] - Record manifest-like data even in ad-hoc modules so later promotion to a package is mechanical. - When you convert a program into a library, delete or guard the entry point; otherwise consumers get conflicting roots. 19 (19__modules-and-imports-root-builtin-discovery.xml) - The `-M`/`--dep` workflow is a thin veneer over `std.build.Module`, so prefer the build graph once your project exceeds a single binary. 21 (21__zig-init-and-package-metadata.xml) ## Section: Exercises [section_id: exercises] [section_url: https://zigbook.net/chapters/20__concept-primer-modules-vs-programs-vs-packages-vs-libraries#exercises] - Extend `module_role_map.zig` so the cheat sheet is driven by data loaded from a JSON manifest, then compare the ergonomics with direct Zig structs. 12 (12__config-as-data.xml), json.zig (https://github.com/ziglang/zig/tree/master/lib/std/json.zig) - Modify the overlay demo to register two external modules and emit their declaration counts, reinforcing how `--dep` queues multiple imports. - Draft a `zig build` script that wraps the overlay example, verifying that the CLI flags map cleanly to `b.addModule` and `module.addImport`. 21 (21__zig-init-and-package-metadata.xml) ## Section: Caveats, alternatives, edge cases [section_id: caveats-alternatives-edge-cases] [section_url: https://zigbook.net/chapters/20__concept-primer-modules-vs-programs-vs-packages-vs-libraries#caveats-alternatives-edge-cases] - Cross-compiling packages may expose `target`-specific modules; document conditional imports to prevent surprise name resolution failures. - If you register a module name twice in the same build graph, the zig CLI reports a collision—treat that as a signal to refactor rather than relying on ordering. 19 (19__modules-and-imports-root-builtin-discovery.xml) - Some tooling still expects the deprecated `--pkg-begin` syntax; upgrade scripts in tandem with the compiler to keep dependency registration consistent. v0.15.2 (https://ziglang.org/download/0.15.1/release-notes.html) # Chapter 21 — Zig Init & Package Metadata [chapter_id: 21__zig-init-and-package-metadata] [chapter_slug: zig-init-and-package-metadata] [chapter_number: 21] [chapter_url: https://zigbook.net/chapters/21__zig-init-and-package-metadata] ## Section: Overview [section_id: overview] [section_url: https://zigbook.net/chapters/21__zig-init-and-package-metadata#overview] Chapter 20 settled the vocabulary distinguishing modules from programs, packages, and libraries; this chapter shows how `zig init` bootstraps that vocabulary into actual files, and how `build.zig.zon` codifies package identity, version constraints, and dependency metadata so the build system and package manager can resolve imports reliably. See 20 (20__concept-primer-modules-vs-programs-vs-packages-vs-libraries.xml) and v0.15.2 (https://ziglang.org/download/0.15.1/release-notes.html). We focus on package metadata structure before diving into build graph authoring in Chapter 22, ensuring you understand what each field in `build.zig.zon` controls and why Zig’s fingerprint mechanism replaced earlier UUID-based schemes. See 22 (22__build-system-deep-dive.xml), build.zig.zon (https://github.com/ziglang/zig/blob/master/lib/init/build.zig.zon), and Build.zig (https://github.com/ziglang/zig/tree/master/lib/std/Build.zig). ## Section: Learning Goals [section_id: learning-goals] [section_url: https://zigbook.net/chapters/21__zig-init-and-package-metadata#learning-goals] - Use `zig init` and `zig init --minimal` to scaffold new projects with appropriate boilerplate for modules, executables, and tests. - Interpret every field in `build.zig.zon`: name, version, fingerprint, minimum Zig version, dependencies, and paths. - Distinguish remote dependencies (URL + hash), local dependencies (path), and lazy dependencies (deferred fetch). - Explain why fingerprints provide globally unique package identity and how they prevent hostile fork confusion. ## Section: Scaffolding projects with [section_id: zig-init-basics] [section_url: https://zigbook.net/chapters/21__zig-init-and-package-metadata#zig-init-basics] Zig 0.15.2 updated the default `zig init` template to encourage splitting reusable modules from executable entry points, addressing a common newcomer confusion where library code was unnecessarily compiled as static archives instead of being exposed as pure Zig modules. See build.zig (https://github.com/ziglang/zig/blob/master/lib/init/build.zig). ### Subsection: Default Template: Module + Executable [section_id: default-template] [section_url: https://zigbook.net/chapters/21__zig-init-and-package-metadata#default-template] Running `zig init` in an empty directory generates four files demonstrating the recommended pattern for projects that want both a reusable module and a CLI tool: ```shell $ mkdir myproject && cd myproject $ zig init info: created build.zig info: created build.zig.zon info: created src/main.zig info: created src/root.zig info: see `zig build --help` for a menu of options ``` The generated structure separates concerns: - `src/root.zig`: Reusable module exposing public API (e.g., `bufferedPrint`, `add`) - `src/main.zig`: Executable entry point importing and using the module - `build.zig`: Build graph wiring both the module and executable artifacts - `build.zig.zon`: Package metadata including name, version, and fingerprint This layout makes it trivial for external packages to depend on your module without inheriting unnecessary executable code, while still providing a convenient CLI for local development or distribution. 20 (20__concept-primer-modules-vs-programs-vs-packages-vs-libraries.xml) TIP: If you only need a module or only need an executable, delete the files you don’t need and simplify `build.zig` accordingly—the template is a starting point, not a mandate. ### Subsection: Minimal Template: Stub for Experienced Users [section_id: minimal-template] [section_url: https://zigbook.net/chapters/21__zig-init-and-package-metadata#minimal-template] For users who know the build system and want minimal boilerplate, `zig init --minimal` generates only `build.zig.zon` and a stub `build.zig`: ```shell $ mkdir minimal-project && cd minimal-project $ zig init --minimal info: successfully populated 'build.zig.zon' and 'build.zig' ``` The resulting `build.zig.zon` is compact: ```zig .{ .name = .minimal_project, .version = "0.0.1", .minimum_zig_version = "0.15.2", .paths = .{""}, .fingerprint = 0x52714d1b5f619765, } ``` The stub `build.zig` is equally terse: ```zig const std = @import("std"); pub fn build(b: *std.Build) void { _ = b; // stub } ``` This mode is intended for cases where you have a clear build strategy in mind and want to avoid deleting boilerplate comments and example code. ## Section: Anatomy of [section_id: build-zig-zon-anatomy] [section_url: https://zigbook.net/chapters/21__zig-init-and-package-metadata#build-zig-zon-anatomy] Zig Object Notation (ZON) is a strict subset of Zig syntax used for data literals; `build.zig.zon` is the canonical file the build runner parses to resolve package metadata before invoking your `build.zig` script. See zon.zig (https://github.com/ziglang/zig/tree/master/lib/std/zon.zig) and Zoir.zig (https://github.com/ziglang/zig/tree/master/lib/std/zig/Zoir.zig). ### Subsection: How ZON files are parsed [section_id: zon-parse-modes] [section_url: https://zigbook.net/chapters/21__zig-init-and-package-metadata#zon-parse-modes] From the parser’s point of view, `.zon` manifests are just another mode of `Ast.parse()`. The tokenizer is shared between `.zig` and `.zon` files, but `.zig` is parsed as a container of declarations while `.zon` is parsed as a single expression—exactly what `build.zig.zon` contains. ```text graph TD START["Ast.parse()"] --> TOKENIZE["Tokenize source"] TOKENIZE --> MODE{Mode?} MODE -->|".zig"| PARSEROOT["Parse.parseRoot()"] MODE -->|".zon"| PARSEZON["Parse.parseZon()"] PARSEROOT --> CONTAINERMEMBERS["parseContainerMembers()"] CONTAINERMEMBERS --> ROOTAST["Root AST
(container decls)"] PARSEZON --> EXPR["expectExpr()"] EXPR --> EXPRAST["Root AST
(single expression)"] ROOTAST --> ASTRETURN["Return Ast struct"] EXPRAST --> ASTRETURN ``` - Zig mode (`.zig` files): Parses a full source file as a container with declarations - ZON mode (`.zon` files): Parses a single expression (Zig Object Notation) Sources: lib/std/zig/Parse.zig:192-205, lib/std/zig/Parse.zig:208-228 ### Subsection: Required Fields [section_id: zon-required-fields] [section_url: https://zigbook.net/chapters/21__zig-init-and-package-metadata#zon-required-fields] Every `build.zig.zon` must define these core fields: ```zig .{ .name = .myproject, .version = "0.1.0", .minimum_zig_version = "0.15.2", .paths = .{""}, .fingerprint = 0xa1b2c3d4e5f60718, } ``` - `.name`: A symbol literal (e.g., `.myproject`) used as the default dependency key; conventionally lowercase, omitting redundant "zig" prefixes since the package already lives in the Zig namespace. - `.version`: A semantic version string (`"MAJOR.MINOR.PATCH"`) that the package manager will eventually use for deduplication. SemanticVersion.zig (https://github.com/ziglang/zig/tree/master/lib/std/SemanticVersion.zig) - `.minimum_zig_version`: The earliest Zig release that this package supports; older compilers will refuse to build it. - `.paths`: An array of file/directory paths (relative to the build root) included in the package’s content hash; only these files are distributed and cached. - `.fingerprint`: A 64-bit hexadecimal integer serving as the package’s globally unique identifier, generated once by the toolchain and never changed (except in hostile fork scenarios). The following demo shows how these fields map to runtime introspection patterns (though in practice the build runner handles this automatically): ```zig const std = @import("std"); pub fn main() !void { // Demonstrate parsing and introspecting build.zig.zon fields // In practice, the build runner handles this automatically const zon_example = \\.{ \\ .name = .demo, \\ .version = "0.1.0", \\ .minimum_zig_version = "0.15.2", \\ .fingerprint = 0x1234567890abcdef, \\ .paths = .{"build.zig", "src"}, \\ .dependencies = .{}, \\} ; std.debug.print("--- build.zig.zon Field Demo ---\n", .{}); std.debug.print("Sample ZON structure:\n{s}\n\n", .{zon_example}); std.debug.print("Field explanations:\n", .{}); std.debug.print(" .name: Package identifier (symbol literal)\n", .{}); std.debug.print(" .version: Semantic version string\n", .{}); std.debug.print(" .minimum_zig_version: Minimum supported Zig\n", .{}); std.debug.print(" .fingerprint: Unique package ID (hex integer)\n", .{}); std.debug.print(" .paths: Files included in package distribution\n", .{}); std.debug.print(" .dependencies: External packages required\n", .{}); std.debug.print("\nNote: Zig 0.15.2 uses .fingerprint for unique identity\n", .{}); std.debug.print(" (Previously used UUID-style identifiers)\n", .{}); } ``` Run: ```shell $ zig run zon_field_demo.zig ``` Output: ```shell === build.zig.zon Field Demo === Sample ZON structure: .{ .name = .demo, .version = "0.1.0", .minimum_zig_version = "0.15.2", .fingerprint = 0x1234567890abcdef, .paths = .{"build.zig", "src"}, .dependencies = .{}, } Field explanations: .name: Package identifier (symbol literal) .version: Semantic version string .minimum_zig_version: Minimum supported Zig .fingerprint: Unique package ID (hex integer) .paths: Files included in package distribution .dependencies: External packages required Note: Zig 0.15.2 uses .fingerprint for unique identity (Previously used UUID-style identifiers) ``` NOTE: Zig 0.15.2 replaced the old UUID-style `.id` field with the more compact `.fingerprint` field, simplifying generation and comparison while maintaining global uniqueness guarantees.See Zig 0.15.2 release notes, section "Zig Init". ### Subsection: Fingerprint: Global Identity and Fork Detection [section_id: fingerprint-identity] [section_url: https://zigbook.net/chapters/21__zig-init-and-package-metadata#fingerprint-identity] The `.fingerprint` field is the linchpin of package identity: it is generated once when you first run `zig init`, and should never change for the lifetime of the package unless you are deliberately forking it into a new identity. Changing the fingerprint of an actively maintained upstream project is considered a hostile fork—an attempt to hijack the package’s identity and redirect users to different code. Legitimate forks (where the upstream is abandoned) should regenerate the fingerprint to establish a new identity, while maintaining forks (backports, security patches) preserve the original fingerprint to signal continuity. ```zig const std = @import("std"); pub fn main() !void { std.debug.print("--- Package Identity Validation ---\n\n", .{}); // Simulate package metadata inspection const pkg_name = "mylib"; const pkg_version = "1.0.0"; const fingerprint: u64 = 0xabcdef1234567890; std.debug.print("Package: {s}\n", .{pkg_name}); std.debug.print("Version: {s}\n", .{pkg_version}); std.debug.print("Fingerprint: 0x{x}\n\n", .{fingerprint}); // Validate semantic version format const version_valid = validateSemVer(pkg_version); std.debug.print("Version format valid: {}\n", .{version_valid}); // Check fingerprint uniqueness std.debug.print("\nFingerprint ensures:\n", .{}); std.debug.print(" - Globally unique package identity\n", .{}); std.debug.print(" - Unambiguous version detection\n", .{}); std.debug.print(" - Fork detection (hostile vs. legitimate)\n", .{}); std.debug.print("\nWARNING: Changing fingerprint of a maintained project\n", .{}); std.debug.print(" is considered a hostile fork attempt!\n", .{}); } fn validateSemVer(version: []const u8) bool { // Simplified validation: check for X.Y.Z format var parts: u8 = 0; for (version) |c| { if (c == '.') parts += 1; } return parts == 2; // Must have exactly 2 dots } ``` Run: ```shell $ zig run fingerprint_demo.zig ``` Output: ```shell === Package Identity Validation === Package: mylib Version: 1.0.0 Fingerprint: 0xabcdef1234567890 Version format valid: true Fingerprint ensures: - Globally unique package identity - Unambiguous version detection - Fork detection (hostile vs. legitimate) WARNING: Changing fingerprint of a maintained project is considered a hostile fork attempt! ``` IMPORTANT: The inline comment `// Changing this has security and trust implications.` in the generated `.zon` file is deliberately preserved to surface during code review if someone modifies the fingerprint without understanding the consequences. ### Subsection: Dependencies: Remote, Local, and Lazy [section_id: dependencies-field] [section_url: https://zigbook.net/chapters/21__zig-init-and-package-metadata#dependencies-field] The `.dependencies` field is a struct literal mapping dependency names to fetch specifications; each entry is either a remote URL dependency, a local filesystem path dependency, or a lazily-fetched optional dependency. ### Subsection: Annotated Dependency Examples [section_id: annotated-dependencies] [section_url: https://zigbook.net/chapters/21__zig-init-and-package-metadata#annotated-dependencies] ```zig .{ // Package name: used as key in dependency tables // Convention: lowercase, no "zig" prefix (redundant in Zig namespace) .name = .mylib, // Semantic version for package deduplication .version = "1.2.3", // Globally unique package identifier // Generated once by toolchain, then never changes // Allows unambiguous detection of package updates .fingerprint = 0xa1b2c3d4e5f60718, // Minimum supported Zig version .minimum_zig_version = "0.15.2", // External dependencies .dependencies = .{ // Remote dependency with URL and hash .example_remote = .{ .url = "https://github.com/user/repo/archive/tag.tar.gz", // Multihash format: source of truth for package identity .hash = "1220abcdef1234567890abcdef1234567890abcdef1234567890abcdef12345678", }, // Local path dependency (no hash needed) .example_local = .{ .path = "../sibling-package", }, // Lazy dependency: only fetched if actually used .example_lazy = .{ .url = "https://example.com/optional.tar.gz", .hash = "1220fedcba0987654321fedcba0987654321fedcba0987654321fedcba098765", .lazy = true, }, }, // Files included in package hash // Only these files/directories are distributed .paths = .{ "build.zig", "build.zig.zon", "src", "LICENSE", "README.md", }, } ``` - Remote dependencies specify `.url` (a tarball/zip archive location) and `.hash` (a multihash-format content hash). The hash is the source of truth: even if the URL changes or mirrors are added, the package identity remains tied to the hash. - Local dependencies specify `.path` (a relative directory from the build root). No hash is computed because the filesystem is the authority; this is useful for monorepo layouts or during development before publishing. - Lazy dependencies add `.lazy = true` to defer fetching until the dependency is actually imported by a build script. This reduces bandwidth for optional features or platform-specific code paths. ### Subsection: Dependency Types in Practice [section_id: dependency-types-demo] [section_url: https://zigbook.net/chapters/21__zig-init-and-package-metadata#dependency-types-demo] ```zig const std = @import("std"); pub fn main() !void { std.debug.print("--- Dependency Types Comparison ---\n\n", .{}); // Demonstrate different dependency specification patterns const deps = [_]Dependency{ .{ .name = "remote_package", .kind = .{ .remote = .{ .url = "https://example.com/pkg.tar.gz", .hash = "122012345678...", } }, .lazy = false, }, .{ .name = "local_package", .kind = .{ .local = .{ .path = "../local-lib", } }, .lazy = false, }, .{ .name = "lazy_optional", .kind = .{ .remote = .{ .url = "https://example.com/opt.tar.gz", .hash = "1220abcdef...", } }, .lazy = true, }, }; for (deps, 0..) |dep, i| { std.debug.print("Dependency {d}: {s}\n", .{ i + 1, dep.name }); std.debug.print(" Type: {s}\n", .{@tagName(dep.kind)}); std.debug.print(" Lazy: {}\n", .{dep.lazy}); switch (dep.kind) { .remote => |r| { std.debug.print(" URL: {s}\n", .{r.url}); std.debug.print(" Hash: {s}\n", .{r.hash}); std.debug.print(" (Fetched from network, cached locally)\n", .{}); }, .local => |l| { std.debug.print(" Path: {s}\n", .{l.path}); std.debug.print(" (No hash needed, relative to build root)\n", .{}); }, } std.debug.print("\n", .{}); } std.debug.print("Key differences:\n", .{}); std.debug.print(" - Remote: Uses hash as source of truth\n", .{}); std.debug.print(" - Local: Direct filesystem path\n", .{}); std.debug.print(" - Lazy: Only fetched when actually imported\n", .{}); } const Dependency = struct { name: []const u8, kind: union(enum) { remote: struct { url: []const u8, hash: []const u8, }, local: struct { path: []const u8, }, }, lazy: bool, }; ``` Run: ```shell $ zig run dependency_types.zig ``` Output: ```shell === Dependency Types Comparison === Dependency 1: remote_package Type: remote Lazy: false URL: https://example.com/pkg.tar.gz Hash: 122012345678... (Fetched from network, cached locally) Dependency 2: local_package Type: local Lazy: false Path: ../local-lib (No hash needed, relative to build root) Dependency 3: lazy_optional Type: remote Lazy: true URL: https://example.com/opt.tar.gz Hash: 1220abcdef... (Fetched from network, cached locally) Key differences: - Remote: Uses hash as source of truth - Local: Direct filesystem path - Lazy: Only fetched when actually imported ``` TIP: Use local paths during active development across multiple packages in the same workspace, then switch to remote URLs with hashes when publishing for external consumers. 24 (24__zig-package-manager-deep.xml) Chapter 24 revisits these concepts in depth by walking through a package resolution pipeline that starts from `build.zig.zon`. 24 (24__zig-package-manager-deep.xml) ### Subsection: Paths: Controlling Package Distribution [section_id: paths-field] [section_url: https://zigbook.net/chapters/21__zig-init-and-package-metadata#paths-field] The `.paths` field specifies which files and directories are included when computing the package hash and distributing the package; everything not listed is excluded from the cached artifact. Typical patterns: ```zig .paths = .{ "build.zig", // Build script is always needed "build.zig.zon", // Metadata file itself "src", // Source code directory (recursive) "LICENSE", // Legal requirement "README.md", // Documentation } ``` Listing a directory includes all files within it recursively; listing the empty string `""` includes the build root itself (equivalent to listing every file individually, which is rarely desired). IMPORTANT: Exclude generated artifacts (`zig-cache/`, `zig-out/`), large assets not needed for compilation, and internal development tools from `.paths` to keep package downloads small and deterministic. ### Subsection: Under the hood: ZON files in dependency tracking [section_id: _under_the_hood_zon_files_in_dependency_tracking] [section_url: https://zigbook.net/chapters/21__zig-init-and-package-metadata#_under_the_hood_zon_files_in_dependency_tracking] The compiler’s incremental dependency tracker treats ZON files as a distinct dependee category alongside source hashes, embedded files, and declaration-based dependencies. The core storage is an `InternPool` that owns multiple maps into a shared `dep_entries` array: ```text graph TB subgraph "InternPool - Dependency Storage" SRCHASHDEPS["src_hash_deps
Map: TrackedInst.Index → DepEntry.Index"] NAVVALDEPS["nav_val_deps
Map: Nav.Index → DepEntry.Index"] NAVTYDEPS["nav_ty_deps
Map: Nav.Index → DepEntry.Index"] INTERNEDDEPS["interned_deps
Map: Index → DepEntry.Index"] ZONFILEDEPS["zon_file_deps
Map: FileIndex → DepEntry.Index"] EMBEDFILEDEPS["embed_file_deps
Map: EmbedFile.Index → DepEntry.Index"] NSDEPS["namespace_deps
Map: TrackedInst.Index → DepEntry.Index"] NSNAMEDEPS["namespace_name_deps
Map: NamespaceNameKey → DepEntry.Index"] FIRSTDEP["first_dependency
Map: AnalUnit → DepEntry.Index"] DEPENTRIES["dep_entries
ArrayListUnmanaged"] FREEDEP["free_dep_entries
ArrayListUnmanaged"] end subgraph "DepEntry Structure" DEPENTRY["DepEntry
{depender: AnalUnit,
next_dependee: DepEntry.Index.Optional,
next_depender: DepEntry.Index.Optional}"] end SRCHASHDEPS --> DEPENTRIES NAVVALDEPS --> DEPENTRIES NAVTYDEPS --> DEPENTRIES INTERNEDDEPS --> DEPENTRIES ZONFILEDEPS --> DEPENTRIES EMBEDFILEDEPS --> DEPENTRIES NSDEPS --> DEPENTRIES NSNAMEDEPS --> DEPENTRIES FIRSTDEP --> DEPENTRIES DEPENTRIES --> DEPENTRY FREEDEP -.->|"reuses indices from"| DEPENTRIES ``` The dependency tracking system uses multiple hash maps to look up dependencies by different dependee types. All maps point into a shared `dep_entries` array, which stores the actual `DepEntry` structures forming linked lists of dependencies. Sources: src/InternPool.zig:34-85 ```text graph LR subgraph "Source-Level Dependencies" SRCHASH["Source Hash
TrackedInst.Index
src_hash_deps"] ZONFILE["ZON File
FileIndex
zon_file_deps"] EMBEDFILE["Embedded File
EmbedFile.Index
embed_file_deps"] end subgraph "Nav Dependencies" NAVVAL["Nav Value
Nav.Index
nav_val_deps"] NAVTY["Nav Type
Nav.Index
nav_ty_deps"] end subgraph "Type/Value Dependencies" INTERNED["Interned Value
Index
interned_deps
runtime funcs, container types"] end subgraph "Namespace Dependencies" NSFULL["Full Namespace
TrackedInst.Index
namespace_deps"] NSNAME["Namespace Name
NamespaceNameKey
namespace_name_deps"] end subgraph "Memoized State" MEMO["Memoized Fields
panic_messages, etc."] end ``` Each category tracks a different kind of dependee: | Dependee Type | Map Name | Key Type | When Invalidated | | --- | --- | --- | --- | | Source Hash | `src_hash_deps` | `TrackedInst.Index` | ZIR instruction body changes | | Nav Value | `nav_val_deps` | `Nav.Index` | Declaration value changes | | Nav Type | `nav_ty_deps` | `Nav.Index` | Declaration type changes | | Interned Value | `interned_deps` | `Index` | Function IES changes, container type recreated | | ZON File | `zon_file_deps` | `FileIndex` | ZON file imported via `@import` changes | | Embedded File | `embed_file_deps` | `EmbedFile.Index` | File content accessed via `@embedFile` changes | | Full Namespace | `namespace_deps` | `TrackedInst.Index` | Any name added/removed in namespace | | Namespace Name | `namespace_name_deps` | `NamespaceNameKey` | Specific name existence changes | | Memoized State | `memoized_state_*_deps` | N/A (single entry) | Compiler state fields change | Sources: src/InternPool.zig:34-71 ### Subsection: Minimum Zig Version: Compatibility Bounds [section_id: minimum-zig-version] [section_url: https://zigbook.net/chapters/21__zig-init-and-package-metadata#minimum-zig-version] The `.minimum_zig_version` field declares the earliest Zig release that the package can build with; older compilers will refuse to proceed, preventing silent miscompilations due to missing features or changed semantics. When the language stabilizes at 1.0.0, this field will interact with semantic versioning to provide compatibility guarantees; before 1.0.0, it serves as a forward-looking compatibility declaration even though breaking changes happen every release. ### Subsection: Version: Semantic Versioning for Deduplication [section_id: version-field] [section_url: https://zigbook.net/chapters/21__zig-init-and-package-metadata#version-field] The `.version` field currently documents the package’s semantic version but does not yet enforce compatibility ranges or automatic deduplication; that functionality is planned for post-1.0.0 when the language stabilizes. Follow semantic versioning conventions: - MAJOR: Increment for incompatible API changes - MINOR: Increment for backward-compatible feature additions - PATCH: Increment for backward-compatible bug fixes This discipline will pay off once the package manager can auto-resolve compatible versions within dependency trees. 24 (24__zig-package-manager-deep.xml) ## Section: Practical Workflow: From Init to First Build [section_id: zig-init-workflow] [section_url: https://zigbook.net/chapters/21__zig-init-and-package-metadata#zig-init-workflow] A typical project initialization sequence looks like this: ```shell $ mkdir mylib && cd mylib $ zig init info: created build.zig info: created build.zig.zon info: created src/main.zig info: created src/root.zig $ zig build $ zig build test All 3 tests passed. $ zig build run All your codebase are belong to us. Run `zig build test` to run the tests. ``` At this point, you have: 1. A reusable module (`src/root.zig`) exposing `bufferedPrint` and `add` 2. An executable (`src/main.zig`) importing and using the module 3. Tests for both the module and executable 4. Package metadata (`build.zig.zon`) ready for publishing To share your module with other packages, you would publish the repository with a tagged release, document the URL and hash, and consumers would add it to their `.dependencies` table. ## Section: Notes & Caveats [section_id: notes-caveats] [section_url: https://zigbook.net/chapters/21__zig-init-and-package-metadata#notes-caveats] - The fingerprint is generated from a random seed; regenerating `build.zig.zon` will produce a different fingerprint unless you preserve the original. - Changing `.name` does not change the fingerprint; the name is a convenience alias while the fingerprint is the identity. - Local path dependencies bypass the hash-based content addressing entirely; they are trusted based on filesystem state at build time. - The package manager caches fetched dependencies in a global cache directory; subsequent builds with the same hash skip re-downloading. ## Section: Exercises [section_id: exercises] [section_url: https://zigbook.net/chapters/21__zig-init-and-package-metadata#exercises] - Run `zig init` in a new directory, then modify `build.zig.zon` to add a fake remote dependency with a placeholder hash; observe the error when running `zig build --fetch`. - Create two packages in sibling directories, configure one as a local path dependency of the other, and verify that changes in the dependency are immediately visible without re-fetching. - Generate a `build.zig.zon` with `zig init --minimal`, then manually add a `.dependencies` table and compare the resulting structure with the annotated example in this chapter. - Fork a hypothetical package by regenerating the fingerprint (delete the field and run `zig build`), then document in a README why this is a new identity rather than a hostile takeover. ## Section: Caveats, alternatives, edge cases [section_id: caveats-alternatives-edge-cases] [section_url: https://zigbook.net/chapters/21__zig-init-and-package-metadata#caveats-alternatives-edge-cases] - If you omit `.paths`, the package manager may include unintended files in the distribution, inflating download size and exposing internal implementation details. - Remote dependency URLs can become stale if the host moves or removes the archive; consider mirroring critical dependencies or using content-addressed storage systems. 24 (24__zig-package-manager-deep.xml) - The `zig fetch --save ` command automates adding a remote dependency to `.dependencies` by downloading, hashing, and inserting the correct entry—use it instead of hand-typing hashes. - Lazy dependencies require build script cooperation: if your `build.zig` unconditionally references a lazy dependency without checking availability, the build will fail with a "dependency not available" error. # Chapter 22 — Build System Deep Dive [chapter_id: 22__build-system-deep-dive] [chapter_slug: build-system-deep-dive] [chapter_number: 22] [chapter_url: https://zigbook.net/chapters/22__build-system-deep-dive] ## Section: Overview [section_id: overview] [section_url: https://zigbook.net/chapters/22__build-system-deep-dive#overview] Chapter 21 21 (21__zig-init-and-package-metadata.xml) showed how `build.zig.zon` declares package metadata; this chapter reveals how `build.zig` orchestrates compilation by authoring a directed acyclic graph of build steps using the `std.Build` API, which the build runner executes to produce artifacts—executables, libraries, tests, and custom transformations—while caching intermediate results and parallelizing independent work (see Build.zig (https://github.com/ziglang/zig/tree/master/lib/std/Build.zig)). Unlike `zig run` or `zig build-exe`, which compile a single entry point imperatively, `build.zig` is executable Zig code that constructs a declarative build graph: nodes represent compilation steps, edges represent dependencies, and the build runner (`zig build`) traverses the graph optimally. For release details, see v0.15.2 (https://ziglang.org/download/0.15.1/release-notes.html). ## Section: Learning Goals [section_id: learning-goals] [section_url: https://zigbook.net/chapters/22__build-system-deep-dive#learning-goals] - Distinguish `zig build` (build graph execution) from `zig run` / `zig build-exe` (direct compilation). - Use `b.standardTargetOptions()` and `b.standardOptimizeOption()` to expose user-configurable target and optimization choices. - Create modules with `b.addModule()` and `b.createModule()`, understanding when to expose modules publicly versus privately (see Module.zig (https://github.com/ziglang/zig/tree/master/lib/std/Build/Module.zig)). - Build executables with `b.addExecutable()`, libraries with `b.addLibrary()`, and wire dependencies between artifacts (see Compile.zig (https://github.com/ziglang/zig/tree/master/lib/std/Build/Step/Compile.zig)). - Integrate tests with `b.addTest()` and wire custom top-level steps with `b.step()`. - Debug build failures using `zig build -v` and interpret graph errors from missing modules or incorrect dependencies. ## Section: as Executable Zig Code [section_id: build-zig-as-code] [section_url: https://zigbook.net/chapters/22__build-system-deep-dive#build-zig-as-code] Every `build.zig` exports a `pub fn build(b: *std.Build)` function that the build runner invokes after parsing `build.zig.zon` and setting up the build graph context; within this function, you use methods on the `*std.Build` pointer to register steps, artifacts, and dependencies declaratively. 21 (21__zig-init-and-package-metadata.xml) ### Subsection: Imperative Commands vs. Declarative Graph [section_id: imperative-vs-declarative] [section_url: https://zigbook.net/chapters/22__build-system-deep-dive#imperative-vs-declarative] When you run `zig run main.zig`, the compiler immediately compiles `main.zig` and executes it—a single-shot imperative workflow. When you run `zig build`, the runner first executes `build.zig` to construct a graph of steps, then analyzes that graph to determine which steps need to run (based on cache state and dependencies), and finally executes those steps in parallel where possible. This declarative approach enables: - Incremental builds: unchanged artifacts are not recompiled - Parallel execution: independent steps run simultaneously - Reproducibility: the same graph produces the same outputs - Extensibility: custom steps integrate seamlessly build.zig template (https://github.com/ziglang/zig/blob/master/lib/init/build.zig) ### Subsection: Minimal [section_id: minimal-build] [section_url: https://zigbook.net/chapters/22__build-system-deep-dive#minimal-build] The simplest `build.zig` creates one executable and installs it: ```zig const std = @import("std"); // Minimal build.zig: single executable, no options // Demonstrates the simplest possible build script for the Zig build system. pub fn build(b: *std.Build) void { // Create an executable compilation step with minimal configuration. // This represents the fundamental pattern for producing a binary artifact. const exe = b.addExecutable(.{ // The output binary name (becomes "hello" or "hello.exe") .name = "hello", // Configure the root module with source file and compilation settings .root_module = b.createModule(.{ // Specify the entry point source file relative to build.zig .root_source_file = b.path("main.zig"), // Target the host machine (the system running the build) .target = b.graph.host, // Use Debug optimization level (no optimizations, debug symbols included) .optimize = .Debug, }), }); // Register the executable to be installed to the output directory. // When `zig build` runs, this artifact will be copied to zig-out/bin/ b.installArtifact(exe); } ``` ```zig // Entry point for a minimal Zig build system example. // This demonstrates the simplest possible Zig program structure that can be built // using the Zig build system, showing the basic main function and standard library import. const std = @import("std"); pub fn main() !void { std.debug.print("Hello from minimal build!\n", .{}); } ``` Build and run: ```shell $ zig build $ ./zig-out/bin/hello ``` Output: ```shell Hello from minimal build! ``` This example hard-codes `b.graph.host` (the machine running the build) as the target and `.Debug` optimization, so users cannot customize it. For real projects, expose these as options. IMPORTANT: The `build` function does not compile anything itself—it only registers steps in the graph. The build runner executes the graph after `build()` returns. ## Section: Standard Options Helpers [section_id: standard-options] [section_url: https://zigbook.net/chapters/22__build-system-deep-dive#standard-options] Most projects want users to control the target architecture/OS and optimization level; `std.Build` provides two helpers that expose these as CLI flags and handle defaults gracefully. ### Subsection: : Cross-Compilation Made Easy [section_id: standard-target-options] [section_url: https://zigbook.net/chapters/22__build-system-deep-dive#standard-target-options] `b.standardTargetOptions(.{})` returns a `std.Build.ResolvedTarget` that respects the `-Dtarget=` flag, allowing users to cross-compile without modifying `build.zig`: ```shell $ zig build -Dtarget=x86_64-linux # Linux x86_64 $ zig build -Dtarget=aarch64-macos # macOS ARM64 $ zig build -Dtarget=wasm32-wasi # WebAssembly WASI ``` The empty options struct `(.{})` accepts defaults; you can optionally whitelist targets or specify a fallback: ```zig const target = b.standardTargetOptions(.{ .default_target = .{ .cpu_arch = .x86_64, .os_tag = .linux }, }); ``` ### Subsection: : User-Controlled Optimization [section_id: standard-optimize-options] [section_url: https://zigbook.net/chapters/22__build-system-deep-dive#standard-optimize-options] `b.standardOptimizeOption(.{})` returns a `std.builtin.OptimizeMode` that respects the `-Doptimize=` flag, with values `.Debug`, `.ReleaseSafe`, `.ReleaseFast`, or `.ReleaseSmall`: ```shell $ zig build # Debug (default) $ zig build -Doptimize=ReleaseFast # Maximum speed $ zig build -Doptimize=ReleaseSmall # Minimum size ``` The options struct accepts a `.preferred_optimize_mode` to suggest a default when the user doesn’t specify one. If you pass no preference, the system infers from the package’s `release_mode` setting in `build.zig.zon`. 21 (21__zig-init-and-package-metadata.xml) Under the hood, the chosen `OptimizeMode` feeds into the compiler’s configuration and affects safety checks, debug information, and backend optimization levels: ```text graph TB subgraph "Optimization Mode Effects" OptMode["optimize_mode: OptimizeMode"] OptMode --> SafetyChecks["Runtime Safety Checks"] OptMode --> DebugInfo["Debug Information"] OptMode --> CodegenStrategy["Codegen Strategy"] OptMode --> LLVMOpt["LLVM Optimization Level"] SafetyChecks --> Overflow["Integer overflow checks"] SafetyChecks --> Bounds["Bounds checking"] SafetyChecks --> Null["Null pointer checks"] SafetyChecks --> Unreachable["Unreachable assertions"] DebugInfo --> StackTraces["Stack traces"] DebugInfo --> DWARF["DWARF debug info"] DebugInfo --> LineInfo["Source line information"] CodegenStrategy --> Inlining["Inline heuristics"] CodegenStrategy --> Unrolling["Loop unrolling"] CodegenStrategy --> Vectorization["SIMD vectorization"] LLVMOpt --> O0["Debug: -O0"] LLVMOpt --> O2Safe["ReleaseSafe: -O2 + safety"] LLVMOpt --> O3["ReleaseFast: -O3"] LLVMOpt --> Oz["ReleaseSmall: -Oz"] end ``` This is the same `OptimizeMode` that `b.standardOptimizeOption()` returns, so the flags you expose in `build.zig` directly determine which safety checks remain enabled and which optimization pipeline the compiler selects. ### Subsection: Complete Example with Standard Options [section_id: configurable-example] [section_url: https://zigbook.net/chapters/22__build-system-deep-dive#configurable-example] ```zig const std = @import("std"); // Demonstrating standardTargetOptions and standardOptimizeOption pub fn build(b: *std.Build) void { // Allows user to choose target: zig build -Dtarget=x86_64-linux const target = b.standardTargetOptions(.{}); // Allows user to choose optimization: zig build -Doptimize=ReleaseFast const optimize = b.standardOptimizeOption(.{}); const exe = b.addExecutable(.{ .name = "configurable", .root_module = b.createModule(.{ .root_source_file = b.path("main.zig"), .target = target, .optimize = optimize, }), }); b.installArtifact(exe); // Add run step const run_cmd = b.addRunArtifact(exe); run_cmd.step.dependOn(b.getInstallStep()); const run_step = b.step("run", "Run the application"); run_step.dependOn(&run_cmd.step); } ``` ```zig // This program demonstrates how to access and display Zig's built-in compilation // information through the `builtin` module. It's used in the zigbook to teach // readers about build system introspection and standard options. // Import the standard library for debug printing capabilities const std = @import("std"); // Import builtin module to access compile-time information about the target // platform, CPU architecture, and optimization mode const builtin = @import("builtin"); // Main entry point that prints compilation target information // Returns an error union to handle potential I/O failures from debug.print pub fn main() !void { // Print the target architecture (e.g., x86_64, aarch64) and operating system // (e.g., linux, windows) by extracting tag names from the builtin constants std.debug.print("Target: {s}-{s}\n", .{ @tagName(builtin.cpu.arch), @tagName(builtin.os.tag), }); // Print the optimization mode (Debug, ReleaseSafe, ReleaseFast, or ReleaseSmall) // that was specified during compilation std.debug.print("Optimize: {s}\n", .{@tagName(builtin.mode)}); } ``` Build and run with options: ```shell $ zig build -Dtarget=x86_64-linux -Doptimize=ReleaseFast run ``` Output (example): ```text Target: x86_64-linux Optimize: ReleaseFast ``` TIP: Always use `standardTargetOptions()` and `standardOptimizeOption()` unless you have a very specific reason to hard-code values (e.g., firmware targeting a fixed embedded system). ## Section: Modules: Public and Private [section_id: modules-and-imports] [section_url: https://zigbook.net/chapters/22__build-system-deep-dive#modules-and-imports] Zig 0.15.2 distinguishes public modules (exposed to consumers via `b.addModule()`) from private modules (internal to the current package, created with `b.createModule()`). Public modules appear in downstream `build.zig` files via `b.dependency()`, while private modules exist only within your build graph. ### Subsection: vs. [section_id: add-module-vs-create-module] [section_url: https://zigbook.net/chapters/22__build-system-deep-dive#add-module-vs-create-module] - `b.addModule(name, options)` creates a module and registers it in the package’s public module table, making it available to consumers who depend on this package. - `b.createModule(options)` creates a module without exposing it; useful for executable-specific code or internal helpers. Both functions return a `*std.Build.Module`, which you wire into compilation steps via the `.imports` field. ### Subsection: Example: Public Module and Executable [section_id: module-example] [section_url: https://zigbook.net/chapters/22__build-system-deep-dive#module-example] ```zig const std = @import("std"); // Demonstrating module creation and imports pub fn build(b: *std.Build) void { const target = b.standardTargetOptions(.{}); const optimize = b.standardOptimizeOption(.{}); // Create a reusable module (public) const math_mod = b.addModule("math", .{ .root_source_file = b.path("math.zig"), .target = target, }); // Create the executable with import of the module const exe = b.addExecutable(.{ .name = "calculator", .root_module = b.createModule(.{ .root_source_file = b.path("main.zig"), .target = target, .optimize = optimize, .imports = &.{ .{ .name = "math", .module = math_mod }, }, }), }); b.installArtifact(exe); const run_step = b.step("run", "Run the calculator"); const run_cmd = b.addRunArtifact(exe); run_step.dependOn(&run_cmd.step); } ``` ```zig // This module provides basic arithmetic operations for the zigbook build system examples. // It demonstrates how to create a reusable module that can be imported by other Zig files. /// Adds two 32-bit signed integers and returns their sum. /// This function is marked pub to be accessible from other modules that import this file. pub fn add(a: i32, b: i32) i32 { return a + b; } /// Multiplies two 32-bit signed integers and returns their product. /// This function is marked pub to be accessible from other modules that import this file. pub fn multiply(a: i32, b: i32) i32 { return a * b; } ``` ```zig // This program demonstrates how to use custom modules in Zig's build system. // It imports a local "math" module and uses its functions to perform basic arithmetic operations. // Import the standard library for debug printing capabilities const std = @import("std"); // Import the custom math module which provides arithmetic operations const math = @import("math"); // Main entry point demonstrating module usage with basic arithmetic pub fn main() !void { // Define two constant operands for demonstration const a = 10; const b = 20; // Print the result of addition using the imported math module std.debug.print("{d} + {d} = {d}\n", .{ a, b, math.add(a, b) }); // Print the result of multiplication using the imported math module std.debug.print("{d} * {d} = {d}\n", .{ a, b, math.multiply(a, b) }); } ``` Build and run: ```shell $ zig build run ``` Output: ```shell 10 + 20 = 30 10 * 20 = 200 ``` Here `math` is a public module (consumers of this package can `@import("math")`), while the executable’s root module is private (created with `createModule`). NOTE: The `.imports` field in `Module.CreateOptions` is a slice of `.{ .name = …​, .module = …​ }` pairs, allowing you to map arbitrary import names to module pointers—useful for avoiding name collisions when consuming multiple packages. ## Section: Artifacts: Executables, Libraries, Objects [section_id: artifacts] [section_url: https://zigbook.net/chapters/22__build-system-deep-dive#artifacts] An artifact is a compile step that produces a binary output: an executable, a static or dynamic library, or an object file. The `std.Build` API provides `addExecutable()`, `addLibrary()`, and `addObject()` functions that return `*Step.Compile` pointers. ### Subsection: : Building Programs [section_id: add-executable] [section_url: https://zigbook.net/chapters/22__build-system-deep-dive#add-executable] `b.addExecutable(.{ .name = …​, .root_module = …​ })` creates a `Step.Compile` that links a `main` function (or `_start` for freestanding) into an executable: ```zig const exe = b.addExecutable(.{ .name = "myapp", .root_module = b.createModule(.{ .root_source_file = b.path("src/main.zig"), .target = target, .optimize = optimize, }), }); b.installArtifact(exe); ``` - `.name`: The output filename (e.g., `myapp.exe` on Windows, `myapp` on Unix). - `.root_module`: The module containing the entry point. - Optional: `.version`, `.linkage` (for PIE), `.max_rss`, `.use_llvm`, `.use_lld`, `.zig_lib_dir`. ### Subsection: : Static and Dynamic Libraries [section_id: add-library] [section_url: https://zigbook.net/chapters/22__build-system-deep-dive#add-library] `b.addLibrary(.{ .name = …​, .root_module = …​, . linkage = …​ })` creates a library: ```zig const lib = b.addLibrary(.{ .name = "utils", .root_module = b.createModule(.{ .root_source_file = b.path("utils.zig"), .target = target, .optimize = optimize, }), . linkage = .static, // or .dynamic .version = .{ .major = 1, .minor = 0, .patch = 0 }, }); b.installArtifact(lib); ``` - `.linkage = .static` produces a `.a` (Unix) or `.lib` (Windows) archive. - `.linkage = .dynamic` produces a `.so` (Unix), `.dylib` (macOS), or `.dll` (Windows) shared library. - `.version`: Semantic version embedded in the library metadata (Unix only). ### Subsection: Linking Libraries to Executables [section_id: linking-libraries] [section_url: https://zigbook.net/chapters/22__build-system-deep-dive#linking-libraries] To link a library into an executable, call `exe.linkLibrary(lib)` after creating both artifacts: ```zig const std = @import("std"); // Demonstrating library creation pub fn build(b: *std.Build) void { const target = b.standardTargetOptions(.{}); const optimize = b.standardOptimizeOption(.{}); // Create a static library const lib = b.addLibrary(.{ .name = "utils", .root_module = b.createModule(.{ .root_source_file = b.path("utils.zig"), .target = target, .optimize = optimize, }), .linkage = .static, .version = .{ .major = 1, .minor = 0, .patch = 0 }, }); b.installArtifact(lib); // Create an executable that links the library const exe = b.addExecutable(.{ .name = "demo", .root_module = b.createModule(.{ .root_source_file = b.path("main.zig"), .target = target, .optimize = optimize, }), }); exe.linkLibrary(lib); b.installArtifact(exe); const run_step = b.step("run", "Run the demo"); const run_cmd = b.addRunArtifact(exe); run_step.dependOn(&run_cmd.step); } ``` ```zig //! Utility module demonstrating exported functions and formatted output. //! This module is part of the build system deep dive chapter, showing how to create //! library functions that can be exported and used across different build artifacts. const std = @import("std"); /// Doubles the input integer value. /// This function is exported and can be called from C or other languages. /// Uses the `export` keyword to make it available in the compiled library. export fn util_double(x: i32) i32 { return x * 2; } /// Squares the input integer value. /// This function is exported and can be called from C or other languages. /// Uses the `export` keyword to make it available in the compiled library. export fn util_square(x: i32) i32 { return x * x; } /// Formats a message with an integer value into the provided buffer. /// This is a public Zig function (not exported) that demonstrates buffer-based formatting. /// /// Returns a slice of the buffer containing the formatted message, or an error if /// the buffer is too small to hold the formatted output. pub fn formatMessage(buf: []u8, value: i32) ![]const u8 { return std.fmt.bufPrint(buf, "Value: {d}", .{value}); } ``` ```zig // Import the standard library for printing capabilities const std = @import("std"); // External function declaration: doubles the input integer // This function is defined in a separate library/object file extern fn util_double(x: i32) i32; // External function declaration: squares the input integer // This function is defined in a separate library/object file extern fn util_square(x: i32) i32; // Main entry point demonstrating library linking // Calls external utility functions to show build system integration pub fn main() !void { // Test value for demonstrating the external functions const x: i32 = 7; // Print the result of doubling x using the external function std.debug.print("double({d}) = {d}\n", .{ x, util_double(x) }); // Print the result of squaring x using the external function std.debug.print("square({d}) = {d}\n", .{ x, util_square(x) }); } ``` Build and run: ```shell $ zig build run ``` Output: ```shell double(7) = 14 square(7) = 49 ``` IMPORTANT: When linking a Zig library, symbols must be `export`ed (for C ABI) or you must use module imports—Zig does not have a linker-level "public Zig API" concept distinct from module exports. ### Subsection: Installing Artifacts: [section_id: install-artifact] [section_url: https://zigbook.net/chapters/22__build-system-deep-dive#install-artifact] `b.installArtifact(exe)` adds a dependency on the default install step (`zig build` with no arguments) that copies the artifact to `zig-out/bin/` (executables) or `zig-out/lib/` (libraries). You can customize the install directory or skip installation entirely if the artifact is intermediate-only. ## Section: Tests and Test Steps [section_id: tests-and-test-steps] [section_url: https://zigbook.net/chapters/22__build-system-deep-dive#tests-and-test-steps] Zig’s test blocks integrate directly into the build system: `b.addTest(.{ .root_module = …​ })` creates a special executable that runs all `test` blocks in the given module, reporting pass/fail to the build runner. 13 (13__testing-and-leak-detection.xml) ### Subsection: : Compiling Test Executables [section_id: add-test] [section_url: https://zigbook.net/chapters/22__build-system-deep-dive#add-test] ```zig const lib_tests = b.addTest(.{ .root_module = lib_mod, }); const run_lib_tests = b.addRunArtifact(lib_tests); const test_step = b.step("test", "Run library tests"); test_step.dependOn(&run_lib_tests.step); ``` `b.addTest()` returns a `*Step.Compile` just like `addExecutable()`, but it compiles the module in test mode, linking the test runner and enabling test-only code paths. ### Subsection: Complete Test Integration Example [section_id: test-example] [section_url: https://zigbook.net/chapters/22__build-system-deep-dive#test-example] ```zig const std = @import("std"); // Demonstrating test integration pub fn build(b: *std.Build) void { const target = b.standardTargetOptions(.{}); const optimize = b.standardOptimizeOption(.{}); const lib_mod = b.addModule("mylib", .{ .root_source_file = b.path("lib.zig"), .target = target, }); // Create tests for the library module const lib_tests = b.addTest(.{ .root_module = lib_mod, }); const run_lib_tests = b.addRunArtifact(lib_tests); // Create a test step const test_step = b.step("test", "Run library tests"); test_step.dependOn(&run_lib_tests.step); // Also create an executable that uses the library const exe = b.addExecutable(.{ .name = "app", .root_module = b.createModule(.{ .root_source_file = b.path("main.zig"), .target = target, .optimize = optimize, .imports = &.{ .{ .name = "mylib", .module = lib_mod }, }, }), }); b.installArtifact(exe); } ``` ```zig /// Computes the factorial of a non-negative integer using recursion. /// The factorial of n (denoted as n!) is the product of all positive integers less than or equal to n. /// Base case: factorial(0) = factorial(1) = 1 /// Recursive case: factorial(n) = n * factorial(n-1) pub fn factorial(n: u32) u32 { // Base case: 0! and 1! both equal 1 if (n <= 1) return 1; // Recursive case: multiply n by factorial of (n-1) return n * factorial(n - 1); } // Test: Verify that the factorial of 0 returns 1 (base case) test "factorial of 0 is 1" { const std = @import("std"); try std.testing.expectEqual(@as(u32, 1), factorial(0)); } // Test: Verify that the factorial of 5 returns 120 (5! = 5*4*3*2*1 = 120) test "factorial of 5 is 120" { const std = @import("std"); try std.testing.expectEqual(@as(u32, 120), factorial(5)); } // Test: Verify that the factorial of 1 returns 1 (base case) test "factorial of 1 is 1" { const std = @import("std"); try std.testing.expectEqual(@as(u32, 1), factorial(1)); } ``` ```zig // Main entry point demonstrating the factorial function from mylib. // This example shows how to: // - Import and use custom library modules // - Call library functions with different input values // - Display computed results using debug printing const std = @import("std"); const mylib = @import("mylib"); pub fn main() !void { std.debug.print("5! = {d}\n", .{mylib.factorial(5)}); std.debug.print("10! = {d}\n", .{mylib.factorial(10)}); } ``` Run tests: ```shell $ zig build test ``` Output (success): ```text All 3 tests passed. ``` TIP: Create separate test steps for each module to isolate failures and enable parallel test execution. To see how this scales up in a large codebase, the Zig compiler’s own wires many specialized test steps into a single umbrella step: ```text graph TB subgraph "Test Steps" TEST_STEP["test step
(umbrella step)"] FMT["test-fmt
Format checking"] CASES["test-cases
Compiler test cases"] MODULES["test-modules
Per-target module tests"] UNIT["test-unit
Compiler unit tests"] STANDALONE["Standalone tests"] CLI["CLI tests"] STACK_TRACE["Stack trace tests"] ERROR_TRACE["Error trace tests"] LINK["Link tests"] C_ABI["C ABI tests"] INCREMENTAL["test-incremental
Incremental compilation"] end subgraph "Module Tests" BEHAVIOR["behavior tests
test/behavior.zig"] COMPILER_RT["compiler_rt tests
lib/compiler_rt.zig"] ZIGC["zigc tests
lib/c.zig"] STD["std tests
lib/std/std.zig"] LIBC_TESTS["libc tests"] end subgraph "Test Configuration" TARGET_MATRIX["test_targets array
Different architectures
Different OSes
Different ABIs"] OPT_MODES["Optimization modes:
Debug, ReleaseFast
ReleaseSafe, ReleaseSmall"] FILTERS["test-filter
test-target-filter"] end TEST_STEP --> FMT TEST_STEP --> CASES TEST_STEP --> MODULES TEST_STEP --> UNIT TEST_STEP --> STANDALONE TEST_STEP --> CLI TEST_STEP --> STACK_TRACE TEST_STEP --> ERROR_TRACE TEST_STEP --> LINK TEST_STEP --> C_ABI TEST_STEP --> INCREMENTAL MODULES --> BEHAVIOR MODULES --> COMPILER_RT MODULES --> ZIGC MODULES --> STD TARGET_MATRIX --> MODULES OPT_MODES --> MODULES FILTERS --> MODULES ``` Your own projects can borrow this pattern: one high-level `test` step that fans out to format checks, unit tests, integration tests, and cross-target test matrices, all wired together using the same `b.step` and `b.addTest` primitives. ## Section: Top-Level Steps: Custom Build Commands [section_id: top-level-steps] [section_url: https://zigbook.net/chapters/22__build-system-deep-dive#top-level-steps] A top-level step is a named entry point that users invoke with `zig build `. You create them with `b.step(name, description)` and wire dependencies using `step.dependOn(other_step)`. ### Subsection: Creating a Step [section_id: step-example] [section_url: https://zigbook.net/chapters/22__build-system-deep-dive#step-example] ```zig const run_step = b.step("run", "Run the application"); const run_cmd = b.addRunArtifact(exe); run_cmd.step.dependOn(b.getInstallStep()); run_step.dependOn(&run_cmd.step); ``` - `b.step("run", …​)` creates the top-level step users invoke. - `b.addRunArtifact(exe)` creates a step that executes the compiled binary. - `run_cmd.step.dependOn(b.getInstallStep())` ensures the binary is installed before running it. - `run_step.dependOn(&run_cmd.step)` links the top-level step to the run command. This pattern appears in almost every `zig init`-generated `build.zig`. In the Zig compiler’s own , the default install and test steps form a larger dependency graph: ```text graph TB subgraph "Installation Step (default)" INSTALL["b.getInstallStep()"] end subgraph "Compiler Artifacts" EXE_STEP["exe.step
(compile compiler)"] INSTALL_EXE["install_exe.step
(install binary)"] end subgraph "Documentation" LANGREF["generateLangRef()"] INSTALL_LANGREF["install_langref.step"] STD_DOCS_GEN["autodoc_test"] INSTALL_STD_DOCS["install_std_docs.step"] end subgraph "Library Files" LIB_FILES["installDirectory(lib/)"] end subgraph "Test Steps" TEST["test step"] FMT["test-fmt step"] CASES["test-cases step"] MODULES["test-modules step"] end INSTALL --> INSTALL_EXE INSTALL --> INSTALL_LANGREF INSTALL --> LIB_FILES INSTALL_EXE --> EXE_STEP INSTALL_LANGREF --> LANGREF INSTALL --> INSTALL_STD_DOCS INSTALL_STD_DOCS --> STD_DOCS_GEN TEST --> EXE_STEP TEST --> FMT TEST --> CASES TEST --> MODULES CASES --> EXE_STEP MODULES --> EXE_STEP ``` Running `zig build` (with no explicit step) typically executes a default install step like this, while `zig build test` executes a dedicated test step that depends on the same core compile actions. To place this chapter in the wider Zig toolchain, the compiler’s own bootstrap process uses CMake to produce an intermediate executable, then invokes on its native script: ```text graph TB subgraph "CMake Stage (stage2)" CMAKE["CMake"] ZIG2_C["zig2.c
(generated C code)"] ZIGCPP["zigcpp
(C++ LLVM/Clang wrapper)"] ZIG2["zig2 executable"] CMAKE --> ZIG2_C CMAKE --> ZIGCPP ZIG2_C --> ZIG2 ZIGCPP --> ZIG2 end subgraph "Native Build System (stage3)" BUILD_ZIG["build.zig
Native Build Script"] BUILD_FN["build() function"] COMPILER_STEP["addCompilerStep()"] EXE["std.Build.Step.Compile
(compiler executable)"] INSTALL["Installation Steps"] BUILD_ZIG --> BUILD_FN BUILD_FN --> COMPILER_STEP COMPILER_STEP --> EXE EXE --> INSTALL end subgraph "Build Arguments" ZIG_BUILD_ARGS["ZIG_BUILD_ARGS
--zig-lib-dir
-Dversion-string
-Dtarget
-Denable-llvm
-Doptimize"] end ZIG2 -->|"zig2 build"| BUILD_ZIG ZIG_BUILD_ARGS --> BUILD_FN subgraph "Output" STAGE3_BIN["stage3/bin/zig"] STD_LIB["stage3/lib/zig/std/"] LANGREF["stage3/doc/langref.html"] end INSTALL --> STAGE3_BIN INSTALL --> STD_LIB INSTALL --> LANGREF ``` In other words, the same APIs you use for application projects also drive the self-hosted Zig compiler build. ## Section: Custom Build Options [section_id: custom-options] [section_url: https://zigbook.net/chapters/22__build-system-deep-dive#custom-options] Beyond `standardTargetOptions()` and `standardOptimizeOption()`, you can define arbitrary user-facing flags with `b.option()` and expose them to Zig source code via `b.addOptions()` (see Options.zig (https://github.com/ziglang/zig/tree/master/lib/std/Build/Step/Options.zig)). ### Subsection: : CLI Flags [section_id: option-api] [section_url: https://zigbook.net/chapters/22__build-system-deep-dive#option-api] `b.option(T, name, description)` registers a user-facing flag and returns `?T` (null if the user didn’t provide it): ```zig const enable_logging = b.option(bool, "enable-logging", "Enable debug logging") orelse false; const app_name = b.option([]const u8, "app-name", "Application name") orelse "MyApp"; ``` Users pass values via `-Dname=value`: ```shell $ zig build -Denable-logging -Dapp-name=CustomName run ``` ### Subsection: : Passing Config to Code [section_id: add-options] [section_url: https://zigbook.net/chapters/22__build-system-deep-dive#add-options] `b.addOptions()` creates a step that generates a Zig source file from key-value pairs, which you then import as a module: ```zig const std = @import("std"); // Demonstrating custom build options pub fn build(b: *std.Build) void { const target = b.standardTargetOptions(.{}); const optimize = b.standardOptimizeOption(.{}); // Custom boolean option const enable_logging = b.option( bool, "enable-logging", "Enable debug logging", ) orelse false; // Custom string option const app_name = b.option( []const u8, "app-name", "Application name", ) orelse "MyApp"; // Create options module to pass config to code const config = b.addOptions(); config.addOption(bool, "enable_logging", enable_logging); config.addOption([]const u8, "app_name", app_name); const config_module = config.createModule(); const exe = b.addExecutable(.{ .name = "configapp", .root_module = b.createModule(.{ .root_source_file = b.path("main.zig"), .target = target, .optimize = optimize, .imports = &.{ .{ .name = "config", .module = config_module }, }, }), }); b.installArtifact(exe); const run_step = b.step("run", "Run the app"); const run_cmd = b.addRunArtifact(exe); run_step.dependOn(&run_cmd.step); } ``` ```zig // Import standard library for debug printing functionality const std = @import("std"); // Import build-time configuration options defined in build.zig const config = @import("config"); /// Entry point of the application demonstrating the use of build options. /// This function showcases how to access and use configuration values that /// are set during the build process through the Zig build system. pub fn main() !void { // Display the application name from build configuration std.debug.print("Application: {s}\n", .{config.app_name}); // Display the logging toggle status from build configuration std.debug.print("Logging enabled: {}\n", .{config.enable_logging}); // Conditionally execute debug logging based on build-time configuration // This demonstrates compile-time branching using build options if (config.enable_logging) { std.debug.print("[DEBUG] This is a debug message\n", .{}); } } ``` Build and run with custom options: ```shell $ zig build run -Denable-logging -Dapp-name=TestApp ``` Output: ```shell Application: TestApp Logging enabled: true [DEBUG] This is a debug message ``` This pattern avoids the need for environment variables or runtime config files when build-time constants suffice. The Zig compiler itself uses the same approach: command-line `-D` options are parsed with `b.option()`, collected into an options step with `b.addOptions()`, and then imported as a `build_options` module that regular Zig code can read. ```text graph LR subgraph "Command Line" CLI["-Ddebug-allocator
-Denable-llvm
-Dversion-string
etc."] end subgraph "build.zig" PARSE["b.option()
Parse options"] OPTIONS["exe_options =
b.addOptions()"] ADD["exe_options.addOption()"] PARSE --> OPTIONS OPTIONS --> ADD end subgraph "Generated Module" BUILD_OPTIONS["build_options
(auto-generated)"] CONSTANTS["pub const mem_leak_frames = 4;
pub const have_llvm = true;
pub const version = '0.16.0';
etc."] BUILD_OPTIONS --> CONSTANTS end subgraph "Compiler Source" IMPORT["@import('build_options')"] USE["if (build_options.have_llvm) { ... }"] IMPORT --> USE end CLI --> PARSE ADD --> BUILD_OPTIONS BUILD_OPTIONS --> IMPORT ``` Treat `b.addOptions()` as a structured, type-checked configuration channel from your `zig build` command line into ordinary Zig modules, just as the compiler does for its own `build_options` module. ## Section: Debugging Build Failures [section_id: debugging-builds] [section_url: https://zigbook.net/chapters/22__build-system-deep-dive#debugging-builds] When `zig build` fails, the error message usually points to a missing module, incorrect dependency, or misconfigured step. The `-v` flag enables verbose output showing all compiler invocations. ### Subsection: : Inspecting Compiler Invocations [section_id: verbose-flag] [section_url: https://zigbook.net/chapters/22__build-system-deep-dive#verbose-flag] ```shell $ zig build -v zig build-exe /path/to/main.zig -target x86_64-linux -O Debug -femit-bin=zig-cache/... zig build-lib /path/to/lib.zig -target x86_64-linux -O Debug -femit-bin=zig-cache/... ... ``` This reveals the exact `zig` subcommands the build runner executes, helping diagnose flag issues or missing files. ### Subsection: Common Graph Errors [section_id: common-errors] [section_url: https://zigbook.net/chapters/22__build-system-deep-dive#common-errors] - "module 'foo' not found": The `.imports` table doesn’t include a module named `foo`, or a dependency wasn’t wired correctly. - "circular dependency detected": Two steps depend on each other transitively—build graphs must be acyclic. - "file not found: src/main.zig": The path passed to `b.path()` doesn’t exist relative to the build root. - "no member named 'root_source_file' in ExecutableOptions": You’re using Zig 0.15.2 syntax with an older compiler, or vice versa. ## Section: Notes & Caveats [section_id: notes-caveats] [section_url: https://zigbook.net/chapters/22__build-system-deep-dive#notes-caveats] - The build runner caches artifact hashes in `zig-cache/`; deleting this directory forces a full rebuild. - Passing `--` after `zig build run` forwards arguments to the executed binary: `zig build run — --help`. - `b.installArtifact()` is the canonical way to expose outputs; avoid manual file copying unless you have a specific need. - The default install step (`zig build` with no arguments) installs all artifacts registered with `installArtifact()`—if you want a no-op default, don’t install anything. ## Section: Exercises [section_id: exercises] [section_url: https://zigbook.net/chapters/22__build-system-deep-dive#exercises] - Modify the minimal example to hard-code a cross-compilation target (e.g., `wasm32-wasi`) and verify the output format with `file zig-out/bin/hello`. 43 (41__cross-compilation-and-wasm.xml) - Extend the modules example to create a second module `utils` that `math` imports, demonstrating transitive dependencies. - Add a custom option `-Dmax-threads=N` to the options example and use it to initialize a compile-time constant thread pool size. - Create a library with both static and dynamic linkage modes, install both, and inspect the output files to see the size difference. ## Section: Caveats, Alternatives, Edge Cases [section_id: caveats-alternatives-edge-cases] [section_url: https://zigbook.net/chapters/22__build-system-deep-dive#caveats-alternatives-edge-cases] - Zig 0.14.0 introduced the `root_module` field; older code using `root_source_file` directly on `ExecutableOptions` will fail on Zig 0.15.2. - Some projects still use `--pkg-begin`/`--pkg-end` flags manually instead of the module system—these are deprecated and should be migrated to `Module.addImport()`. 20 (20__concept-primer-modules-vs-programs-vs-packages-vs-libraries.xml) - The build runner does not support incremental compilation of `build.zig` itself—changing `build.zig` triggers a full graph re-evaluation. - If you see "userland" mentioned in documentation, it means the build system is implemented entirely in Zig standard library code, not compiler magic—you can read `std.Build` source to understand any behavior. # Chapter 23 — Project [chapter_id: 23__project-library-and-executable-workspace] [chapter_slug: project-library-and-executable-workspace] [chapter_number: 23] [chapter_url: https://zigbook.net/chapters/23__project-library-and-executable-workspace] ## Section: Overview [section_id: overview] [section_url: https://zigbook.net/chapters/23__project-library-and-executable-workspace#overview] Chapter 22 taught the `std.Build` API mechanics; this chapter consolidates that knowledge through a complete project: TextKit, a text processing library paired with a CLI tool that demonstrates real-world patterns for structuring workspaces, organizing modules, linking artifacts, integrating tests, and creating custom build steps. See 22 (22__build-system-deep-dive.xml) and Build.zig (https://github.com/ziglang/zig/tree/master/lib/std/Build.zig). By walking through TextKit’s implementation—from module organization to build script orchestration—you will understand how professional Zig projects separate concerns between reusable libraries and application-specific executables, while maintaining a single unified build graph that handles compilation, testing, and distribution. See 21 (21__zig-init-and-package-metadata.xml) and Compile.zig (https://github.com/ziglang/zig/tree/master/lib/std/Build/Step/Compile.zig). ## Section: Learning Goals [section_id: learning-goals] [section_url: https://zigbook.net/chapters/23__project-library-and-executable-workspace#learning-goals] - Structure a workspace with both library and executable artifacts sharing a common `build.zig`. - Organize library code into multiple modules for maintainability and testability. 20 (20__concept-primer-modules-vs-programs-vs-packages-vs-libraries.xml) - Build a static library with `b.addLibrary()` and install it for external consumption. - Create an executable that imports and uses the library module. 22 (22__build-system-deep-dive.xml) - Integrate comprehensive tests for both library and executable components. 13 (13__testing-and-leak-detection.xml) - Define custom build steps beyond the default install, run, and test targets. - Understand the contrast between `zig build` (graph-based) and `zig build-exe` (imperative). ## Section: Project Structure: TextKit [section_id: project-overview] [section_url: https://zigbook.net/chapters/23__project-library-and-executable-workspace#project-overview] TextKit is a text processing utility consisting of: - Library (): Reusable text processing functions exposed as a module - Executable (): Command-line interface consuming the library - Tests: Comprehensive coverage for library functionality - Custom Steps: Demonstration commands beyond standard build/test/run ### Subsection: Directory Layout [section_id: directory-layout] [section_url: https://zigbook.net/chapters/23__project-library-and-executable-workspace#directory-layout] ```text textkit/ ├── build.zig # Build graph definition ├── build.zig.zon # Package metadata ├── sample.txt # Demo input file └── src/ ├── textkit.zig # Library root (public API) ├── string_utils.zig # String manipulation utilities ├── text_stats.zig # Text analysis functions └── main.zig # CLI executable entry point ``` This layout follows Zig conventions: `src/` contains all source files, `build.zig` orchestrates compilation, and `build.zig.zon` declares package identity. See 21 (21__zig-init-and-package-metadata.xml) and init templates (https://github.com/ziglang/zig/blob/master/lib/init/). ## Section: Library Implementation [section_id: library-implementation] [section_url: https://zigbook.net/chapters/23__project-library-and-executable-workspace#library-implementation] The TextKit library exposes two primary modules: `StringUtils` for character-level operations and `TextStats` for document analysis. See Module.zig (https://github.com/ziglang/zig/tree/master/lib/std/Build/Module.zig). ### Subsection: String Utilities Module [section_id: string-utils-module] [section_url: https://zigbook.net/chapters/23__project-library-and-executable-workspace#string-utils-module] ```zig // Import the standard library for testing utilities const std = @import("std"); /// String utilities for text processing pub const StringUtils = struct { /// Count occurrences of a character in a string /// Returns the total number of times the specified character appears pub fn countChar(text: []const u8, char: u8) usize { var count: usize = 0; // Iterate through each character in the text for (text) |c| { // Increment counter when matching character is found if (c == char) count += 1; } return count; } /// Check if string contains only ASCII characters /// ASCII characters have values from 0-127 pub fn isAscii(text: []const u8) bool { for (text) |c| { // Any character with value > 127 is non-ASCII if (c > 127) return false; } return true; } /// Reverse a string in place /// Modifies the input buffer directly using two-pointer technique pub fn reverse(text: []u8) void { // Early return for empty strings if (text.len == 0) return; var left: usize = 0; var right: usize = text.len - 1; // Swap characters from both ends moving towards the center while (left < right) { const temp = text[left]; text[left] = text[right]; text[right] = temp; left += 1; right -= 1; } } }; // Test suite verifying countChar functionality with various inputs test "countChar counts occurrences" { const text = "hello world"; // Verify counting of 'l' character (appears 3 times) try std.testing.expectEqual(@as(usize, 3), StringUtils.countChar(text, 'l')); // Verify counting of 'o' character (appears 2 times) try std.testing.expectEqual(@as(usize, 2), StringUtils.countChar(text, 'o')); // Verify counting returns 0 for non-existent character try std.testing.expectEqual(@as(usize, 0), StringUtils.countChar(text, 'x')); } // Test suite verifying ASCII detection for different character sets test "isAscii detects ASCII strings" { // Standard ASCII letters should return true try std.testing.expect(StringUtils.isAscii("hello")); // ASCII digits should return true try std.testing.expect(StringUtils.isAscii("123")); // String with non-ASCII character (é = 233) should return false try std.testing.expect(!StringUtils.isAscii("héllo")); } // Test suite verifying in-place string reversal test "reverse reverses string" { // Create a mutable buffer to test in-place reversal var buffer = [_]u8{ 'h', 'e', 'l', 'l', 'o' }; StringUtils.reverse(&buffer); // Verify the buffer contents are reversed try std.testing.expectEqualSlices(u8, "olleh", &buffer); } ``` This module demonstrates: - Struct-based organization: Static methods grouped under `StringUtils` - Inline tests: Each function paired with its test cases for locality - Simple algorithms: Character counting, ASCII validation, in-place reversal ### Subsection: Text Statistics Module [section_id: text-stats-module] [section_url: https://zigbook.net/chapters/23__project-library-and-executable-workspace#text-stats-module] ```zig const std = @import("std"); /// Text statistics and analysis structure /// Provides functionality to analyze text content and compute various metrics /// such as word count, line count, and character count. pub const TextStats = struct { /// Total number of words found in the analyzed text word_count: usize, /// Total number of lines in the analyzed text line_count: usize, /// Total number of characters in the analyzed text char_count: usize, /// Analyze text and compute statistics /// Iterates through the input text to count words, lines, and characters. /// Words are defined as sequences of non-whitespace characters separated by whitespace. /// Lines are counted based on newline characters, with special handling for text /// that doesn't end with a newline. pub fn analyze(text: []const u8) TextStats { var stats = TextStats{ .word_count = 0, .line_count = 0, .char_count = text.len, }; // Track whether we're currently inside a word to avoid counting multiple // consecutive whitespace characters as separate word boundaries var in_word = false; for (text) |c| { if (c == '\n') { stats.line_count += 1; in_word = false; } else if (std.ascii.isWhitespace(c)) { // Whitespace marks the end of a word in_word = false; } else if (!in_word) { // Transition from whitespace to non-whitespace marks a new word stats.word_count += 1; in_word = true; } } // Count last line if text doesn't end with newline if (text.len > 0 and text[text.len - 1] != '\n') { stats.line_count += 1; } return stats; } // Format and write statistics to the provided writer // Outputs the statistics in a human-readable format: "Lines: X, Words: Y, Chars: Z" pub fn format(self: TextStats, writer: *std.Io.Writer) std.Io.Writer.Error!void { try writer.print("Lines: {d}, Words: {d}, Chars: {d}", .{ self.line_count, self.word_count, self.char_count, }); } }; // Verify that TextStats correctly analyzes multi-line text with multiple words test "TextStats analyzes simple text" { const text = "hello world\nfoo bar"; const stats = TextStats.analyze(text); try std.testing.expectEqual(@as(usize, 2), stats.line_count); try std.testing.expectEqual(@as(usize, 4), stats.word_count); try std.testing.expectEqual(@as(usize, 19), stats.char_count); } // Verify that TextStats correctly handles edge case of empty input test "TextStats handles empty text" { const text = ""; const stats = TextStats.analyze(text); try std.testing.expectEqual(@as(usize, 0), stats.line_count); try std.testing.expectEqual(@as(usize, 0), stats.word_count); try std.testing.expectEqual(@as(usize, 0), stats.char_count); } ``` Key patterns: - State aggregation: `TextStats` struct holds computed statistics - Analysis function: Pure function taking text, returning stats - Format method: Zig 0.15.2 format interface for printing - Comprehensive tests: Edge cases (empty text, no trailing newline) See v0.15.2 (https://ziglang.org/download/0.15.1/release-notes.html) and Io.zig (https://github.com/ziglang/zig/tree/master/lib/std/Io.zig). ### Subsection: Library Root: Public API [section_id: library-root] [section_url: https://zigbook.net/chapters/23__project-library-and-executable-workspace#library-root] ```zig //! TextKit - A text processing library //! //! This library provides utilities for text manipulation and analysis, //! including string utilities and text statistics. pub const StringUtils = @import("string_utils.zig").StringUtils; pub const TextStats = @import("text_stats.zig").TextStats; const std = @import("std"); /// Library version information pub const version = std.SemanticVersion{ .major = 1, .minor = 0, .patch = 0, }; test { // Ensure all module tests are run std.testing.refAllDecls(@This()); } ``` The root file (`textkit.zig`) serves as the library’s public interface: - Re-exports: Makes submodules accessible as `textkit.StringUtils` and `textkit.TextStats` - Version metadata: Semantic version for external consumers - Test aggregation: `std.testing.refAllDecls()` ensures all module tests run This pattern allows internal reorganization without breaking consumer imports. 20 (20__concept-primer-modules-vs-programs-vs-packages-vs-libraries.xml), testing.zig (https://github.com/ziglang/zig/tree/master/lib/std/testing.zig) ## Section: Executable Implementation [section_id: executable-implementation] [section_url: https://zigbook.net/chapters/23__project-library-and-executable-workspace#executable-implementation] The CLI tool wraps library functionality in a user-friendly command-line interface with subcommands for different operations. process.zig (https://github.com/ziglang/zig/tree/master/lib/std/process.zig) ### Subsection: CLI Structure and Argument Parsing [section_id: cli-structure] [section_url: https://zigbook.net/chapters/23__project-library-and-executable-workspace#cli-structure] ```zig const std = @import("std"); const textkit = @import("textkit"); pub fn main() !void { // Set up a general-purpose allocator for dynamic memory allocation var gpa = std.heap.GeneralPurposeAllocator(.{}){}; defer _ = gpa.deinit(); const allocator = gpa.allocator(); // Retrieve command line arguments passed to the program const args = try std.process.argsAlloc(allocator); defer std.process.argsFree(allocator, args); // Ensure at least one command argument is provided (args[0] is the program name) if (args.len < 2) { try printUsage(); return; } // Extract the command verb from the first argument const command = args[1]; // Dispatch to the appropriate handler based on the command if (std.mem.eql(u8, command, "analyze")) { // 'analyze' requires a filename argument if (args.len < 3) { std.debug.print("Error: analyze requires a filename\n", .{}); return; } try analyzeFile(allocator, args[2]); } else if (std.mem.eql(u8, command, "reverse")) { // 'reverse' requires text to reverse if (args.len < 3) { std.debug.print("Error: reverse requires text\n", .{}); return; } try reverseText(args[2]); } else if (std.mem.eql(u8, command, "count")) { // 'count' requires both text and a single character to count if (args.len < 4) { std.debug.print("Error: count requires text and character\n", .{}); return; } // Validate that the character argument is exactly one byte if (args[3].len != 1) { std.debug.print("Error: character must be single byte\n", .{}); return; } try countCharacter(args[2], args[3][0]); } else { // Handle unrecognized commands std.debug.print("Unknown command: {s}\n", .{command}); try printUsage(); } } /// Print usage information to guide users on available commands fn printUsage() !void { const usage = \\TextKit CLI - Text processing utility \\ \\Usage: \\ textkit-cli analyze Analyze text file statistics \\ textkit-cli reverse Reverse the given text \\ textkit-cli count Count character occurrences \\ ; std.debug.print("{s}", .{usage}); } /// Read a file and display statistical analysis of its text content fn analyzeFile(allocator: std.mem.Allocator, filename: []const u8) !void { // Open the file in read-only mode from the current working directory const file = try std.fs.cwd().openFile(filename, .{}); defer file.close(); // Read the entire file content into memory (limited to 1MB) const content = try file.readToEndAlloc(allocator, 1024 * 1024); defer allocator.free(content); // Use textkit library to compute text statistics const stats = textkit.TextStats.analyze(content); // Display the computed statistics to the user std.debug.print("File: {s}\n", .{filename}); std.debug.print(" Lines: {d}\n", .{stats.line_count}); std.debug.print(" Words: {d}\n", .{stats.word_count}); std.debug.print(" Characters: {d}\n", .{stats.char_count}); std.debug.print(" ASCII only: {}\n", .{textkit.StringUtils.isAscii(content)}); } /// Reverse the provided text and display both original and reversed versions fn reverseText(text: []const u8) !void { // Allocate a stack buffer for in-place reversal var buffer: [1024]u8 = undefined; // Ensure the input text fits within the buffer if (text.len > buffer.len) { std.debug.print("Error: text too long (max {d} chars)\n", .{buffer.len}); return; } // Copy input text into the mutable buffer for reversal @memcpy(buffer[0..text.len], text); // Perform in-place reversal using textkit utility textkit.StringUtils.reverse(buffer[0..text.len]); // Display both the original and reversed text std.debug.print("Original: {s}\n", .{text}); std.debug.print("Reversed: {s}\n", .{buffer[0..text.len]}); } /// Count occurrences of a specific character in the provided text fn countCharacter(text: []const u8, char: u8) !void { // Use textkit to count character occurrences const count = textkit.StringUtils.countChar(text, char); // Display the count result std.debug.print("Character '{c}' appears {d} time(s) in: {s}\n", .{ char, count, text, }); } // Test that all declarations in this module are reachable and compile correctly test "main program compiles" { std.testing.refAllDecls(@This()); } ``` The executable demonstrates: - Command dispatch: Routing subcommands to handler functions - Argument validation: Checking parameter counts and formats - Error handling: Graceful failures with informative messages - Library consumption: Clean imports via `@import("textkit")` 2 (02__control-flow-essentials.xml) ### Subsection: Command Handler Functions [section_id: cli-handlers] [section_url: https://zigbook.net/chapters/23__project-library-and-executable-workspace#cli-handlers] ```zig const std = @import("std"); const textkit = @import("textkit"); pub fn main() !void { // Set up a general-purpose allocator for dynamic memory allocation var gpa = std.heap.GeneralPurposeAllocator(.{}){}; defer _ = gpa.deinit(); const allocator = gpa.allocator(); // Retrieve command line arguments passed to the program const args = try std.process.argsAlloc(allocator); defer std.process.argsFree(allocator, args); // Ensure at least one command argument is provided (args[0] is the program name) if (args.len < 2) { try printUsage(); return; } // Extract the command verb from the first argument const command = args[1]; // Dispatch to the appropriate handler based on the command if (std.mem.eql(u8, command, "analyze")) { // 'analyze' requires a filename argument if (args.len < 3) { std.debug.print("Error: analyze requires a filename\n", .{}); return; } try analyzeFile(allocator, args[2]); } else if (std.mem.eql(u8, command, "reverse")) { // 'reverse' requires text to reverse if (args.len < 3) { std.debug.print("Error: reverse requires text\n", .{}); return; } try reverseText(args[2]); } else if (std.mem.eql(u8, command, "count")) { // 'count' requires both text and a single character to count if (args.len < 4) { std.debug.print("Error: count requires text and character\n", .{}); return; } // Validate that the character argument is exactly one byte if (args[3].len != 1) { std.debug.print("Error: character must be single byte\n", .{}); return; } try countCharacter(args[2], args[3][0]); } else { // Handle unrecognized commands std.debug.print("Unknown command: {s}\n", .{command}); try printUsage(); } } /// Print usage information to guide users on available commands fn printUsage() !void { const usage = \\TextKit CLI - Text processing utility \\ \\Usage: \\ textkit-cli analyze Analyze text file statistics \\ textkit-cli reverse Reverse the given text \\ textkit-cli count Count character occurrences \\ ; std.debug.print("{s}", .{usage}); } /// Read a file and display statistical analysis of its text content fn analyzeFile(allocator: std.mem.Allocator, filename: []const u8) !void { // Open the file in read-only mode from the current working directory const file = try std.fs.cwd().openFile(filename, .{}); defer file.close(); // Read the entire file content into memory (limited to 1MB) const content = try file.readToEndAlloc(allocator, 1024 * 1024); defer allocator.free(content); // Use textkit library to compute text statistics const stats = textkit.TextStats.analyze(content); // Display the computed statistics to the user std.debug.print("File: {s}\n", .{filename}); std.debug.print(" Lines: {d}\n", .{stats.line_count}); std.debug.print(" Words: {d}\n", .{stats.word_count}); std.debug.print(" Characters: {d}\n", .{stats.char_count}); std.debug.print(" ASCII only: {}\n", .{textkit.StringUtils.isAscii(content)}); } /// Reverse the provided text and display both original and reversed versions fn reverseText(text: []const u8) !void { // Allocate a stack buffer for in-place reversal var buffer: [1024]u8 = undefined; // Ensure the input text fits within the buffer if (text.len > buffer.len) { std.debug.print("Error: text too long (max {d} chars)\n", .{buffer.len}); return; } // Copy input text into the mutable buffer for reversal @memcpy(buffer[0..text.len], text); // Perform in-place reversal using textkit utility textkit.StringUtils.reverse(buffer[0..text.len]); // Display both the original and reversed text std.debug.print("Original: {s}\n", .{text}); std.debug.print("Reversed: {s}\n", .{buffer[0..text.len]}); } /// Count occurrences of a specific character in the provided text fn countCharacter(text: []const u8, char: u8) !void { // Use textkit to count character occurrences const count = textkit.StringUtils.countChar(text, char); // Display the count result std.debug.print("Character '{c}' appears {d} time(s) in: {s}\n", .{ char, count, text, }); } // Test that all declarations in this module are reachable and compile correctly test "main program compiles" { std.testing.refAllDecls(@This()); } ``` Each handler showcases different library features: - `analyzeFile`: File I/O, memory allocation, text statistics - `reverseText`: Stack buffer usage, string manipulation - `countCharacter`: Simple library delegation ## Section: Build Script: Orchestrating the Workspace [section_id: build-script] [section_url: https://zigbook.net/chapters/23__project-library-and-executable-workspace#build-script] The `build.zig` file ties everything together, defining how library and executable relate and how users interact with the project. ### Subsection: Complete Build Script [section_id: build-script-full] [section_url: https://zigbook.net/chapters/23__project-library-and-executable-workspace#build-script-full] ```zig const std = @import("std"); pub fn build(b: *std.Build) void { // Standard target and optimization options const target = b.standardTargetOptions(.{}); const optimize = b.standardOptimizeOption(.{}); // ===== LIBRARY ===== // Create the TextKit library module const textkit_mod = b.addModule("textkit", .{ .root_source_file = b.path("src/textkit.zig"), .target = target, }); // Build static library artifact const lib = b.addLibrary(.{ .name = "textkit", .root_module = b.createModule(.{ .root_source_file = b.path("src/textkit.zig"), .target = target, .optimize = optimize, }), .version = .{ .major = 1, .minor = 0, .patch = 0 }, .linkage = .static, }); // Install the library (to zig-out/lib/) b.installArtifact(lib); // ===== EXECUTABLE ===== // Create executable that uses the library const exe = b.addExecutable(.{ .name = "textkit-cli", .root_module = b.createModule(.{ .root_source_file = b.path("src/main.zig"), .target = target, .optimize = optimize, .imports = &.{ .{ .name = "textkit", .module = textkit_mod }, }, }), }); // Install the executable (to zig-out/bin/) b.installArtifact(exe); // ===== RUN STEP ===== // Create a run step for the executable const run_cmd = b.addRunArtifact(exe); run_cmd.step.dependOn(b.getInstallStep()); // Forward command-line arguments to the application if (b.args) |args| { run_cmd.addArgs(args); } const run_step = b.step("run", "Run the TextKit CLI"); run_step.dependOn(&run_cmd.step); // ===== TESTS ===== // Library tests const lib_tests = b.addTest(.{ .root_module = textkit_mod, }); const run_lib_tests = b.addRunArtifact(lib_tests); // Executable tests (minimal for main.zig) const exe_tests = b.addTest(.{ .root_module = exe.root_module, }); const run_exe_tests = b.addRunArtifact(exe_tests); // Test step that runs all tests const test_step = b.step("test", "Run all tests"); test_step.dependOn(&run_lib_tests.step); test_step.dependOn(&run_exe_tests.step); // ===== CUSTOM STEPS ===== // Demo step that shows usage const demo_step = b.step("demo", "Run demo commands"); const demo_reverse = b.addRunArtifact(exe); demo_reverse.addArgs(&.{ "reverse", "Hello Zig!" }); demo_step.dependOn(&demo_reverse.step); const demo_count = b.addRunArtifact(exe); demo_count.addArgs(&.{ "count", "mississippi", "s" }); demo_step.dependOn(&demo_count.step); } ``` ### Subsection: Build Script Sections Explained [section_id: build-script-sections] [section_url: https://zigbook.net/chapters/23__project-library-and-executable-workspace#build-script-sections] ### Subsection: Library Creation [section_id: library-section] [section_url: https://zigbook.net/chapters/23__project-library-and-executable-workspace#library-section] ```zig const std = @import("std"); pub fn build(b: *std.Build) void { // Standard target and optimization options const target = b.standardTargetOptions(.{}); const optimize = b.standardOptimizeOption(.{}); // ===== LIBRARY ===== // Create the TextKit library module const textkit_mod = b.addModule("textkit", .{ .root_source_file = b.path("src/textkit.zig"), .target = target, }); // Build static library artifact const lib = b.addLibrary(.{ .name = "textkit", .root_module = b.createModule(.{ .root_source_file = b.path("src/textkit.zig"), .target = target, .optimize = optimize, }), .version = .{ .major = 1, .minor = 0, .patch = 0 }, .linkage = .static, }); // Install the library (to zig-out/lib/) b.installArtifact(lib); // ===== EXECUTABLE ===== // Create executable that uses the library const exe = b.addExecutable(.{ .name = "textkit-cli", .root_module = b.createModule(.{ .root_source_file = b.path("src/main.zig"), .target = target, .optimize = optimize, .imports = &.{ .{ .name = "textkit", .module = textkit_mod }, }, }), }); // Install the executable (to zig-out/bin/) b.installArtifact(exe); // ===== RUN STEP ===== // Create a run step for the executable const run_cmd = b.addRunArtifact(exe); run_cmd.step.dependOn(b.getInstallStep()); // Forward command-line arguments to the application if (b.args) |args| { run_cmd.addArgs(args); } const run_step = b.step("run", "Run the TextKit CLI"); run_step.dependOn(&run_cmd.step); // ===== TESTS ===== // Library tests const lib_tests = b.addTest(.{ .root_module = textkit_mod, }); const run_lib_tests = b.addRunArtifact(lib_tests); // Executable tests (minimal for main.zig) const exe_tests = b.addTest(.{ .root_module = exe.root_module, }); const run_exe_tests = b.addRunArtifact(exe_tests); // Test step that runs all tests const test_step = b.step("test", "Run all tests"); test_step.dependOn(&run_lib_tests.step); test_step.dependOn(&run_exe_tests.step); // ===== CUSTOM STEPS ===== // Demo step that shows usage const demo_step = b.step("demo", "Run demo commands"); const demo_reverse = b.addRunArtifact(exe); demo_reverse.addArgs(&.{ "reverse", "Hello Zig!" }); demo_step.dependOn(&demo_reverse.step); const demo_count = b.addRunArtifact(exe); demo_count.addArgs(&.{ "count", "mississippi", "s" }); demo_step.dependOn(&demo_count.step); } ``` Two module creations serve different purposes: - `textkit_mod`: Public module for consumers (via `b.addModule`) - `lib`: Static library artifact with separate module configuration The library module specifies only `.target` because optimization is user-facing, while the library artifact requires both `.target` and `.optimize` for compilation. NOTE: We use `.linkage = .static` to produce a `.a` archive file; change to `.dynamic` for `.so`/`.dylib`/`.dll` shared libraries. 22 (22__build-system-deep-dive.xml) ### Subsection: Executable with Library Import [section_id: executable-section] [section_url: https://zigbook.net/chapters/23__project-library-and-executable-workspace#executable-section] ```zig const std = @import("std"); pub fn build(b: *std.Build) void { // Standard target and optimization options const target = b.standardTargetOptions(.{}); const optimize = b.standardOptimizeOption(.{}); // ===== LIBRARY ===== // Create the TextKit library module const textkit_mod = b.addModule("textkit", .{ .root_source_file = b.path("src/textkit.zig"), .target = target, }); // Build static library artifact const lib = b.addLibrary(.{ .name = "textkit", .root_module = b.createModule(.{ .root_source_file = b.path("src/textkit.zig"), .target = target, .optimize = optimize, }), .version = .{ .major = 1, .minor = 0, .patch = 0 }, .linkage = .static, }); // Install the library (to zig-out/lib/) b.installArtifact(lib); // ===== EXECUTABLE ===== // Create executable that uses the library const exe = b.addExecutable(.{ .name = "textkit-cli", .root_module = b.createModule(.{ .root_source_file = b.path("src/main.zig"), .target = target, .optimize = optimize, .imports = &.{ .{ .name = "textkit", .module = textkit_mod }, }, }), }); // Install the executable (to zig-out/bin/) b.installArtifact(exe); // ===== RUN STEP ===== // Create a run step for the executable const run_cmd = b.addRunArtifact(exe); run_cmd.step.dependOn(b.getInstallStep()); // Forward command-line arguments to the application if (b.args) |args| { run_cmd.addArgs(args); } const run_step = b.step("run", "Run the TextKit CLI"); run_step.dependOn(&run_cmd.step); // ===== TESTS ===== // Library tests const lib_tests = b.addTest(.{ .root_module = textkit_mod, }); const run_lib_tests = b.addRunArtifact(lib_tests); // Executable tests (minimal for main.zig) const exe_tests = b.addTest(.{ .root_module = exe.root_module, }); const run_exe_tests = b.addRunArtifact(exe_tests); // Test step that runs all tests const test_step = b.step("test", "Run all tests"); test_step.dependOn(&run_lib_tests.step); test_step.dependOn(&run_exe_tests.step); // ===== CUSTOM STEPS ===== // Demo step that shows usage const demo_step = b.step("demo", "Run demo commands"); const demo_reverse = b.addRunArtifact(exe); demo_reverse.addArgs(&.{ "reverse", "Hello Zig!" }); demo_step.dependOn(&demo_reverse.step); const demo_count = b.addRunArtifact(exe); demo_count.addArgs(&.{ "count", "mississippi", "s" }); demo_step.dependOn(&demo_count.step); } ``` The `.imports` table connects `main.zig` to the library module, enabling `@import("textkit")`. The name "textkit" is arbitrary—you could rename it to "lib" and use `@import("lib")` instead. ### Subsection: Run Step with Argument Forwarding [section_id: run-section] [section_url: https://zigbook.net/chapters/23__project-library-and-executable-workspace#run-section] ```zig const std = @import("std"); pub fn build(b: *std.Build) void { // Standard target and optimization options const target = b.standardTargetOptions(.{}); const optimize = b.standardOptimizeOption(.{}); // ===== LIBRARY ===== // Create the TextKit library module const textkit_mod = b.addModule("textkit", .{ .root_source_file = b.path("src/textkit.zig"), .target = target, }); // Build static library artifact const lib = b.addLibrary(.{ .name = "textkit", .root_module = b.createModule(.{ .root_source_file = b.path("src/textkit.zig"), .target = target, .optimize = optimize, }), .version = .{ .major = 1, .minor = 0, .patch = 0 }, .linkage = .static, }); // Install the library (to zig-out/lib/) b.installArtifact(lib); // ===== EXECUTABLE ===== // Create executable that uses the library const exe = b.addExecutable(.{ .name = "textkit-cli", .root_module = b.createModule(.{ .root_source_file = b.path("src/main.zig"), .target = target, .optimize = optimize, .imports = &.{ .{ .name = "textkit", .module = textkit_mod }, }, }), }); // Install the executable (to zig-out/bin/) b.installArtifact(exe); // ===== RUN STEP ===== // Create a run step for the executable const run_cmd = b.addRunArtifact(exe); run_cmd.step.dependOn(b.getInstallStep()); // Forward command-line arguments to the application if (b.args) |args| { run_cmd.addArgs(args); } const run_step = b.step("run", "Run the TextKit CLI"); run_step.dependOn(&run_cmd.step); // ===== TESTS ===== // Library tests const lib_tests = b.addTest(.{ .root_module = textkit_mod, }); const run_lib_tests = b.addRunArtifact(lib_tests); // Executable tests (minimal for main.zig) const exe_tests = b.addTest(.{ .root_module = exe.root_module, }); const run_exe_tests = b.addRunArtifact(exe_tests); // Test step that runs all tests const test_step = b.step("test", "Run all tests"); test_step.dependOn(&run_lib_tests.step); test_step.dependOn(&run_exe_tests.step); // ===== CUSTOM STEPS ===== // Demo step that shows usage const demo_step = b.step("demo", "Run demo commands"); const demo_reverse = b.addRunArtifact(exe); demo_reverse.addArgs(&.{ "reverse", "Hello Zig!" }); demo_step.dependOn(&demo_reverse.step); const demo_count = b.addRunArtifact(exe); demo_count.addArgs(&.{ "count", "mississippi", "s" }); demo_step.dependOn(&demo_count.step); } ``` This standard pattern: 1. Creates a run artifact step 2. Depends on installation (ensures binary is in `zig-out/bin/`) 3. Forwards CLI arguments after `--` 4. Wires to top-level `run` step 22 (22__build-system-deep-dive.xml), Run.zig (https://github.com/ziglang/zig/tree/master/lib/std/Build/Step/Run.zig) ### Subsection: Test Integration [section_id: test-section] [section_url: https://zigbook.net/chapters/23__project-library-and-executable-workspace#test-section] ```zig const std = @import("std"); pub fn build(b: *std.Build) void { // Standard target and optimization options const target = b.standardTargetOptions(.{}); const optimize = b.standardOptimizeOption(.{}); // ===== LIBRARY ===== // Create the TextKit library module const textkit_mod = b.addModule("textkit", .{ .root_source_file = b.path("src/textkit.zig"), .target = target, }); // Build static library artifact const lib = b.addLibrary(.{ .name = "textkit", .root_module = b.createModule(.{ .root_source_file = b.path("src/textkit.zig"), .target = target, .optimize = optimize, }), .version = .{ .major = 1, .minor = 0, .patch = 0 }, .linkage = .static, }); // Install the library (to zig-out/lib/) b.installArtifact(lib); // ===== EXECUTABLE ===== // Create executable that uses the library const exe = b.addExecutable(.{ .name = "textkit-cli", .root_module = b.createModule(.{ .root_source_file = b.path("src/main.zig"), .target = target, .optimize = optimize, .imports = &.{ .{ .name = "textkit", .module = textkit_mod }, }, }), }); // Install the executable (to zig-out/bin/) b.installArtifact(exe); // ===== RUN STEP ===== // Create a run step for the executable const run_cmd = b.addRunArtifact(exe); run_cmd.step.dependOn(b.getInstallStep()); // Forward command-line arguments to the application if (b.args) |args| { run_cmd.addArgs(args); } const run_step = b.step("run", "Run the TextKit CLI"); run_step.dependOn(&run_cmd.step); // ===== TESTS ===== // Library tests const lib_tests = b.addTest(.{ .root_module = textkit_mod, }); const run_lib_tests = b.addRunArtifact(lib_tests); // Executable tests (minimal for main.zig) const exe_tests = b.addTest(.{ .root_module = exe.root_module, }); const run_exe_tests = b.addRunArtifact(exe_tests); // Test step that runs all tests const test_step = b.step("test", "Run all tests"); test_step.dependOn(&run_lib_tests.step); test_step.dependOn(&run_exe_tests.step); // ===== CUSTOM STEPS ===== // Demo step that shows usage const demo_step = b.step("demo", "Run demo commands"); const demo_reverse = b.addRunArtifact(exe); demo_reverse.addArgs(&.{ "reverse", "Hello Zig!" }); demo_step.dependOn(&demo_reverse.step); const demo_count = b.addRunArtifact(exe); demo_count.addArgs(&.{ "count", "mississippi", "s" }); demo_step.dependOn(&demo_count.step); } ``` Separating library and executable tests isolates failures and enables parallel execution. Both depend on the same `test` step so `zig build test` runs everything. 13 (13__testing-and-leak-detection.xml) ### Subsection: Custom Demo Step [section_id: custom-steps] [section_url: https://zigbook.net/chapters/23__project-library-and-executable-workspace#custom-steps] ```zig const std = @import("std"); pub fn build(b: *std.Build) void { // Standard target and optimization options const target = b.standardTargetOptions(.{}); const optimize = b.standardOptimizeOption(.{}); // ===== LIBRARY ===== // Create the TextKit library module const textkit_mod = b.addModule("textkit", .{ .root_source_file = b.path("src/textkit.zig"), .target = target, }); // Build static library artifact const lib = b.addLibrary(.{ .name = "textkit", .root_module = b.createModule(.{ .root_source_file = b.path("src/textkit.zig"), .target = target, .optimize = optimize, }), .version = .{ .major = 1, .minor = 0, .patch = 0 }, .linkage = .static, }); // Install the library (to zig-out/lib/) b.installArtifact(lib); // ===== EXECUTABLE ===== // Create executable that uses the library const exe = b.addExecutable(.{ .name = "textkit-cli", .root_module = b.createModule(.{ .root_source_file = b.path("src/main.zig"), .target = target, .optimize = optimize, .imports = &.{ .{ .name = "textkit", .module = textkit_mod }, }, }), }); // Install the executable (to zig-out/bin/) b.installArtifact(exe); // ===== RUN STEP ===== // Create a run step for the executable const run_cmd = b.addRunArtifact(exe); run_cmd.step.dependOn(b.getInstallStep()); // Forward command-line arguments to the application if (b.args) |args| { run_cmd.addArgs(args); } const run_step = b.step("run", "Run the TextKit CLI"); run_step.dependOn(&run_cmd.step); // ===== TESTS ===== // Library tests const lib_tests = b.addTest(.{ .root_module = textkit_mod, }); const run_lib_tests = b.addRunArtifact(lib_tests); // Executable tests (minimal for main.zig) const exe_tests = b.addTest(.{ .root_module = exe.root_module, }); const run_exe_tests = b.addRunArtifact(exe_tests); // Test step that runs all tests const test_step = b.step("test", "Run all tests"); test_step.dependOn(&run_lib_tests.step); test_step.dependOn(&run_exe_tests.step); // ===== CUSTOM STEPS ===== // Demo step that shows usage const demo_step = b.step("demo", "Run demo commands"); const demo_reverse = b.addRunArtifact(exe); demo_reverse.addArgs(&.{ "reverse", "Hello Zig!" }); demo_step.dependOn(&demo_reverse.step); const demo_count = b.addRunArtifact(exe); demo_count.addArgs(&.{ "count", "mississippi", "s" }); demo_step.dependOn(&demo_count.step); } ``` Custom steps showcase functionality without requiring user input. `zig build demo` runs predefined commands sequentially, demonstrating the CLI’s capabilities. ## Section: Using the Project [section_id: using-the-project] [section_url: https://zigbook.net/chapters/23__project-library-and-executable-workspace#using-the-project] TextKit supports multiple workflows for building, testing, and running. 22 (22__build-system-deep-dive.xml) ### Subsection: Building Library and Executable [section_id: building] [section_url: https://zigbook.net/chapters/23__project-library-and-executable-workspace#building] ```shell $ zig build ``` - Library: `zig-out/lib/libtextkit.a` - Executable: `zig-out/bin/textkit-cli` Both artifacts are installed to standard locations by default. ### Subsection: Running Tests [section_id: testing] [section_url: https://zigbook.net/chapters/23__project-library-and-executable-workspace#testing] ```shell $ zig build test ``` Output (success): ```text All 5 tests passed. ``` Tests from both `string_utils.zig`, `text_stats.zig`, and `main.zig` run together, reporting aggregate results. 13 (13__testing-and-leak-detection.xml) ### Subsection: Running the CLI [section_id: running-cli] [section_url: https://zigbook.net/chapters/23__project-library-and-executable-workspace#running-cli] ### Subsection: View Usage [section_id: cli-usage] [section_url: https://zigbook.net/chapters/23__project-library-and-executable-workspace#cli-usage] ```shell $ zig build run ``` Output: ```shell TextKit CLI - Text processing utility Usage: textkit-cli analyze Analyze text file statistics textkit-cli reverse Reverse the given text textkit-cli count Count character occurrences ``` ### Subsection: Reverse Text [section_id: cli-reverse] [section_url: https://zigbook.net/chapters/23__project-library-and-executable-workspace#cli-reverse] ```shell $ zig build run -- reverse "Hello World" ``` Output: ```shell Original: Hello World Reversed: dlroW olleH ``` ### Subsection: Count Characters [section_id: cli-count] [section_url: https://zigbook.net/chapters/23__project-library-and-executable-workspace#cli-count] ```shell $ zig build run -- count "mississippi" "s" ``` Output: ```shell Character 's' appears 4 time(s) in: mississippi ``` ### Subsection: Analyze File [section_id: cli-analyze] [section_url: https://zigbook.net/chapters/23__project-library-and-executable-workspace#cli-analyze] ```shell $ zig build run -- analyze sample.txt ``` Output: ```shell File: sample.txt Lines: 7 Words: 51 Characters: 336 ASCII only: true ``` ### Subsection: Running Demo Step [section_id: demo-step] [section_url: https://zigbook.net/chapters/23__project-library-and-executable-workspace#demo-step] ```shell $ zig build demo ``` Output: ```shell Original: Hello Zig! Reversed: !giZ olleH Character 's' appears 4 time(s) in: mississippi ``` Executes multiple commands in sequence without user interaction—useful for CI/CD pipelines or quick verification. ## Section: Contrasting Build Workflows [section_id: contrasting-workflows] [section_url: https://zigbook.net/chapters/23__project-library-and-executable-workspace#contrasting-workflows] Understanding when to use `zig build` versus `zig build-exe` clarifies the build system’s purpose. ### Subsection: Direct compilation with [section_id: zig-build-exe-approach] [section_url: https://zigbook.net/chapters/23__project-library-and-executable-workspace#zig-build-exe-approach] ```shell $ zig build-exe src/main.zig --name textkit-cli --pkg-begin textkit src/textkit.zig --pkg-end ``` This imperative command: - Compiles immediately without graph construction - Requires manual specification of all modules and flags - Produces no caching or incremental compilation benefits - Suitable for quick one-off builds or debugging ### Subsection: Graph-based build with [section_id: zig-build-approach] [section_url: https://zigbook.net/chapters/23__project-library-and-executable-workspace#zig-build-approach] ```shell $ zig build ``` This declarative command: - Executes `build.zig` to construct a dependency graph - Caches artifacts and skips unchanged steps - Parallelizes independent compilations - Supports user customization via `-D` flags - Integrates testing, installation, and custom steps The graph-based approach scales better as projects grow, making `zig build` the standard for non-trivial codebases. 22 (22__build-system-deep-dive.xml) ## Section: Design Patterns and Best Practices [section_id: design-patterns] [section_url: https://zigbook.net/chapters/23__project-library-and-executable-workspace#design-patterns] TextKit demonstrates several professional patterns worth adopting. ### Subsection: Module Organization [section_id: module-organization] [section_url: https://zigbook.net/chapters/23__project-library-and-executable-workspace#module-organization] - Single responsibility: Each module (`string_utils`, `text_stats`) focuses on one concern - Root re-exports: `textkit.zig` provides unified public API - Test co-location: Tests live next to implementation for maintainability 20 (20__concept-primer-modules-vs-programs-vs-packages-vs-libraries.xml) ### Subsection: Build Script Patterns [section_id: build-script-patterns] [section_url: https://zigbook.net/chapters/23__project-library-and-executable-workspace#build-script-patterns] - Standard options first: Always start with `standardTargetOptions()` and `standardOptimizeOption()` - Logical grouping: Comment sections (===== LIBRARY =====) improve readability - Artifact installation: Call `installArtifact()` for everything users should access - Test separation: Independent library and executable test steps isolate failures 22 (22__build-system-deep-dive.xml) ### Subsection: CLI Design Patterns [section_id: cli-design] [section_url: https://zigbook.net/chapters/23__project-library-and-executable-workspace#cli-design] - Subcommand dispatch: Central router delegates to handler functions - Graceful degradation: Usage messages for invalid input - Resource cleanup: `defer` ensures allocator and file handle cleanup - Library separation: All logic in library, CLI is thin wrapper ## Section: Exercises [section_id: exercises] [section_url: https://zigbook.net/chapters/23__project-library-and-executable-workspace#exercises] - Add a new subcommand `trim` that removes leading/trailing whitespace from text, implementing the function in `string_utils.zig` with tests. ascii.zig (https://github.com/ziglang/zig/tree/master/lib/std/ascii.zig) - Convert the library from static (`.linkage = .static`) to dynamic (`.linkage = .dynamic`) and observe the output file differences. - Create a second executable `textkit-batch` that processes multiple files in parallel using threads, sharing the same library module. 37 (29__threads-and-atomics.xml) - Add a custom build step `bench` that runs performance benchmarks on `StringUtils.reverse` with various input sizes. ## Section: Notes & Caveats [section_id: notes-caveats] [section_url: https://zigbook.net/chapters/23__project-library-and-executable-workspace#notes-caveats] - The static library (`.a` file) is not strictly necessary since Zig can link modules directly, but producing the library artifact demonstrates traditional library distribution patterns. - When creating both a public module (`b.addModule`) and a library artifact (`b.addLibrary`), ensure both point to the same root source file to avoid confusion. - The `installArtifact()` step installs to `zig-out/` by default; override with `.prefix` option for custom installation paths. - Tests in `main.zig` typically verify only that the executable compiles; comprehensive functionality tests belong in library modules. 13 (13__testing-and-leak-detection.xml) ## Section: Caveats, alternatives, edge cases [section_id: caveats-alternatives-edge-cases] [section_url: https://zigbook.net/chapters/23__project-library-and-executable-workspace#caveats-alternatives-edge-cases] - If the library were header-only (no runtime code), you wouldn’t need `addLibrary()`—only the module definition suffices. 20 (20__concept-primer-modules-vs-programs-vs-packages-vs-libraries.xml) - Zig 0.14.0 deprecated direct `root_source_file` in `ExecutableOptions`; always use `root_module` wrapper as shown here. - For C interop scenarios, you’d add `lib.linkLibC()` and potentially generate headers with `lib.addCSourceFile()` plus `installHeader()`. - Large projects might split `build.zig` into helper functions or separate files included via `@import("build_helpers.zig")`—the build script is regular Zig code. # Chapter 24 — Zig Package Manager & (Deep) [chapter_id: 24__zig-package-manager-deep] [chapter_slug: zig-package-manager-deep] [chapter_number: 24] [chapter_url: https://zigbook.net/chapters/24__zig-package-manager-deep] ## Section: Overview [section_id: overview] [section_url: https://zigbook.net/chapters/24__zig-package-manager-deep#overview] Chapter 22 introduced the build system’s API for creating artifacts and configuring builds; Chapter 23 demonstrated workspace organization with libraries and executables.This chapter completes the build system foundation by examining dependency management—how Zig projects declare, fetch, verify, cache, and integrate external packages through the `build.zig.zon` manifest and the package manager built into the Zig toolchain. Build.zig (https://github.com/ziglang/zig/tree/master/lib/std/Build.zig) Unlike traditional package managers that operate as separate tools with their own metadata formats and resolution algorithms, Zig’s package manager is an integral part of the build system itself, leveraging the same deterministic caching infrastructure used for compilation artifacts (see Cache.zig (https://github.com/ziglang/zig/tree/master/lib/std/Build/Cache.zig)). The `build.zig.zon` file—a Zig Object Notation (ZON) document—serves as the single source of truth for package metadata, dependency declarations, and inclusion rules, while `build.zig` orchestrates how those dependencies integrate into your project’s module graph. 20 (20__concept-primer-modules-vs-programs-vs-packages-vs-libraries.xml) By the end of this chapter, you will understand the full lifecycle of a dependency: from declaration in `build.zig.zon`, through cryptographic verification and caching, to module registration and import in your Zig source code. You will also learn patterns for reproducible builds, lazy dependency loading, and local development workflows that balance convenience with security. ## Section: Learning Goals [section_id: learning-goals] [section_url: https://zigbook.net/chapters/24__zig-package-manager-deep#learning-goals] - Understand the structure and semantics of `build.zig.zon` manifest files (see build.zig.zon template (https://github.com/ziglang/zig/blob/master/lib/init/build.zig.zon)). - Declare dependencies using URL-based fetching and path-based local references. - Explain the role of cryptographic hashes in dependency verification and content-addressing. - Navigate the dependency resolution pipeline from fetch to cache to availability. - Integrate fetched dependencies into `build.zig` using `b.dependency()` and `b.lazyDependency()`. - Differentiate between eager and lazy dependency loading strategies. - Understand reproducibility guarantees: lockfiles, hash verification, and deterministic manifests. - Work with the global package cache and understand offline build workflows. - Use `zig fetch` commands for dependency management. ## Section: The Schema [section_id: build-zig-zon-schema] [section_url: https://zigbook.net/chapters/24__zig-package-manager-deep#build-zig-zon-schema] The `build.zig.zon` file is a Zig-native data format—essentially a single anonymous struct literal—that describes package metadata. It is parsed by the Zig compiler at build time, providing strong typing and familiar syntax while remaining human-readable and simple to author. Unlike JSON or TOML, ZON benefits from Zig’s compile-time evaluation, allowing structured data to be validated and transformed during the build process. ### Subsection: Minimal Manifest [section_id: minimal-manifest] [section_url: https://zigbook.net/chapters/24__zig-package-manager-deep#minimal-manifest] Every `build.zig.zon` file must declare at least the package name, version, and minimum supported Zig version: ```zig .{ .name = "myproject", .version = "0.1.0", .minimum_zig_version = "0.15.2", .fingerprint = 0x1234567890abcdef, .paths = .{ "build.zig", "build.zig.zon", "src", "LICENSE", }, } ``` The `.paths` field specifies which files and directories are included when this package is fetched by another project. This inclusion list directly affects the computed package hash—only listed files contribute to the hash, ensuring deterministic content addressing. TIP: The `.paths` field acts as both an inclusion filter and a documentation aid. Always list `build.zig`, `build.zig.zon`, and your source directories. Exclude generated files, test artifacts, and editor-specific files that should not be part of the package’s canonical content. ### Subsection: Package Identity and Versioning [section_id: package-identity-fields] [section_url: https://zigbook.net/chapters/24__zig-package-manager-deep#package-identity-fields] The `.name` and `.version` fields together establish package identity. As of Zig 0.15.2, the package manager does not yet perform automatic version resolution or deduplication, but these fields prepare for future enhancements and help human maintainers understand package relationships. The `.minimum_zig_version` field communicates compatibility expectations. When a package declares a minimum version, the build system will refuse to proceed if the current Zig toolchain is older, preventing obscure compilation failures due to missing features or changed semantics. The `.fingerprint` field (omitted in the minimal example but shown in the template) is a unique identifier generated once when the package is created and never changed thereafter. This fingerprint enables unambiguous detection of package forks and updates, protecting against hostile forks that attempt to impersonate upstream projects. WARNING: Changing the `.fingerprint` has security and trust implications. It signals that this package is a distinct entity from its origin, which may break trust chains and confuse dependency resolution in future Zig versions. ### Subsection: Declaring Dependencies [section_id: declaring-dependencies] [section_url: https://zigbook.net/chapters/24__zig-package-manager-deep#declaring-dependencies] Dependencies are declared in the `.dependencies` struct. Each dependency must provide either a `.url` and `.hash` pair (for remote packages) or a `.path` (for local packages): ```zig .{ .name = "consumer", .version = "0.2.0", .minimum_zig_version = "0.15.2", .dependencies = .{ // Path-based dependency (local development) .mylib = .{ .path = "../mylib", }, // URL-based dependency would look like: // .known_folders = .{ // .url = "https://github.com/ziglibs/known-folders/archive/refs/tags/v1.1.0.tar.gz", // .hash = "1220c1aa96c9cf0a7df5848c9d50e0e1f1e8b6ac8e7f5e4c0f4c5e6f7a8b9c0d", // }, }, .paths = .{ "build.zig", "build.zig.zon", "src", }, } ``` URL-based dependencies are fetched from the network, verified against the provided hash, and cached globally. Path-based dependencies reference a directory relative to the build root, useful during local development or when vendoring dependencies. The hash uses the multihash format, where the prefix `1220` indicates SHA-256. This content-addressed approach ensures that packages are identified by their contents rather than their URLs, making the package manager resilient to URL changes and mirror availability. IMPORTANT: The `.hash` field is the source of truth—packages do not come from a URL; they come from a hash. The URL is merely one possible mirror for obtaining content that matches the hash. This design separates package identity (content) from package location (URL). ### Subsection: Lazy vs Eager Dependencies [section_id: lazy-dependencies] [section_url: https://zigbook.net/chapters/24__zig-package-manager-deep#lazy-dependencies] By default, all declared dependencies are eager: they are fetched and verified before the build script runs. For optional dependencies that are only needed under certain conditions (e.g., debugging tools, benchmarking utilities, or platform-specific extensions), you can mark them as lazy with `.lazy = true`: ```zig .{ .name = "app", .version = "1.0.0", .minimum_zig_version = "0.15.2", .dependencies = .{ // Eager dependency: always fetched .core = .{ .path = "../core", }, // Lazy dependency: only fetched when actually used .benchmark_utils = .{ .path = "../benchmark_utils", .lazy = true, }, .debug_visualizer = .{ .path = "../debug_visualizer", .lazy = true, }, }, .paths = .{ "build.zig", "build.zig.zon", "src", }, } ``` Lazy dependencies are not fetched until `build.zig` explicitly requests them via `b.lazyDependency()`. If the build script never calls `lazyDependency()` for a given package, that package remains unfetched, saving download time and disk space. This two-phase approach allows the build script to declare optional dependencies without forcing all users to download them. When a lazy dependency is requested but not yet available, the build runner will fetch it, then re-run the build script—a transparent process that balances flexibility with determinism. ## Section: Dependency Resolution Pipeline [section_id: resolution-pipeline] [section_url: https://zigbook.net/chapters/24__zig-package-manager-deep#resolution-pipeline] Understanding how Zig transforms a `.dependencies` declaration into a usable module illuminates the package manager’s design and helps debug fetch failures or integration issues. ### Subsection: 1. Parse and Validate [section_id: parse-and-validate] [section_url: https://zigbook.net/chapters/24__zig-package-manager-deep#parse-and-validate] When you run `zig build`, the compiler first parses `build.zig.zon` as a ZON literal (see build_runner.zig (https://github.com/ziglang/zig/blob/master/lib/compiler/build_runner.zig)). This parse step validates syntax and ensures all required fields are present. The compiler checks: - Each dependency has either `.url`+`.hash` or `.path` (but not both) - Hash strings use valid multihash encoding - The `.minimum_zig_version` is not newer than the running toolchain ### Subsection: 2. Fetch and Verify [section_id: fetch-and-verify] [section_url: https://zigbook.net/chapters/24__zig-package-manager-deep#fetch-and-verify] For each eager dependency with a `.url`, the build runner: 1. Computes a unique cache key from the hash 2. Checks if the package exists in the global cache (`~/.cache/zig/p//` on Unix-like systems) 3. If not cached, downloads the URL contents 4. Extracts the archive if needed (supports `.tar.gz`, `.tar.xz`, `.zip`) 5. Applies the `.paths` filter from the dependency’s own `build.zig.zon` 6. Computes the hash of the filtered content 7. Verifies it matches the declared `.hash` field 8. Stores the verified content in the global cache If hash verification fails, the build aborts with a clear error message indicating hash mismatch. This prevents supply-chain attacks where a compromised mirror serves different content. Path-based dependencies skip the fetch step—they are always available relative to the build root. ### Subsection: 3. Cache Lookup and Reuse [section_id: cache-lookup] [section_url: https://zigbook.net/chapters/24__zig-package-manager-deep#cache-lookup] Once a package is cached, subsequent builds reuse the cached version without re-downloading or re-verifying. The global cache is shared across all Zig projects on the system, so fetching a popular dependency once benefits all projects. The cache directory structure is content-addressed: each package’s hash directly maps to a cache subdirectory. This makes cache management transparent and predictable—you can inspect cached packages or clear the cache without risk of corrupting build state. ### Subsection: 4. Dependency Graph Construction [section_id: dependency-graph-construction] [section_url: https://zigbook.net/chapters/24__zig-package-manager-deep#dependency-graph-construction] After all eager dependencies are available, the build runner constructs a dependency graph. Each package’s `build.zig` is loaded as a Zig module, and the `build()` function is called to register artifacts and steps. Lazy dependencies are not loaded at this stage. Instead, the build runner marks them as "potentially needed" and proceeds. If `build.zig` calls `b.lazyDependency()` for a lazy package that hasn’t been fetched yet, the build runner records the request, completes the current build pass, fetches the lazy dependencies, and re-runs the build script. This deferred-fetch mechanism allows build scripts to conditionally load dependencies based on user options or target characteristics without forcing all users to download every optional package. Internally, Zig records dependencies on ZON manifests and other dependees inside the `InternPool`, so that changes to `build.zig.zon` or embedded files can invalidate only the analysis units that depend on them: ```text graph TB subgraph "InternPool - Dependency Storage" SRCHASHDEPS["src_hash_deps
Map: TrackedInst.Index → DepEntry.Index"] NAVVALDEPS["nav_val_deps
Map: Nav.Index → DepEntry.Index"] NAVTYDEPS["nav_ty_deps
Map: Nav.Index → DepEntry.Index"] INTERNEDDEPS["interned_deps
Map: Index → DepEntry.Index"] ZONFILEDEPS["zon_file_deps
Map: FileIndex → DepEntry.Index"] EMBEDFILEDEPS["embed_file_deps
Map: EmbedFile.Index → DepEntry.Index"] NSDEPS["namespace_deps
Map: TrackedInst.Index → DepEntry.Index"] NSNAMEDEPS["namespace_name_deps
Map: NamespaceNameKey → DepEntry.Index"] FIRSTDEP["first_dependency
Map: AnalUnit → DepEntry.Index"] DEPENTRIES["dep_entries
ArrayListUnmanaged"] FREEDEP["free_dep_entries
ArrayListUnmanaged"] end subgraph "DepEntry Structure" DEPENTRY["DepEntry
{depender: AnalUnit,
next_dependee: DepEntry.Index.Optional,
next_depender: DepEntry.Index.Optional}"] end SRCHASHDEPS --> DEPENTRIES NAVVALDEPS --> DEPENTRIES NAVTYDEPS --> DEPENTRIES INTERNEDDEPS --> DEPENTRIES ZONFILEDEPS --> DEPENTRIES EMBEDFILEDEPS --> DEPENTRIES NSDEPS --> DEPENTRIES NSNAMEDEPS --> DEPENTRIES FIRSTDEP --> DEPENTRIES DEPENTRIES --> DEPENTRY FREEDEP -.->|"reuses indices from"| DEPENTRIES ``` ZON files participate in the same incremental compilation graph as source hashes and embedded files: updating `build.zig.zon` updates the corresponding `zon_file_deps` entries, which in turn mark dependent analysis units and build steps as outdated. More broadly, ZON manifests are just one of several dependee categories that the compiler tracks; at a high level these groups look like this: ```text graph LR subgraph "Source-Level Dependencies" SRCHASH["Source Hash
TrackedInst.Index
src_hash_deps"] ZONFILE["ZON File
FileIndex
zon_file_deps"] EMBEDFILE["Embedded File
EmbedFile.Index
embed_file_deps"] end subgraph "Nav Dependencies" NAVVAL["Nav Value
Nav.Index
nav_val_deps"] NAVTY["Nav Type
Nav.Index
nav_ty_deps"] end subgraph "Type/Value Dependencies" INTERNED["Interned Value
Index
interned_deps
runtime funcs, container types"] end subgraph "Namespace Dependencies" NSFULL["Full Namespace
TrackedInst.Index
namespace_deps"] NSNAME["Namespace Name
NamespaceNameKey
namespace_name_deps"] end subgraph "Memoized State" MEMO["Memoized Fields
panic_messages, etc."] end ``` The package manager sits on top of this infrastructure: `.dependencies` entries in `build.zig.zon` ultimately translate into ZON-file dependees and cached content that participate in the same dependency system. ### Subsection: Conceptual Example: Resolution Pipeline [section_id: conceptual-example] [section_url: https://zigbook.net/chapters/24__zig-package-manager-deep#conceptual-example] The following example demonstrates the logical flow of dependency resolution: ```zig // Conceptual example showing the dependency resolution pipeline const std = @import("std"); const DependencyState = enum { declared, // Listed in build.zig.zon downloading, // URL being fetched verifying, // Hash being checked cached, // Stored in global cache available, // Ready for use }; const Dependency = struct { name: []const u8, url: ?[]const u8, path: ?[]const u8, hash: ?[]const u8, lazy: bool, state: DependencyState, }; pub fn main() !void { std.debug.print("--- Zig Package Manager Resolution Pipeline ---\n\n", .{}); // Stage 1: Parse build.zig.zon std.debug.print("1. Parse build.zig.zon dependencies\n", .{}); var deps = [_]Dependency{ .{ .name = "core", .path = "../core", .url = null, .hash = null, .lazy = false, .state = .declared, }, .{ .name = "utils", .url = "https://example.com/utils.tar.gz", .path = null, .hash = "1220abcd...", .lazy = false, .state = .declared, }, .{ .name = "optional_viz", .url = "https://example.com/viz.tar.gz", .path = null, .hash = "1220ef01...", .lazy = true, .state = .declared, }, }; // Stage 2: Resolve eager dependencies std.debug.print("\n2. Resolve eager dependencies\n", .{}); for (&deps) |*dep| { if (!dep.lazy) { std.debug.print(" - {s}: ", .{dep.name}); if (dep.path) |p| { std.debug.print("local path '{s}' → available\n", .{p}); dep.state = .available; } else if (dep.url) |_| { std.debug.print("fetching → verifying → cached → available\n", .{}); dep.state = .available; } } } // Stage 3: Lazy dependencies deferred std.debug.print("\n3. Lazy dependencies (deferred until used)\n", .{}); for (deps) |dep| { if (dep.lazy) { std.debug.print(" - {s}: waiting for lazyDependency() call\n", .{dep.name}); } } // Stage 4: Build script execution triggers lazy fetch std.debug.print("\n4. Build script requests lazy dependency\n", .{}); std.debug.print(" - optional_viz requested → fetching now\n", .{}); // Stage 5: Cache lookup std.debug.print("\n5. Cache locations\n", .{}); std.debug.print(" - Global: ~/.cache/zig/p//\n", .{}); std.debug.print(" - Project: .zig-cache/\n", .{}); std.debug.print("\n=== Resolution Complete ===\n", .{}); } ``` Run: ```shell $ zig run 07_resolution_pipeline_demo.zig ``` Output: ```shell === Zig Package Manager Resolution Pipeline === 1. Parse build.zig.zon dependencies 2. Resolve eager dependencies - core: local path '../core' → available - utils: fetching → verifying → cached → available 3. Lazy dependencies (deferred until used) - optional_viz: waiting for lazyDependency() call 4. Build script requests lazy dependency - optional_viz requested → fetching now 5. Cache locations - Global: ~/.cache/zig/p// - Project: .zig-cache/ === Resolution Complete === ``` This conceptual model matches the actual implementation in the build runner and standard library. ## Section: Integrating Dependencies in [section_id: integrating-dependencies] [section_url: https://zigbook.net/chapters/24__zig-package-manager-deep#integrating-dependencies] Declaring a dependency in `build.zig.zon` makes it available for fetching; integrating it into your build requires calling `b.dependency()` or `b.lazyDependency()` in `build.zig` to obtain a `*std.Build.Dependency` handle, then extracting modules or artifacts from that dependency. ### Subsection: Using [section_id: basic-dependency-usage] [section_url: https://zigbook.net/chapters/24__zig-package-manager-deep#basic-dependency-usage] For eager dependencies, use `b.dependency(name, args)` where `name` matches a key in `.dependencies` and `args` is a struct containing build options to pass down to the dependency’s build script: ```zig const std = @import("std"); pub fn build(b: *std.Build) void { const target = b.standardTargetOptions(.{}); const optimize = b.standardOptimizeOption(.{}); // Fetch the dependency defined in build.zig.zon const mylib_dep = b.dependency("mylib", .{ .target = target, .optimize = optimize, }); // Get the module from the dependency const mylib_module = mylib_dep.module("mylib"); const exe = b.addExecutable(.{ .name = "app", .root_module = b.createModule(.{ .root_source_file = b.path("src/main.zig"), .target = target, .optimize = optimize, }), }); // Import the dependency module exe.root_module.addImport("mylib", mylib_module); b.installArtifact(exe); const run_cmd = b.addRunArtifact(exe); run_cmd.step.dependOn(b.getInstallStep()); if (b.args) |args| { run_cmd.addArgs(args); } const run_step = b.step("run", "Run the app"); run_step.dependOn(&run_cmd.step); } ``` The `b.dependency()` call returns a `*Dependency`, which provides methods to access the dependency’s artifacts (`.artifact()`), modules (`.module()`), lazy paths (`.path()`), and named write-files (`.namedWriteFiles()`). The `args` parameter forwards build options to the dependency, allowing you to configure the dependency’s target, optimization level, or custom features. This ensures the dependency is built with compatible settings. TIP: Always pass `.target` and `.optimize` to dependencies unless you have a specific reason not to. Mismatched target settings can cause link errors or subtle ABI incompatibilities. ### Subsection: Using [section_id: lazy-dependency-usage] [section_url: https://zigbook.net/chapters/24__zig-package-manager-deep#lazy-dependency-usage] For lazy dependencies, use `b.lazyDependency(name, args)` instead. This function returns `?*Dependency`—`null` if the dependency has not yet been fetched: ```zig const std = @import("std"); pub fn build(b: *std.Build) void { const target = b.standardTargetOptions(.{}); const optimize = b.standardOptimizeOption(.{}); // Core dependency is always loaded const core_dep = b.dependency("core", .{ .target = target, .optimize = optimize, }); const exe = b.addExecutable(.{ .name = "app", .root_module = b.createModule(.{ .root_source_file = b.path("src/main.zig"), .target = target, .optimize = optimize, }), }); exe.root_module.addImport("core", core_dep.module("core")); b.installArtifact(exe); // Conditionally use lazy dependencies based on build options const enable_benchmarks = b.option(bool, "benchmarks", "Enable benchmark mode") orelse false; const enable_debug_viz = b.option(bool, "debug-viz", "Enable debug visualizations") orelse false; if (enable_benchmarks) { // lazyDependency returns null if not yet fetched if (b.lazyDependency("benchmark_utils", .{ .target = target, .optimize = optimize, })) |bench_dep| { exe.root_module.addImport("benchmark", bench_dep.module("benchmark")); } } if (enable_debug_viz) { if (b.lazyDependency("debug_visualizer", .{ .target = target, .optimize = optimize, })) |viz_dep| { exe.root_module.addImport("visualizer", viz_dep.module("visualizer")); } } const run_cmd = b.addRunArtifact(exe); run_cmd.step.dependOn(b.getInstallStep()); const run_step = b.step("run", "Run the app"); run_step.dependOn(&run_cmd.step); } ``` When `lazyDependency()` returns `null`, the build runner records the request and re-runs the build script after fetching the missing dependency. On the second pass, `lazyDependency()` will succeed, and the build proceeds normally. This pattern allows build scripts to conditionally include optional features without forcing all users to fetch those dependencies: ```shell $ zig build # Core functionality only $ zig build -Dbenchmarks=true # Fetches benchmark_utils if needed $ zig build -Ddebug-viz=true # Fetches debug_visualizer if needed ``` CAUTION: Mixing `b.dependency()` and `b.lazyDependency()` for the same package is an error. If a dependency is marked `.lazy = true` in `build.zig.zon`, you must use `b.lazyDependency()`. If it’s eager (default), you must use `b.dependency()`. The build system enforces this to prevent inconsistent fetch behavior. ## Section: Hash Verification and Multihash Format [section_id: hash-verification-and-multihash] [section_url: https://zigbook.net/chapters/24__zig-package-manager-deep#hash-verification-and-multihash] Cryptographic hashes are central to Zig’s package manager, ensuring that fetched content matches expectations and protecting against tampering or corruption. ### Subsection: Multihash Format [section_id: multihash-format] [section_url: https://zigbook.net/chapters/24__zig-package-manager-deep#multihash-format] Zig uses the multihash format to encode hash digests. A multihash string consists of: 1. A prefix indicating the hash algorithm (e.g., `1220` for SHA-256) 2. The hex-encoded hash digest For SHA-256, the prefix `1220` breaks down as: - `12` (hex) = SHA-256 algorithm identifier - `20` (hex) = 32 bytes = SHA-256 digest length The following example demonstrates conceptual hash computation (the actual implementation lives in the build runner and cache system): ```zig // This example demonstrates how hash verification works conceptually. // In practice, Zig handles this automatically during `zig fetch`. const std = @import("std"); pub fn main() !void { // Simulate fetching a package const package_contents = "This is the package source code."; // Compute the hash var hasher = std.crypto.hash.sha2.Sha256.init(.{}); hasher.update(package_contents); var digest: [32]u8 = undefined; hasher.final(&digest); // Format as hex for display std.debug.print("Package hash: {x}\n", .{digest}); std.debug.print("Expected hash in build.zig.zon: 1220{x}\n", .{digest}); std.debug.print("\nNote: The '1220' prefix indicates SHA-256 in multihash format.\n", .{}); } ``` Run: ```shell $ zig run 06_hash_verification_example.zig ``` Output: ```shell Package hash: 69b2de89d968f316b3679f2e68ecacb50fd3064e0e0ee7922df4e1ced43744d2 Expected hash in build.zig.zon: 122069b2de89d968f316b3679f2e68ecacb50fd3064e0e0ee7922df4e1ced43744d2 Note: The `1220` prefix indicates SHA-256 in multihash format. ``` The compiler uses a similar "hash → compare → reuse" pattern for incremental compilation when deciding whether to reuse cached IR for a declaration: ```text graph TB Process["Process declaration"] --> UpdateHasher["src_hasher.update()"] UpdateHasher --> HashBytes["Hash relevant source bytes"] HashBytes --> HashDeps["Hash dependencies"] HashDeps --> FinalHash["Produce source hash"] FinalHash --> CompareOld["Compare with cached hash"] CompareOld -->|Different| Recompile["Invalidate and recompile"] CompareOld -->|Same| UseCached["Use cached ZIR/AIR"] ``` This is conceptually the same as package hashing: for both source and dependencies, Zig computes a content hash, compares it with a cached value, and either reuses cached artifacts or recomputes them. In practice, you rarely need to compute hashes manually. The `zig fetch` command automates this: ```shell $ zig fetch https://example.com/package.tar.gz ``` Zig downloads the package, computes the hash, and prints the complete multihash string you can copy into `build.zig.zon`. NOTE: The multihash format is forward-compatible with future hash algorithms. If Zig adopts SHA-3 or BLAKE3, new prefix codes will identify those algorithms without breaking existing manifests. ## Section: Reproducibility and Deterministic Builds [section_id: reproducibility-guarantees] [section_url: https://zigbook.net/chapters/24__zig-package-manager-deep#reproducibility-guarantees] Reproducibility—the ability to recreate identical build outputs given the same inputs—is a cornerstone of reliable software distribution. Zig’s package manager contributes to reproducibility through content addressing, hash verification, and explicit versioning. ### Subsection: Content Addressing [section_id: content-addressing] [section_url: https://zigbook.net/chapters/24__zig-package-manager-deep#content-addressing] Because packages are identified by hash rather than URL, the package manager is inherently resilient to URL changes, mirror failures, and upstream relocations. As long as some mirror provides content matching the hash, the package is usable. This content-addressed design also prevents certain classes of supply-chain attacks: an attacker who compromises a single mirror cannot inject malicious code unless they also break the hash function (SHA-256), which is computationally infeasible. The same content-addressing principle appears elsewhere in Zig’s implementation: the `InternPool` stores each distinct type or value exactly once and identifies it by an index, with dependency tracking built on top of these content-derived keys rather than on file paths or textual names. ### Subsection: Lockfile Semantics and Transitive Dependencies [section_id: lockfile-semantics] [section_url: https://zigbook.net/chapters/24__zig-package-manager-deep#lockfile-semantics] As of Zig 0.15.2, the package manager does not generate a separate lockfile—`build.zig.zon` itself serves as the lockfile. Each dependency’s hash locks its content, and transitive dependencies are locked by the direct dependency’s hash (since the direct dependency’s `build.zig.zon` specifies its own dependencies). This approach simplifies the mental model: there is one source of truth (`build.zig.zon`), and the hash chain ensures transitivity without additional metadata files. Future Zig versions may introduce explicit lockfiles for advanced use cases (e.g., tracking resolved URLs or deduplicating transitive dependencies), but the core content-addressing principle will remain. v0.15.2 (https://ziglang.org/download/0.15.1/release-notes.html) ### Subsection: Offline Builds and Cache Portability [section_id: offline-builds] [section_url: https://zigbook.net/chapters/24__zig-package-manager-deep#offline-builds] Once all dependencies are cached, you can build offline indefinitely. The global cache persists across projects, so fetching a dependency once benefits all future projects that use it. To prepare for offline builds: 1. Run `zig build --fetch` to fetch all declared dependencies without building 2. Verify the cache is populated: `ls ~/.cache/zig/p/` 3. Disconnect from the network and run `zig build` normally If you need to transfer a project with its dependencies to an air-gapped environment, you can: 1. Fetch all dependencies on a networked machine 2. Archive the `~/.cache/zig/p/` directory 3. Extract the archive on the air-gapped machine to the same cache location 4. Run `zig build` normally NOTE: Path-based dependencies (`.path = "…​"`) do not require network access and work immediately offline. ## Section: Using for Dependency Management [section_id: zig-fetch-commands] [section_url: https://zigbook.net/chapters/24__zig-package-manager-deep#zig-fetch-commands] The `zig fetch` command provides a CLI for managing dependencies without editing `build.zig.zon` manually. ### Subsection: Fetching and Saving Dependencies [section_id: fetching-and-saving] [section_url: https://zigbook.net/chapters/24__zig-package-manager-deep#fetching-and-saving] To add a new dependency: ```shell $ zig fetch --save https://github.com/example/package/archive/v1.0.0.tar.gz ``` This command: 1. Downloads the URL 2. Computes the hash 3. Adds an entry to `.dependencies` in `build.zig.zon` 4. Saves the package name and hash You can then reference the dependency by name in `build.zig`. ### Subsection: Fetching Without Saving [section_id: fetching-without-saving] [section_url: https://zigbook.net/chapters/24__zig-package-manager-deep#fetching-without-saving] To fetch a URL and print its hash without modifying `build.zig.zon`: ```shell $ zig fetch https://example.com/package.tar.gz ``` This is useful for verifying package integrity or preparing vendored dependencies. ### Subsection: Recursive Fetch [section_id: recursive-fetch] [section_url: https://zigbook.net/chapters/24__zig-package-manager-deep#recursive-fetch] To fetch all dependencies transitively (including dependencies of dependencies): ```shell $ zig build --fetch ``` This populates the cache with everything needed for a complete build, ensuring offline builds will succeed. ## Section: Exercises [section_id: exercises] [section_url: https://zigbook.net/chapters/24__zig-package-manager-deep#exercises] 1. Minimal Package: Create a new Zig library with `zig init-lib`, examine the generated `build.zig.zon`, and explain the purpose of each top-level field. 21 (21__zig-init-and-package-metadata.xml) 2. Path-Based Dependency: Set up two sibling directories (`mylib/` and `myapp/`). Make `myapp` depend on `mylib` using `.path`, implement a simple function in `mylib`, call it from `myapp`, and build successfully. 3. Hash Verification Failure: Intentionally corrupt a dependency’s hash in `build.zig.zon` (change one character) and run `zig build`. Observe and interpret the error message. 4. Lazy Dependency Workflow: Create a project with a lazy dependency for a benchmarking module. Verify that `zig build` (without options) does not fetch the dependency, but `zig build -Dbenchmarks=true` does. 5. Cache Inspection: Run `zig build --fetch` on a project with remote dependencies, then explore the global cache directory (`~/.cache/zig/p/` on Unix). Identify the package directories by their hash prefixes. 6. Offline Build Test: Fetch all dependencies for a project, disconnect from the network (or block DNS resolution), and confirm `zig build` succeeds. Reconnect and add a new dependency to verify fetch works again. ## Section: Notes & Caveats [section_id: notes-and-caveats] [section_url: https://zigbook.net/chapters/24__zig-package-manager-deep#notes-and-caveats] - URL Stability: While content addressing makes the package manager resilient to URL changes, always prefer stable release URLs (tagged releases, not `main` branch archives) to minimize maintenance burden. - Path Dependencies in Distributed Packages: If your package uses `.path` dependencies, those paths must exist relative to the package root when fetched by consumers. Prefer URL-based dependencies for distributed packages to avoid path resolution issues. - Transitive Dependency Deduplication: Zig 0.15.2 does not deduplicate transitive dependencies with different hash strings, even if they refer to the same content. Future versions may implement smarter deduplication. - Security and Trust: Hash verification protects against transport corruption and most tampering, but does not validate package provenance. Trust the source of the hash (e.g., a project’s official repository or release page), not just any mirror. - Build Option Forwarding: When calling `b.dependency()`, carefully choose which build options to forward. Forwarding too many can cause build failures if the dependency doesn’t recognize an option; forwarding too few can result in mismatched configurations. ## Section: Caveats, Alternatives, and Edge Cases [section_id: caveats-alternatives-edge-cases] [section_url: https://zigbook.net/chapters/24__zig-package-manager-deep#caveats-alternatives-edge-cases] - Lazy Dependency Refetch: If you delete a lazy dependency from the cache and re-run `zig build` without the option that triggers it, the dependency remains unfetched. Only when the build script calls `lazyDependency()` again will the fetch occur. - Hash Mismatches After Upstream Changes: If an upstream package changes its content without changing its version tag, and you re-fetch the URL, you’ll encounter a hash mismatch. Always delete the old `.hash` in `build.zig.zon` when updating a URL to signal that you expect new content. - Vendoring Dependencies: For projects with strict supply-chain requirements, consider vendoring dependencies by committing them to your repository (using `.path` references) instead of relying on URL-based fetches. This trades repository size for control. - Mirror Configuration: Zig 0.15.2 does not yet support mirror lists or fallback URLs per dependency. If your primary URL becomes unavailable, you must manually update `build.zig.zon` to a new URL (the hash remains the same, ensuring content integrity). - Fingerprint Collisions: The `.fingerprint` field is a 64-bit value chosen randomly. Collisions are statistically unlikely but not impossible. Future Zig versions may detect and handle fingerprint conflicts during dependency resolution. ## Section: Summary [section_id: summary] [section_url: https://zigbook.net/chapters/24__zig-package-manager-deep#summary] This chapter explored the full lifecycle of Zig package management: - schema: Package metadata, dependency declarations, inclusion rules, and fingerprint identity. - Dependency types: URL-based vs path-based; eager vs lazy loading strategies. - Resolution pipeline: Parse → fetch → verify → cache → construct dependency graph. - Integration in : Using `b.dependency()` and `b.lazyDependency()` to access modules and artifacts. - Hash verification: Multihash format, SHA-256 content addressing, supply-chain protection. - Reproducibility: Content addressing, lockfile semantics, offline builds, cache portability. - commands: Adding, fetching, and verifying dependencies from the CLI. You now have a complete mental model of Zig’s build system: artifact creation, workspace organization, and dependency management (this chapter). The next chapter will extend this foundation by diving deeper into module resolution mechanics and discovery patterns. Understanding the package manager’s design—content addressing, lazy loading, cryptographic verification—empowers you to build reproducible, secure, and maintainable Zig projects, whether working solo or integrating third-party libraries into production systems. # Chapter 25 — Module Resolution & Discovery (Deep Concept) [chapter_id: 25__module-resolution-and-discovery-deep] [chapter_slug: module-resolution-and-discovery-deep] [chapter_number: 25] [chapter_url: https://zigbook.net/chapters/25__module-resolution-and-discovery-deep] ## Section: Overview [section_id: overview] [section_url: https://zigbook.net/chapters/25__module-resolution-and-discovery-deep#overview] This chapter zooms in on what happens after packages register modules—how names become concrete imports, when the compiler opens files, and what hooks control discovery (see build_runner.zig (https://github.com/ziglang/zig/blob/master/lib/compiler/build_runner.zig)). We will model the module graph, illuminate the difference between filesystem paths and registered namespaces, and show how to guard optional helpers without scattering fragile `#ifdef`-style logic. Along the way we will explore compile-time imports, test-specific discovery, and safe probing with `@hasDecl`, reinforcing the writer API changes introduced in Zig 0.15.2 so every example doubles as a reference for correct stdout usage (see v0.15.2 (https://ziglang.org/download/0.15.1/release-notes.html#) and File.zig (https://github.com/ziglang/zig/tree/master/lib/std/fs/File.zig)). ## Section: Learning Goals [section_id: learning-goals] [section_url: https://zigbook.net/chapters/25__module-resolution-and-discovery-deep#learning-goals] - Trace how the build runner expands registered module names into a dependency-aware module graph. 24 (24__zig-package-manager-deep.xml) - Distinguish filesystem-relative imports from build-registered modules and predict which wins in ambiguous cases (see Build.zig (https://github.com/ziglang/zig/tree/master/lib/std/Build.zig) and 22 (22__build-system-deep-dive.xml)). - Recognize every mechanism that triggers module discovery: direct imports, `comptime` blocks, `test` declarations, exports, and entry-point probing (see start.zig (https://github.com/ziglang/zig/tree/master/lib/std/start.zig) and testing.zig (https://github.com/ziglang/zig/tree/master/lib/std/testing.zig)). - Apply compile-time guards to make optional tooling disappear from release artifacts while keeping debug builds richly instrumented (see 19 (19__modules-and-imports-root-builtin-discovery.xml) and builtin.zig (https://github.com/ziglang/zig/tree/master/lib/std/builtin.zig)). - Use `@hasDecl` and related reflection helpers to detect capabilities without relying on lossy string comparisons or unchecked assumptions (see meta.zig (https://github.com/ziglang/zig/tree/master/lib/std/meta.zig) and 15 (15__comptime-and-reflection.xml)). - Document and test discovery policies so collaborators understand when the build graph will include extra modules. 13 (13__testing-and-leak-detection.xml) ## Section: Module Graph Mapping [section_id: module-graph-mapping] [section_url: https://zigbook.net/chapters/25__module-resolution-and-discovery-deep#module-graph-mapping] The compiler turns every translation unit into a struct-like namespace. Imports correspond to edges in that graph, and the build runner feeds it a list of pre-registered namespaces so modules resolve deterministically even when no file with that name exists on disk. Under the hood, these namespaces live inside the `Zcu` compilation state alongside the intern pool, files, and analysis work queues: ```text graph TB ZCU["Zcu"] subgraph "Compilation State" INTERNPOOL["intern_pool: InternPool"] FILES["files: MultiArrayList(File)"] NAMESPACES["namespaces: MultiArrayList(Namespace)"] end subgraph "Source Tracking" ASTGEN["astgen_work_queue"] SEMA["sema_work_queue"] CODEGEN["codegen_work_queue"] end subgraph "Threading" WORKERS["comp.thread_pool"] PERTHREAD["per_thread: []PerThread"] end subgraph "Symbol Management" NAVS["Navigation Values (Navs)"] UAVS["Unbound Anon Values (Uavs)"] EXPORTS["single_exports / multi_exports"] end ZCU --> INTERNPOOL ZCU --> FILES ZCU --> NAMESPACES ZCU --> ASTGEN ZCU --> SEMA ZCU --> CODEGEN ZCU --> WORKERS ZCU --> PERTHREAD ZCU --> NAVS ZCU --> UAVS ZCU --> EXPORTS ``` Module resolution walks this namespace graph as it evaluates `@import` edges, using the same `Zcu` and `InternPool` machinery that powers incremental compilation and symbol resolution. ### Subsection: Root, , and namespaces [section_id: root-std-builtin] [section_url: https://zigbook.net/chapters/25__module-resolution-and-discovery-deep#root-std-builtin] The root module is whichever file the compiler treats as the entry point. From that root you can inspect yourself via `@import("root")`, reach the bundled standard library through `@import("std")`, and access compiler-provided metadata via `@import("builtin")`. The following probe prints what each namespace exposes and demonstrates that filesystem-based imports (`extras.zig`) participate in the same graph. 19 (19__modules-and-imports-root-builtin-discovery.xml) ```zig const std = @import("std"); const builtin = @import("builtin"); const root = @import("root"); const extras = @import("extras.zig"); pub fn helperSymbol() void {} pub fn main() !void { var stdout_buffer: [512]u8 = undefined; var file_writer = std.fs.File.stdout().writer(&stdout_buffer); const out = &file_writer.interface; try out.print("root has main(): {}\n", .{@hasDecl(root, "main")}); try out.print("root has helperSymbol(): {}\n", .{@hasDecl(root, "helperSymbol")}); try out.print("std namespace type: {s}\n", .{@typeName(@TypeOf(@import("std")))}); try out.print("current build mode: {s}\n", .{@tagName(builtin.mode)}); try out.print("extras.greet(): {s}\n", .{extras.greet()}); try out.flush(); } ``` Run: ```shell $ zig run 01_root_namespace.zig ``` Output: ```shell root has main(): true root has helperSymbol(): true std namespace type: type current build mode: Debug extras.greet(): extras namespace discovered via file path ``` NOTE: The call to `std.fs.File.stdout().writer(&buffer)` mirrors the 0.15.2 writer API: we buffer, print, and flush to avoid truncated output while remaining allocator-free. ### Subsection: Names registered by the build graph [section_id: name-registration] [section_url: https://zigbook.net/chapters/25__module-resolution-and-discovery-deep#name-registration] When you call `b.createModule` or `exe.addModule`, you register a namespace name (e.g. `"logging"`) and a root source file. Any `@import("logging")` in that build graph points at the registered module even if a `logging.zig` file sits next to the caller. Only when no registered namespace is found does the compiler fall back to path-based resolution relative to the importing file. This is how dependencies fetched via `build.zig.zon` expose their modules: the build script constructs the graph long before user code executes. 24 (24__zig-package-manager-deep.xml) The compiler enforces that a given file belongs to exactly one module. The compile-error test suite includes a case where the same file is imported both as a registered module and as a direct file path, which is rejected: ```zig const case = ctx.obj("file in multiple modules", b.graph.host); case.addDepModule("foo", "foo.zig"); case.addError( \\comptime { \\ _ = @import("foo"); \\ _ = @import("foo.zig"); \\} , &[_][]const u8{ ":1:1: error: file exists in modules 'foo' and 'root'", ":1:1: note: files must belong to only one module", ":1:1: note: file is the root of module 'foo'", ":3:17: note: file is imported here by the root of module 'root'", }); ``` This demonstrates that a file can be either the root of a registered module or part of the root module via path-based import, but not both at once. ## Section: Discovery Triggers and Timing [section_id: discovery-triggers] [section_url: https://zigbook.net/chapters/25__module-resolution-and-discovery-deep#discovery-triggers] Module discovery starts the moment an import string is known at compile time. The compiler parses the dependency graph in waves, queuing new modules as soon as an import is evaluated in a `comptime` context. 15 (15__comptime-and-reflection.xml) ### Subsection: Imports, , and evaluation order [section_id: comptime-imports] [section_url: https://zigbook.net/chapters/25__module-resolution-and-discovery-deep#comptime-imports] A `comptime` block runs during semantic analysis. If it contains `_ = @import("tooling.zig");`, the build runner resolves and parses that module immediately—even if the runtime never references it. Use explicit policies (flags, optimization modes, or build options) so such imports are predictable rather than surprising. TIP: Resist the temptation to inline string concatenation inside `@import`; Zig requires the import target to be a compile-time known string anyway, so prefer a single constant that documents intent. ### Subsection: Tests, exports, and entry probing [section_id: tests-and-entries] [section_url: https://zigbook.net/chapters/25__module-resolution-and-discovery-deep#tests-and-entries] `test` blocks and `pub export` declarations also trigger discovery. When you run `zig test`, the compiler imports every test-bearing module, injects a synthetic main, and invokes `std.testing` harness helpers. Similarly, `std.start` inspects the root module for `main`, `_start`, and platform-specific entry points, pulling in whichever modules those declarations reference along the way. This is why even dormant test helpers must live behind `comptime` guards; otherwise they leak into production artifacts just because a `test` declaration exists. 19 (19__modules-and-imports-root-builtin-discovery.xml) In the Zig compiler’s own build, the path from test declarations through to the test runner and command looks like this: ```text graph TB subgraph "Test Declaration Layer" TESTDECL["test declarations
test keyword"] DOCTEST["doctests
named tests"] ANON["anonymous tests
unnamed tests"] TESTDECL --> DOCTEST TESTDECL --> ANON end subgraph "std.testing Namespace" EXPECT["expect()
expectEqual()
expectError()"] ALLOCATOR["testing.allocator
leak detection"] FAILING["failing_allocator
OOM simulation"] UTILS["expectEqualSlices()
expectEqualStrings()"] EXPECT --> ALLOCATOR ALLOCATOR --> FAILING end subgraph "Test Runner" RUNNER["test_runner.zig
default runner"] STDERR["stderr output"] SUMMARY["test summary
pass/fail/skip counts"] RUNNER --> STDERR RUNNER --> SUMMARY end subgraph "Execution" ZIGTEST["zig test command"] BUILD["test build"] EXEC["execute tests"] REPORT["report results"] ZIGTEST --> BUILD BUILD --> EXEC EXEC --> REPORT end TESTDECL --> EXPECT EXPECT --> RUNNER RUNNER --> ZIGTEST style EXPECT fill:#f9f9f9 style RUNNER fill:#f9f9f9 style TESTDECL fill:#f9f9f9 ``` This makes it clear that adding declarations not only pulls in but also wires your modules into the test build and execution pipeline driven by . ## Section: Conditional Discovery Patterns [section_id: conditional-discovery] [section_url: https://zigbook.net/chapters/25__module-resolution-and-discovery-deep#conditional-discovery] Optional tooling should not require separate branches of your repository. Instead, drive discovery from compile-time data and reflect over namespaces to decide what to activate. 15 (15__comptime-and-reflection.xml) ### Subsection: Gating modules with optimization mode [section_id: opt-mode-gating] [section_url: https://zigbook.net/chapters/25__module-resolution-and-discovery-deep#opt-mode-gating] Optimization mode is baked into `builtin.mode`. Use it to import expensive diagnostics only when building for Debug. The example below wires in `debug_tools.zig` during Debug builds and skips it for ReleaseFast, while also demonstrating the buffered-writer pattern required in Zig 0.15.2. ```zig const std = @import("std"); const builtin = @import("builtin"); pub fn main() !void { comptime { if (builtin.mode == .Debug) { _ = @import("debug_tools.zig"); } } var stdout_buffer: [512]u8 = undefined; var file_writer = std.fs.File.stdout().writer(&stdout_buffer); const out = &file_writer.interface; try out.print("build mode: {s}\n", .{@tagName(builtin.mode)}); if (comptime builtin.mode == .Debug) { const debug = @import("debug_tools.zig"); try out.print("{s}\n", .{debug.banner}); } else { try out.print("no debug tooling imported\n", .{}); } try out.flush(); } ``` Run (Debug): ```shell $ zig run 02_conditional_import.zig ``` Output: ```shell build mode: Debug debug tooling wired at comptime ``` Run (ReleaseFast): ```shell $ zig run -OReleaseFast 02_conditional_import.zig ``` Output: ```shell build mode: ReleaseFast no debug tooling imported ``` NOTE: Because `@import("debug_tools.zig")` sits behind a `comptime` condition, ReleaseFast binaries never even parse the helper, protecting the build from accidentally depending on debug-only globals. ### Subsection: Safe probing with [section_id: safe-probing] [section_url: https://zigbook.net/chapters/25__module-resolution-and-discovery-deep#safe-probing] Rather than assuming a module exports a particular function, probe it. Here we expose a `plugins` namespace that either forwards to `plugins_enabled.zig` or returns an empty struct. `@hasDecl` tells us at compile time whether the optional `install` hook exists, enabling a safe runtime branch that works in every build mode. 15 (15__comptime-and-reflection.xml) ```zig const std = @import("std"); const plugins = @import("plugins.zig"); pub fn main() !void { var stdout_buffer: [512]u8 = undefined; var file_writer = std.fs.File.stdout().writer(&stdout_buffer); const out = &file_writer.interface; if (comptime @hasDecl(plugins.namespace, "install")) { try out.print("plugin discovered: {s}\n", .{plugins.namespace.install()}); } else { try out.print("no plugin available; continuing safely\n", .{}); } try out.flush(); } ``` Run (Debug): ```shell $ zig run 03_safe_probe.zig ``` Output: ```shell plugin discovered: Diagnostics overlay instrumentation active ``` Run (ReleaseFast): ```shell $ zig run -OReleaseFast 03_safe_probe.zig ``` Output: ```shell no plugin available; continuing safely ``` NOTE: Notice that we test for a declaration on the namespace type itself (`plugins.namespace`). This keeps the root module agnostic to the plugin’s internal structure and avoids stringly typed feature toggles. 19 (19__modules-and-imports-root-builtin-discovery.xml) ### Subsection: Namespace hygiene checklist [section_id: namespace-hygiene] [section_url: https://zigbook.net/chapters/25__module-resolution-and-discovery-deep#namespace-hygiene] - Document which modules the build registers and why; treat the list as part of your public API so consumers know what `@import` calls are stable. 22 (22__build-system-deep-dive.xml) - Prefer re-exporting small, typed structs over dumping entire helper modules into the root namespace; this keeps `@hasDecl` probes fast and predictable. - When mixing filesystem and registered imports, choose distinct names so callers never wonder which module they are getting. 24 (24__zig-package-manager-deep.xml) NOTE: Internally, the compiler tracks dependencies on entire namespaces and individual names as separate dependees. This is why adding or renaming declarations in a namespace can invalidate downstream modules even if their source files do not change: ```text graph LR subgraph "Namespace Dependencies" NSFULL["Full Namespace
TrackedInst.Index
namespace_deps"] NSNAME["Namespace Name
NamespaceNameKey
namespace_name_deps"] end subgraph "Memoized State" MEMO["Memoized Fields
panic_messages, etc."] end ``` Each dependee category has its own invalidation rule—for example, `namespace_deps` invalidates on any name change in a namespace, while `namespace_name_deps` tracks the existence of a specific symbol. ## Section: Operational Guidance [section_id: operational-guidance] [section_url: https://zigbook.net/chapters/25__module-resolution-and-discovery-deep#operational-guidance] - Include discovery tests in your CI pipeline: compile Debug and Release builds, ensuring optional tooling toggles on and off exactly once. 13 (13__testing-and-leak-detection.xml) - Use `zig build --fetch` (from Chapter 24) before running experiments so the dependency graph is fully cached and deterministic. 24 (24__zig-package-manager-deep.xml) - Avoid `comptime` imports driven by environment variables or timestamps; they break reproducibility because the dependency graph now depends on mutable host state. - When in doubt, print the module graph via reflection (`@typeInfo(@import("root"))`) in a dedicated debug utility so teammates can inspect the current namespace surface. 15 (15__comptime-and-reflection.xml) ## Section: Notes & Caveats [section_id: notes-caveats] [section_url: https://zigbook.net/chapters/25__module-resolution-and-discovery-deep#notes-caveats] - `std.fs.File.stdout().writer(&buffer)` is the canonical way to emit text in Zig 0.15.2; forgetting to flush will truncate output in these examples and in your own tooling. - Registered module names take precedence over relative files. Choose unique names for vendored code so local helpers do not accidentally shadow dependencies. 24 (24__zig-package-manager-deep.xml) - `@hasDecl` and `@hasField` operate purely at compile time; they do not inspect runtime state. Combine them with explicit policies (flags, options) to avoid misleading “feature present” banners when the hook is gated elsewhere. 15 (15__comptime-and-reflection.xml) ## Section: Exercises [section_id: exercises] [section_url: https://zigbook.net/chapters/25__module-resolution-and-discovery-deep#exercises] - Extend `01_root_namespace.zig` so it iterates `@typeInfo(@import("root")).Struct.decls`, printing a sorted table of symbols along with the module each one lives in. 15 (15__comptime-and-reflection.xml) - Modify `02_conditional_import.zig` to gate the debug tools behind a build-option boolean (e.g. `-Ddev-inspect=true`) and document how the build script would plumb that option through `b.addOptions` in Chapter 22. 22 (22__build-system-deep-dive.xml) - Create a sibling module that uses `comptime { _ = @import("helper.zig"); }` only when `builtin.mode == .Debug`, then write a test that asserts the helper never compiles in ReleaseFast. 13 (13__testing-and-leak-detection.xml) ## Section: Caveats, alternatives, edge cases [section_id: caveats-alternatives-edge-cases] [section_url: https://zigbook.net/chapters/25__module-resolution-and-discovery-deep#caveats-alternatives-edge-cases] - In multi-package workspaces, module names must remain globally unique; consider prefixing with the package name to avoid collisions when two dependencies register `@import("log")`. 23 (23__project-library-and-executable-workspace.xml) - When targeting freestanding environments without a filesystem, configure the build runner to provide synthetic modules via `b.addAnonymousModule`; path-based imports will fail otherwise. - Disabling `std.start` removes the automatic search for `main`; be prepared to export `_start` manually and handle argument decoding yourself. 19 (19__modules-and-imports-root-builtin-discovery.xml) ## Section: Summary [section_id: summary] [section_url: https://zigbook.net/chapters/25__module-resolution-and-discovery-deep#summary] - Module resolution is deterministic: registered namespaces win, filesystem paths serve as a fallback, and every import happens at compile time. - Discovery triggers extend beyond plain imports—`comptime` blocks, tests, exports, and entry probing all influence which modules join the graph. 19 (19__modules-and-imports-root-builtin-discovery.xml) - Compile-time guards (`builtin.mode`, build options) and reflection helpers (`@hasDecl`) let you offer rich debug tooling without contaminating release binaries. 15 (15__comptime-and-reflection.xml) # Chapter 26 — Build System Advanced Topics [chapter_id: 26__build-system-advanced-topics] [chapter_slug: build-system-advanced-topics] [chapter_number: 26] [chapter_url: https://zigbook.net/chapters/26__build-system-advanced-topics] ## Section: Overview [section_id: overview] [section_url: https://zigbook.net/chapters/26__build-system-advanced-topics#overview] Module resolution gave us the vocabulary for reasoning about the compiler’s graph. Now we turn that vocabulary into infrastructure. This chapter digs into `std.Build` beyond the basics, exploring artifact tours and library/executable workspaces. We will register modules intentionally, compose multi-package workspaces, generate build outputs without touching shell scripts, and drive cross-target matrices from a single `build.zig`. See Build.zig (https://github.com/ziglang/zig/tree/master/lib/std/Build.zig). You will learn how named write-files, anonymous modules, and `resolveTargetQuery` feed the build runner, how to keep vendored code isolated from registry dependencies, and how to wire CI jobs that prove your graph behaves in Debug and Release builds alike. See build_runner.zig (https://github.com/ziglang/zig/blob/master/lib/compiler/build_runner.zig). ### Subsection: How the Build System Executes [section_id: _how_the_build_system_executes] [section_url: https://zigbook.net/chapters/26__build-system-advanced-topics#_how_the_build_system_executes] Before diving into advanced patterns, it’s essential to understand how `std.Build` executes. The following diagram shows the complete flow from the Zig compiler invoking your `build.zig` script through to final artifact installation: ```text graph TB subgraph "CMake Stage (stage2)" CMAKE["CMake"] ZIG2_C["zig2.c
(generated C code)"] ZIGCPP["zigcpp
(C++ LLVM/Clang wrapper)"] ZIG2["zig2 executable"] CMAKE --> ZIG2_C CMAKE --> ZIGCPP ZIG2_C --> ZIG2 ZIGCPP --> ZIG2 end subgraph "Native Build System (stage3)" BUILD_ZIG["build.zig
Native Build Script"] BUILD_FN["build() function"] COMPILER_STEP["addCompilerStep()"] EXE["std.Build.Step.Compile
(compiler executable)"] INSTALL["Installation Steps"] BUILD_ZIG --> BUILD_FN BUILD_FN --> COMPILER_STEP COMPILER_STEP --> EXE EXE --> INSTALL end subgraph "Build Arguments" ZIG_BUILD_ARGS["ZIG_BUILD_ARGS
--zig-lib-dir
-Dversion-string
-Dtarget
-Denable-llvm
-Doptimize"] end ZIG2 -->|"zig2 build"| BUILD_ZIG ZIG_BUILD_ARGS --> BUILD_FN subgraph "Output" STAGE3_BIN["stage3/bin/zig"] STD_LIB["stage3/lib/zig/std/"] LANGREF["stage3/doc/langref.html"] end INSTALL --> STAGE3_BIN INSTALL --> STD_LIB INSTALL --> LANGREF ``` Your `build.zig` is a regular Zig program compiled and executed by the compiler. The `build()` function is the entry point, receiving a `*std.Build` instance that provides the API for defining steps, artifacts, and dependencies. Build arguments (`-D` flags) are parsed by `b.option()` and flow into your build logic as compile-time constants. The build runner then traverses the step dependency graph you’ve declared, executing only the steps needed to satisfy the requested target (defaulting to the install step). This declarative model ensures reproducibility: the same inputs always produce the same build graph. ## Section: Learning Goals [section_id: learning-goals] [section_url: https://zigbook.net/chapters/26__build-system-advanced-topics#learning-goals] - Register reusable modules and anonymous packages explicitly, controlling which names appear in the import namespace. 25 (25__module-resolution-and-discovery-deep.xml) - Generate deterministic artifacts (reports, manifests) from the build graph using named write-files instead of ad-hoc shell scripting. - Coordinate multi-target builds with `resolveTargetQuery`, including host sanity checks and cross-compilation pipelines. 22 (22__build-system-deep-dive.xml), Compile.zig (https://github.com/ziglang/zig/tree/master/lib/std/Build/Step/Compile.zig) - Structure composite workspaces so vendored modules remain private while registry packages stay self-contained. 24 (24__zig-package-manager-deep.xml) - Capture reproducibility guarantees in CI: install steps, run steps, and generated artifacts all hang off `std.Build.Step` dependencies. ## Section: Building a Workspace Surface [section_id: workspace-surface] [section_url: https://zigbook.net/chapters/26__build-system-advanced-topics#workspace-surface] A workspace is just a build graph with clear namespace boundaries. The following example promotes three modules—`analytics`, `reporting`, and a vendored `adapters` helper—and shows how a root executable consumes them. We emphasize which modules are globally registered, which remain anonymous, and how to emit documentation straight from the build graph. ```zig const std = @import("std"); pub fn build(b: *std.Build) void { // Standard target and optimization options allow the build to be configured // for different architectures and optimization levels via CLI flags const target = b.standardTargetOptions(.{}); const optimize = b.standardOptimizeOption(.{}); // Create the analytics module - the foundational module that provides // core metric calculation and analysis capabilities const analytics_mod = b.addModule("analytics", .{ .root_source_file = b.path("workspace/analytics/lib.zig"), .target = target, .optimize = optimize, }); // Create the reporting module - depends on analytics to format and display metrics // Uses addModule() which both creates and registers the module in one step const reporting_mod = b.addModule("reporting", .{ .root_source_file = b.path("workspace/reporting/lib.zig"), .target = target, .optimize = optimize, // Import analytics module to access metric types and computation functions .imports = &.{.{ .name = "analytics", .module = analytics_mod }}, }); // Create the adapters module using createModule() - creates but does not register // This demonstrates an anonymous module that other code can import but won't // appear in the global module namespace const adapters_mod = b.createModule(.{ .root_source_file = b.path("workspace/adapters/vendored.zig"), .target = target, .optimize = optimize, // Adapters need analytics to serialize metric data .imports = &.{.{ .name = "analytics", .module = analytics_mod }}, }); // Create the main application module that orchestrates all dependencies // This demonstrates how a root module can compose multiple imported modules const app_module = b.createModule(.{ .root_source_file = b.path("workspace/app/main.zig"), .target = target, .optimize = optimize, .imports = &.{ // Import all three workspace modules to access their functionality .{ .name = "analytics", .module = analytics_mod }, .{ .name = "reporting", .module = reporting_mod }, .{ .name = "adapters", .module = adapters_mod }, }, }); // Create the executable artifact using the composed app module as its root // The root_module field replaces the legacy root_source_file approach const exe = b.addExecutable(.{ .name = "workspace-app", .root_module = app_module, }); // Install the executable to zig-out/bin so it can be run after building b.installArtifact(exe); // Set up a run command that executes the built executable const run_cmd = b.addRunArtifact(exe); // Forward any command-line arguments passed to the build system to the executable if (b.args) |args| { run_cmd.addArgs(args); } // Create a custom build step "run" that users can invoke with `zig build run` const run_step = b.step("run", "Run workspace app with registered modules"); run_step.dependOn(&run_cmd.step); // Create a named write files step to document the module dependency graph // This is useful for understanding the workspace structure without reading code const graph_files = b.addNamedWriteFiles("graph"); // Generate a text file documenting the module registration hierarchy _ = graph_files.add("module-graph.txt", \\workspace module registration map: \\ analytics -> workspace/analytics/lib.zig \\ reporting -> workspace/reporting/lib.zig (imports analytics) \\ adapters -> (anonymous) workspace/adapters/vendored.zig \\ exe root -> workspace/app/main.zig ); // Create a custom build step "graph" that generates module documentation // Users can invoke this with `zig build graph` to output the dependency map const graph_step = b.step("graph", "Emit module graph summary to zig-out"); graph_step.dependOn(&graph_files.step); } ``` The `build()` function follows a deliberate cadence: - `b.addModule("analytics", …)` registers a public name so the entire workspace can `@import("analytics")`. Module.zig (https://github.com/ziglang/zig/tree/master/lib/std/Build/Module.zig) - `b.createModule` creates a private module (`adapters`) that only the root executable sees—ideal for vendored code that consumers should not reach. 24 (24__zig-package-manager-deep.xml) - `b.addNamedWriteFiles("workspace-graph")` produces a `module-graph.txt` file in `zig-out/`, documenting the namespace mapping without bespoke tooling. - Every dependency is threaded through `.imports`, so the compiler never falls back to filesystem guessing. 25 (25__module-resolution-and-discovery-deep.xml) Run workspace app: ```shell $ zig build --build-file 01_workspace_build.zig run ``` Output: ```shell metric: response_ms count: 6 mean: 12.95 deviation: 1.82 profile: stable json export: { "name": "response_ms", "mean": 12.950, "deviation": 1.819, "profile": "stable" } ``` Generate module graph: ```shell $ zig build --build-file 01_workspace_build.zig graph ``` Output: ```shell No stdout expected. ``` NOTE: Named write-files obey the cache: rerunning `zig build … graph` without changes is instant. Check `zig-out/graph/module-graph.txt` to see the mapping emitted by the build runner. ### Subsection: Library code for the workspace [section_id: workspace-library-code] [section_url: https://zigbook.net/chapters/26__build-system-advanced-topics#workspace-library-code] To keep this example self-contained, the modules live next to the build script. Feel free to adapt them to your needs or swap in registry dependencies declared in `build.zig.zon`. ```zig // Analytics library for statistical calculations on metrics const std = @import("std"); // Represents a named metric with associated numerical values pub const Metric = struct { name: []const u8, values: []const f64, }; // Calculates the arithmetic mean (average) of all values in a metric // Returns the sum of all values divided by the count pub fn mean(metric: Metric) f64 { var total: f64 = 0; for (metric.values) |value| { total += value; } return total / @as(f64, @floatFromInt(metric.values.len)); } // Calculates the standard deviation of values in a metric // Uses the population standard deviation formula: sqrt(sum((x - mean)^2) / n) pub fn deviation(metric: Metric) f64 { const avg = mean(metric); var accum: f64 = 0; // Sum the squared differences from the mean for (metric.values) |value| { const delta = value - avg; accum += delta * delta; } // Return the square root of the variance return std.math.sqrt(accum / @as(f64, @floatFromInt(metric.values.len))); } // Classifies a metric as "variable" or "stable" based on its standard deviation // Metrics with deviation > 3.0 are considered variable, otherwise stable pub fn highlight(metric: Metric) []const u8 { return if (deviation(metric) > 3.0) "variable" else "stable"; } ``` ```zig //! Reporting module for displaying analytics metrics in various formats. //! This module provides utilities to render metrics as human-readable text //! or export them in CSV format for further analysis. const std = @import("std"); const analytics = @import("analytics"); /// Renders a metric's statistics to a writer in a human-readable format. /// Outputs the metric name, number of data points, mean, standard deviation, /// and performance profile label. /// /// Parameters: /// - metric: The analytics metric to render /// - writer: Any writer interface that supports the print() method /// /// Returns an error if writing to the output fails. pub fn render(metric: analytics.Metric, writer: anytype) !void { try writer.print("metric: {s}\n", .{metric.name}); try writer.print("count: {}\n", .{metric.values.len}); try writer.print("mean: {d:.2}\n", .{analytics.mean(metric)}); try writer.print("deviation: {d:.2}\n", .{analytics.deviation(metric)}); try writer.print("profile: {s}\n", .{analytics.highlight(metric)}); } /// Exports a metric's statistics as a CSV-formatted string. /// Creates a two-row CSV with headers and a single data row containing /// the metric's name, mean, deviation, and highlight label. /// /// Parameters: /// - metric: The analytics metric to export /// - allocator: Memory allocator for the resulting string /// /// Returns a heap-allocated CSV string, or an error if allocation or formatting fails. /// Caller is responsible for freeing the returned memory. pub fn csv(metric: analytics.Metric, allocator: std.mem.Allocator) ![]u8 { return std.fmt.allocPrint( allocator, "name,mean,deviation,label\n{s},{d:.3},{d:.3},{s}\n", .{ metric.name, analytics.mean(metric), analytics.deviation(metric), analytics.highlight(metric) }, ); } ``` ```zig const std = @import("std"); const analytics = @import("analytics"); /// Serializes a metric into a JSON-formatted string representation. /// /// Creates a formatted JSON object containing the metric's name, calculated mean, /// standard deviation, and performance profile classification. The caller owns /// the returned memory and must free it when done. /// /// Returns an allocated string containing the JSON representation, or an error /// if allocation fails. pub fn emitJson(metric: analytics.Metric, allocator: std.mem.Allocator) ![]u8 { return std.fmt.allocPrint( allocator, "{{\n \"name\": \"{s}\",\n \"mean\": {d:.3},\n \"deviation\": {d:.3},\n \"profile\": \"{s}\"\n}}\n", .{ metric.name, analytics.mean(metric), analytics.deviation(metric), analytics.highlight(metric) }, ); } ``` ```zig // Import standard library for core functionality const std = @import("std"); // Import analytics module for metric data structures const analytics = @import("analytics"); // Import reporting module for metric rendering const reporting = @import("reporting"); // Import adapters module for data format conversion const adapters = @import("adapters"); /// Application entry point demonstrating workspace dependency usage /// Shows how to use multiple workspace modules together for metric processing pub fn main() !void { // Create a fixed-size buffer for stdout operations to avoid dynamic allocation var stdout_buffer: [512]u8 = undefined; // Initialize a buffered writer for stdout to improve I/O performance var writer_state = std.fs.File.stdout().writer(&stdout_buffer); const out = &writer_state.interface; // Create a sample metric with response time measurements in milliseconds const metric = analytics.Metric{ .name = "response_ms", .values = &.{ 12.0, 12.4, 11.9, 12.1, 17.0, 12.3 }, }; // Render the metric using the reporting module's formatting try reporting.render(metric, out); // Initialize general purpose allocator for JSON serialization var gpa = std.heap.GeneralPurposeAllocator(.{}){}; // Ensure allocator cleanup on function exit defer _ = gpa.deinit(); // Convert metric to JSON format using the adapters module const json = try adapters.emitJson(metric, gpa.allocator()); // Free allocated JSON string when done defer gpa.allocator().free(json); // Output the JSON representation of the metric try out.print("json export: {s}\n", .{json}); // Flush buffered output to ensure all data is written try out.flush(); } ``` TIP: `std.fmt.allocPrint` pairs well with allocator plumbing when you want build-time helpers to operate without heap globals. Prefer it over ad-hoc `ArrayList` usage when emitting CSV or JSON snapshots in Zig 0.15.2. See v0.15.2 (https://ziglang.org/download/0.15.1/release-notes.html#upgrading-stdiogetstdoutwriterprint) and fmt.zig (https://github.com/ziglang/zig/tree/master/lib/std/fmt.zig). ### Subsection: Dependency hygiene checklist [section_id: dependency-hygiene] [section_url: https://zigbook.net/chapters/26__build-system-advanced-topics#dependency-hygiene] - Register vendored modules with distinct names and share them only via `.imports`. Do not leak them through `b.addModule` unless consumers are expected to import them directly. - Treat `zig-out/workspace-graph/module-graph.txt` as living documentation. Commit outputs for CI verification or diff them to catch accidental namespace changes. - For registry dependencies, forward `b.dependency()` handles exactly once and wrap them in local modules. This keeps upgrade churn isolated. 24 (24__zig-package-manager-deep.xml) ### Subsection: Build Options as Configuration [section_id: _build_options_as_configuration] [section_url: https://zigbook.net/chapters/26__build-system-advanced-topics#_build_options_as_configuration] Build options provide a powerful mechanism for making your workspace configurable. The following diagram shows how command-line `-D` flags flow through `b.option()`, get added to a generated module via `b.addOptions()`, and become compile-time constants accessible via `@import("build_options")`: ```text graph LR subgraph "Command Line" CLI["-Ddebug-allocator
-Denable-llvm
-Dversion-string
etc."] end subgraph "build.zig" PARSE["b.option()
Parse options"] OPTIONS["exe_options =
b.addOptions()"] ADD["exe_options.addOption()"] PARSE --> OPTIONS OPTIONS --> ADD end subgraph "Generated Module" BUILD_OPTIONS["build_options
(auto-generated)"] CONSTANTS["pub const mem_leak_frames = 4;
pub const have_llvm = true;
pub const version = '0.16.0';
etc."] BUILD_OPTIONS --> CONSTANTS end subgraph "Compiler Source" IMPORT["@import('build_options')"] USE["if (build_options.have_llvm) { ... }"] IMPORT --> USE end CLI --> PARSE ADD --> BUILD_OPTIONS BUILD_OPTIONS --> IMPORT ``` This pattern is essential for parameterized workspaces. Use `b.option(bool, "feature-x", "Enable feature X")` to declare options, then call `options.addOption("feature_x", feature_x)` to make them available at compile time. The generated module is automatically rebuilt when options change, ensuring your binaries always reflect the current configuration. This technique works for version strings, feature flags, debug settings, and any other build-time constant your code needs. ## Section: Target Matrices and Release Channels [section_id: target-matrix] [section_url: https://zigbook.net/chapters/26__build-system-advanced-topics#target-matrix] Complex projects often ship multiple binaries: debug utilities for contributors, ReleaseFast builds for production, and WASI artifacts for automation. Rather than duplicating build logic per target, assemble a matrix that iterates over `std.Target.Query` definitions. ### Subsection: Understanding Target Resolution [section_id: _understanding_target_resolution] [section_url: https://zigbook.net/chapters/26__build-system-advanced-topics#_understanding_target_resolution] Before iterating over targets, it’s important to understand how `b.resolveTargetQuery` transforms partial specifications into fully-resolved targets. The following diagram shows the resolution process: ```text graph LR subgraph "User Input" Query["Target.Query"] Query --> QCpu["cpu_arch: ?Cpu.Arch"] Query --> QModel["cpu_model: CpuModel"] Query --> QOs["os_tag: ?Os.Tag"] Query --> QAbi["abi: ?Abi"] end subgraph "Resolution Process" Resolve["resolveTargetQuery()"] Query --> Resolve Detection["Native Detection"] Defaults["Apply Defaults"] Detection --> Resolve Defaults --> Resolve end subgraph "Fully Resolved" Target["Target"] Resolve --> Target Target --> TCpu["cpu: Cpu"] Target --> TOs["os: Os"] Target --> TAbi["abi: Abi"] Target --> TOfmt["ofmt: ObjectFormat"] end ``` When you pass a `Target.Query` with `null` CPU or OS fields, the resolver detects your native platform and fills in concrete values. Similarly, if you specify an OS without an ABI, the resolver applies the default ABI for that OS (e.g., `.gnu` for Linux, `.msvc` for Windows). This resolution happens once per query and produces a `ResolvedTarget` containing the fully-specified `Target` plus metadata about whether values came from native detection. Understanding this distinction is crucial for cross-compilation: a query with `.cpu_arch = .x86_64` and `.os_tag = .linux` yields a different resolved target on each host platform due to CPU model and feature detection. ```zig const std = @import("std"); /// Represents a target/optimization combination in the build matrix /// Each combo defines a unique build configuration with a descriptive name const Combo = struct { /// Human-readable identifier for this build configuration name: []const u8, /// Target query specifying the CPU architecture, OS, and ABI query: std.Target.Query, /// Optimization level (Debug, ReleaseSafe, ReleaseFast, or ReleaseSmall) optimize: std.builtin.OptimizeMode, }; pub fn build(b: *std.Build) void { // Define a matrix of target/optimization combinations to build // This demonstrates cross-compilation capabilities and optimization strategies const combos = [_]Combo{ // Native build with debug symbols for development .{ .name = "native-debug", .query = .{}, .optimize = .Debug }, // Linux x86_64 build optimized for maximum performance .{ .name = "linux-fast", .query = .{ .cpu_arch = .x86_64, .os_tag = .linux, .abi = .gnu }, .optimize = .ReleaseFast }, // WebAssembly build optimized for minimal binary size .{ .name = "wasi-small", .query = .{ .cpu_arch = .wasm32, .os_tag = .wasi }, .optimize = .ReleaseSmall }, }; // Create a top-level step that builds all target/optimize combinations // Users can invoke this with `zig build matrix` const matrix_step = b.step("matrix", "Build every target/optimize pair"); // Track the run step for the first (host) executable to create a sanity check var host_run_step: ?*std.Build.Step = null; // Iterate through each combo to create and configure build artifacts for (combos, 0..) |combo, index| { // Resolve the target query into a concrete target specification // This validates the query and fills in any unspecified fields with defaults const resolved = b.resolveTargetQuery(combo.query); // Create a module with the resolved target and optimization settings // Using createModule allows precise control over compilation parameters const module = b.createModule(.{ .root_source_file = b.path("matrix/app.zig"), .target = resolved, .optimize = combo.optimize, }); // Create an executable artifact with a unique name for this combo // The name includes the combo identifier to distinguish build outputs const exe = b.addExecutable(.{ .name = b.fmt("matrix-{s}", .{combo.name}), .root_module = module, }); // Install the executable to zig-out/bin for distribution b.installArtifact(exe); // Add this executable's build step as a dependency of the matrix step // This ensures all executables are built when running `zig build matrix` matrix_step.dependOn(&exe.step); // For the first combo (assumed to be the native/host target), // create a run step for quick testing and validation if (index == 0) { // Create a command to run the host executable const run_cmd = b.addRunArtifact(exe); // Forward any command-line arguments to the executable if (b.args) |args| { run_cmd.addArgs(args); } // Create a dedicated step for running the host variant const run_step = b.step("run-host", "Run host variant for sanity checks"); run_step.dependOn(&run_cmd.step); // Store the run step for later use in the matrix step host_run_step = run_step; } } // If a host run step was created, add it as a dependency to the matrix step // This ensures that building the matrix also runs a sanity check on the host executable if (host_run_step) |run_step| { matrix_step.dependOn(run_step); } } ``` Key techniques: - Predeclare a slice of `{ name, query, optimize }` combos. Queries match `zig build -Dtarget` semantics but stay type-checked. - `b.resolveTargetQuery` converts each query into a `ResolvedTarget` so the module inherits canonical CPU/OS defaults. - Aggregating everything under a `matrix` step keeps CI wiring clean: call `zig build -Drelease-mode=fast matrix` (or leave defaults) and let dependencies ensure artefacts exist. - Running the first (host) target as part of the matrix catches regressions without cross-runner emulation. For deeper coverage, enable `b.enable_qemu` / `b.enable_wasmtime` before calling `addRunArtifact`. Run matrix build: ```shell $ zig build --build-file 02_multi_target_matrix.zig matrix ``` Output (host variant): ```text target: x86_64-linux-gnu optimize: Debug ``` ### Subsection: Running Cross-Compiled Targets [section_id: _running_cross_compiled_targets] [section_url: https://zigbook.net/chapters/26__build-system-advanced-topics#_running_cross_compiled_targets] When your matrix includes cross-compilation targets, you’ll need external executors to actually run the binaries. The build system automatically selects the appropriate executor based on host/target compatibility: ```text flowchart TD Start["getExternalExecutor(host, candidate)"] CheckMatch{"OS + CPU\ncompatible?"} CheckDL{"link_libc &&\nhas dynamic_linker?"} DLExists{"Dynamic linker\nexists on host?"} Native["Executor.native"] CheckRosetta{"macOS + arm64 host\n&& x86_64 target?"} Rosetta["Executor.rosetta"] CheckQEMU{"OS matches &&\nallow_qemu?"} QEMU["Executor.qemu\n(e.g., 'qemu-aarch64')"] CheckWasmtime{"target.isWasm() &&\nallow_wasmtime?"} Wasmtime["Executor.wasmtime"] CheckWine{"target.os == .windows\n&& allow_wine?"} Wine["Executor.wine"] CheckDarling{"target.os.isDarwin()\n&& allow_darling?"} Darling["Executor.darling"] BadDL["Executor.bad_dl"] BadOsCpu["Executor.bad_os_or_cpu"] Start --> CheckMatch CheckMatch --> |Yes|CheckDL CheckMatch --> |No|CheckRosetta CheckDL --> |No libc|Native CheckDL --> |Has libc|DLExists DLExists --> |Yes|Native DLExists --> |No|BadDL CheckRosetta --> |Yes|Rosetta CheckRosetta --> |No|CheckQEMU CheckQEMU --> |Yes|QEMU CheckQEMU --> |No|CheckWasmtime CheckWasmtime --> |Yes|Wasmtime CheckWasmtime --> |No|CheckWine CheckWine --> |Yes|Wine CheckWine --> |No|CheckDarling CheckDarling --> |Yes|Darling CheckDarling --> |No|BadOsCpu ``` Enable emulators in your build script by setting `b.enable_qemu = true` or `b.enable_wasmtime = true` before calling `addRunArtifact`. On macOS ARM hosts, x86_64 targets automatically use Rosetta 2. For Linux cross-architecture testing, QEMU user-mode emulation runs ARM/RISC-V/MIPS binaries transparently when the OS matches. WASI targets require Wasmtime, while Windows binaries on Linux can use Wine. If no executor is available, the run step will fail with `Executor.bad_os_or_cpu`—detect this early by testing matrix coverage on representative CI hosts. CAUTION: Cross targets that rely on native system libraries (e.g. glibc) need appropriate sysroot packs. Populate `ZIG_LIBC` or configure `b.libc_file` before adding those combos to production pipelines. ## Section: Vendoring vs Registry Dependencies [section_id: vendoring-vs-registry] [section_url: https://zigbook.net/chapters/26__build-system-advanced-topics#vendoring-vs-registry] - Registry-first approach: keep `build.zig.zon` hashes authoritative, then register each dependency module via `b.dependency()` and `module.addImport()`. 24 (24__zig-package-manager-deep.xml) - Vendor-first approach: drop sources into `deps//` and wire them with `b.addAnonymousModule` or `b.createModule`. Document the provenance in `module-graph.txt` so collaborators know which code is pinned locally. - Whichever strategy you choose, record a policy in CI: a step that fails if `zig out/workspace-graph/module-graph.txt` changes unexpectedly, or a lint that checks vendored directories for LICENSE files. ## Section: CI Scenarios and Automation Hooks [section_id: ci-scenarios] [section_url: https://zigbook.net/chapters/26__build-system-advanced-topics#ci-scenarios] ### Subsection: Step Dependencies in Practice [section_id: _step_dependencies_in_practice] [section_url: https://zigbook.net/chapters/26__build-system-advanced-topics#_step_dependencies_in_practice] CI pipelines benefit from understanding how build steps compose. The following diagram shows a real-world step dependency graph from the Zig compiler’s own build system: ```text graph TB subgraph "Installation Step (default)" INSTALL["b.getInstallStep()"] end subgraph "Compiler Artifacts" EXE_STEP["exe.step
(compile compiler)"] INSTALL_EXE["install_exe.step
(install binary)"] end subgraph "Documentation" LANGREF["generateLangRef()"] INSTALL_LANGREF["install_langref.step"] STD_DOCS_GEN["autodoc_test"] INSTALL_STD_DOCS["install_std_docs.step"] end subgraph "Library Files" LIB_FILES["installDirectory(lib/)"] end subgraph "Test Steps" TEST["test step"] FMT["test-fmt step"] CASES["test-cases step"] MODULES["test-modules step"] end INSTALL --> INSTALL_EXE INSTALL --> INSTALL_LANGREF INSTALL --> LIB_FILES INSTALL_EXE --> EXE_STEP INSTALL_LANGREF --> LANGREF INSTALL --> INSTALL_STD_DOCS INSTALL_STD_DOCS --> STD_DOCS_GEN TEST --> EXE_STEP TEST --> FMT TEST --> CASES TEST --> MODULES CASES --> EXE_STEP MODULES --> EXE_STEP ``` Notice how the default install step (`zig build`) depends on binary installation, documentation, and library files—but not tests. Meanwhile, the test step depends on compilation plus all test substeps. This separation lets CI run `zig build` for release artifacts and `zig build test` for validation in parallel jobs. Each step only executes when its dependencies change, thanks to content-addressed caching. You can inspect this graph locally with `zig build --verbose` or by adding a custom step that dumps dependencies. ### Subsection: Automation Patterns [section_id: _automation_patterns] [section_url: https://zigbook.net/chapters/26__build-system-advanced-topics#_automation_patterns] - Artifact verification: Add a `zig build graph` job that uploads `module-graph.txt` alongside compiled binaries. Consumers can diff namespaces between releases. - Matrix extension: Parameterize the combos array via build options (`-Dinclude-windows=true`). Use `b.option(bool, "include-windows", …)` to let CI toggle extra targets without editing source. - Security posture: Pipe `zig build --fetch` (Chapter 24) into the matrix run so caches populate before cross jobs run offline. See 24 (24__zig-package-manager-deep.xml). - Reproducibility: Teach CI to run `zig build install` twice and assert no files change between runs. Because `std.Build` respects content hashing, the second invocation should no-op unless inputs changed. ### Subsection: Advanced Test Organization [section_id: _advanced_test_organization] [section_url: https://zigbook.net/chapters/26__build-system-advanced-topics#_advanced_test_organization] For comprehensive projects, organizing tests into categories with matrix application requires careful step composition. The following diagram shows a production-grade test hierarchy: ```text graph TB subgraph "Test Steps" TEST_STEP["test step
(umbrella step)"] FMT["test-fmt
Format checking"] CASES["test-cases
Compiler test cases"] MODULES["test-modules
Per-target module tests"] UNIT["test-unit
Compiler unit tests"] STANDALONE["Standalone tests"] CLI["CLI tests"] STACK_TRACE["Stack trace tests"] ERROR_TRACE["Error trace tests"] LINK["Link tests"] C_ABI["C ABI tests"] INCREMENTAL["test-incremental
Incremental compilation"] end subgraph "Module Tests" BEHAVIOR["behavior tests
test/behavior.zig"] COMPILER_RT["compiler_rt tests
lib/compiler_rt.zig"] ZIGC["zigc tests
lib/c.zig"] STD["std tests
lib/std/std.zig"] LIBC_TESTS["libc tests"] end subgraph "Test Configuration" TARGET_MATRIX["test_targets array
Different architectures
Different OSes
Different ABIs"] OPT_MODES["Optimization modes:
Debug, ReleaseFast
ReleaseSafe, ReleaseSmall"] FILTERS["test-filter
test-target-filter"] end TEST_STEP --> FMT TEST_STEP --> CASES TEST_STEP --> MODULES TEST_STEP --> UNIT TEST_STEP --> STANDALONE TEST_STEP --> CLI TEST_STEP --> STACK_TRACE TEST_STEP --> ERROR_TRACE TEST_STEP --> LINK TEST_STEP --> C_ABI TEST_STEP --> INCREMENTAL MODULES --> BEHAVIOR MODULES --> COMPILER_RT MODULES --> ZIGC MODULES --> STD TARGET_MATRIX --> MODULES OPT_MODES --> MODULES FILTERS --> MODULES ``` The umbrella test step aggregates all test categories, letting you run the full suite with `zig build test`. Individual categories can be invoked separately (`zig build test-fmt`, `zig build test-modules`) for faster iteration. Notice how only the module tests receive matrix configuration—format checking and CLI tests don’t vary by target. Use `b.option([]const u8, "test-filter", …)` to let CI run subsets, and apply optimization modes selectively based on test type. This pattern scales to hundreds of test files while keeping build times manageable through parallel execution and caching. ## Section: Notes & Caveats [section_id: notes-caveats] [section_url: https://zigbook.net/chapters/26__build-system-advanced-topics#notes-caveats] - `b.addModule` registers a name globally for the current build graph; `b.createModule` keeps the module private. Mixing them up leads to surprising imports or missing symbols. 25 (25__module-resolution-and-discovery-deep.xml) - Named write-files respect the cache. Delete `.zig-cache` if you need to regenerate them from scratch; otherwise the step can trick you into thinking a change landed when it actually hit the cache. - When iterating matrices, always prune stale binaries with `zig build uninstall` (or a custom `Step.RemoveDir`) to avoid cross-version confusion. ### Subsection: Under the Hood: Dependency Tracking [section_id: _under_the_hood_dependency_tracking] [section_url: https://zigbook.net/chapters/26__build-system-advanced-topics#_under_the_hood_dependency_tracking] The build system’s caching and incremental behavior relies on the compiler’s sophisticated dependency tracking infrastructure. Understanding this helps explain why cached builds are so fast and why certain changes trigger broader rebuilds than expected. ```text graph TB subgraph "InternPool - Dependency Storage" SRCHASHDEPS["src_hash_deps
Map: TrackedInst.Index → DepEntry.Index"] NAVVALDEPS["nav_val_deps
Map: Nav.Index → DepEntry.Index"] NAVTYDEPS["nav_ty_deps
Map: Nav.Index → DepEntry.Index"] INTERNEDDEPS["interned_deps
Map: Index → DepEntry.Index"] ZONFILEDEPS["zon_file_deps
Map: FileIndex → DepEntry.Index"] EMBEDFILEDEPS["embed_file_deps
Map: EmbedFile.Index → DepEntry.Index"] NSDEPS["namespace_deps
Map: TrackedInst.Index → DepEntry.Index"] NSNAMEDEPS["namespace_name_deps
Map: NamespaceNameKey → DepEntry.Index"] FIRSTDEP["first_dependency
Map: AnalUnit → DepEntry.Index"] DEPENTRIES["dep_entries
ArrayListUnmanaged"] FREEDEP["free_dep_entries
ArrayListUnmanaged"] end subgraph "DepEntry Structure" DEPENTRY["DepEntry
{depender: AnalUnit,
next_dependee: DepEntry.Index.Optional,
next_depender: DepEntry.Index.Optional}"] end SRCHASHDEPS --> DEPENTRIES NAVVALDEPS --> DEPENTRIES NAVTYDEPS --> DEPENTRIES INTERNEDDEPS --> DEPENTRIES ZONFILEDEPS --> DEPENTRIES EMBEDFILEDEPS --> DEPENTRIES NSDEPS --> DEPENTRIES NSNAMEDEPS --> DEPENTRIES FIRSTDEP --> DEPENTRIES DEPENTRIES --> DEPENTRY FREEDEP -.->|"reuses indices from"| DEPENTRIES ``` The compiler tracks dependencies at multiple granularities: source file hashes (`src_hash_deps`), navigation values (`nav_val_deps`), types (`nav_ty_deps`), interned constants, ZON files, embedded files, and namespace membership. All these maps point into a shared `dep_entries` array containing `DepEntry` structures that form linked lists. Each entry participates in two lists: one linking all analysis units that depend on a particular dependee (traversed during invalidation), and one linking all dependees of a particular analysis unit (traversed during cleanup). When you modify a source file, the compiler hashes it, looks up dependents in `src_hash_deps`, and marks only those analysis units as outdated. This granular tracking is why changing a private function in one file doesn’t rebuild unrelated modules—the dependency graph precisely captures what actually depends on what. The build system leverages this infrastructure through content addressing: step outputs are cached by their input hashes, and reused when inputs haven’t changed. ## Section: Exercises [section_id: exercises] [section_url: https://zigbook.net/chapters/26__build-system-advanced-topics#exercises] - Extend `01_workspace_build.zig` so the `graph` step emits both a human-readable table and a JSON document. Hint: call `graph_files.add("module-graph.json", …)` with `std.json` output. See json.zig (https://github.com/ziglang/zig/tree/master/lib/std/json.zig). - Add a `-Dtarget-filter` option to `02_multi_target_matrix.zig` that limits matrix execution to a comma-separated allowlist. Use `std.mem.splitScalar` to parse the value. 22 (22__build-system-deep-dive.xml) - Introduce a registry dependency via `b.dependency("logging", .{})` and expose it to the workspace with `module.addImport("logging", dep.module("logging"))`. Document the new namespace in `module-graph.txt`. ## Section: Caveats, alternatives, edge cases [section_id: caveats-alternatives-edge-cases] [section_url: https://zigbook.net/chapters/26__build-system-advanced-topics#caveats-alternatives-edge-cases] - Large workspaces might exceed default install directory limits. Use `b.setInstallPrefix` or `b.setLibDir` before adding artifacts to route outputs into per-target directories. - On Windows, `resolveTargetQuery` requires `abi = .msvc` if you expect MSVC-compatible artifacts; the default `.gnu` ABI yields MinGW binaries. - If you supply anonymous modules to dependencies, remember they are not deduplicated. Reuse the same `b.createModule` instance when multiple artefacts need the same vendored code. ## Section: Summary [section_id: summary] [section_url: https://zigbook.net/chapters/26__build-system-advanced-topics#summary] - Workspaces stay predictable when you register every module explicitly and document the mapping via named write-files. - `resolveTargetQuery` and iteration-friendly combos let you scale to multiple targets without copy/pasting build logic. - CI jobs benefit from `std.Build` primitives: steps articulate dependencies, run artefacts gate sanity checks, and named artefacts capture reproducible metadata. Together with Chapters 22–25, you now have the tools to craft deterministic Zig build graphs that scale across packages, targets, and release channels. # Chapter 27 — Project [chapter_id: 27__project-multi-package-workspace-and-vendor] [chapter_slug: project-multi-package-workspace-and-vendor] [chapter_number: 27] [chapter_url: https://zigbook.net/chapters/27__project-multi-package-workspace-and-vendor] ## Section: Overview [section_id: overview] [section_url: https://zigbook.net/chapters/27__project-multi-package-workspace-and-vendor#overview] Chapter 26 (26__build-system-advanced-topics.xml) explored advanced `std.Build` techniques for coordinating workspaces and matrix builds. This project chapter puts those tools to work: we will assemble a three-package workspace featuring two reusable libraries, a vendored ANSI palette, and an application that renders a latency dashboard. Along the way, we capture metadata with named write-files and install the artefact into `zig-out`, demonstrating how vendor-first workflows coexist with registry-ready modules (see Build.zig (https://github.com/ziglang/zig/tree/master/lib/std/Build.zig) and Dir.zig (https://github.com/ziglang/zig/tree/master/lib/std/Build/Step/InstallDir.zig)). The example is intentionally compact yet realistic—`libA` performs statistical analysis, `libB` formats status lines, and the vendored palette keeps terminal colouring private to the workspace. The build graph registers only the contracts we want consumers to see, mirroring the hygiene rules from the previous concept chapters. 25 (25__module-resolution-and-discovery-deep.xml) ## Section: Learning Goals [section_id: learning-goals] [section_url: https://zigbook.net/chapters/27__project-multi-package-workspace-and-vendor#learning-goals] - Wire multiple libraries and a vendored helper into a single workspace using a shared `deps.zig` registration function (see 26 (26__build-system-advanced-topics.xml) and Module.zig (https://github.com/ziglang/zig/tree/master/lib/std/Build/Module.zig)). - Generate reproducible artefacts (a dependency map) with named write-files and install them into `zig-out` for CI inspection (see File.zig (https://github.com/ziglang/zig/tree/master/lib/std/Build/Step/WriteFile.zig)). - Validate component libraries through `zig build test`, ensuring vendored code participates in the same test harness as registry packages (see testing.zig (https://github.com/ziglang/zig/tree/master/lib/std/testing.zig)). - Apply Zig 0.15.2’s buffered writer API in an application that consumes the workspace modules (see #upgrading stdiogetstdoutwriterprint (https://ziglang.org/download/0.15.1/release-notes.html#upgrading-stdiogetstdoutwriterprint)). ## Section: Workspace blueprint [section_id: workspace-blueprint] [section_url: https://zigbook.net/chapters/27__project-multi-package-workspace-and-vendor#workspace-blueprint] The workspace lives under `chapters-data/code/27__project-multi-package-workspace-and-vendor/`. A minimal manifest declares the package name and the directories that should ship with any release, keeping vendored sources explicit (see build.zig.zon template (https://github.com/ziglang/zig/blob/master/lib/init/build.zig.zon)). ### Subsection: Manifest and layout [section_id: manifest-layout] [section_url: https://zigbook.net/chapters/27__project-multi-package-workspace-and-vendor#manifest-layout] ```zig .{ // Package identifier following Zig naming conventions .name = .workspace_dashboard, // Semantic version for this workspace (major.minor.patch) .version = "0.1.0", // Minimum Zig compiler version required to build this project .minimum_zig_version = "0.15.2", // Explicit list of paths included in the package for distribution and source tracking // This controls what gets packaged when this project is published or vendored .paths = .{ "build.zig", // Main build script "build.zig.zon", // This manifest file "deps.zig", // Centralized dependency configuration "app", // Application entry point and executable code "packages", // Local workspace packages (libA, libB) "vendor", // Vendored third-party dependencies (palette) }, // External dependencies fetched from remote sources // Empty in this workspace as all dependencies are local/vendored .dependencies = .{}, // Cryptographic hash for integrity verification of the package manifest // Automatically computed by the Zig build system .fingerprint = 0x88b8c5fe06a5c6a1, } ``` Run: ```shell $ zig build --build-file build.zig map ``` Output: ```shell no output ``` TIP: Running `map` installs `zig-out/workspace-artifacts/dependency-map.txt`, making the package surface auditable without combing through source trees. ### Subsection: Wiring packages with [section_id: deps-pattern] [section_url: https://zigbook.net/chapters/27__project-multi-package-workspace-and-vendor#deps-pattern] `deps.zig` centralises module registration so every consumer—tests, executables, or future examples—receives the same wiring. We register `libA` and `libB` under public names, while the ANSI palette stays anonymous via `b.createModule`. ```zig // Import the standard library for build system types and utilities const std = @import("std"); // Container struct that holds references to project modules // This allows centralized access to all workspace modules pub const Modules = struct { libA: *std.Build.Module, libB: *std.Build.Module, }; // Creates and configures all project modules with their dependencies // This function sets up the module dependency graph for the workspace: // - palette: vendored external dependency // - libA: internal package with no dependencies // - libB: internal package that depends on both libA and palette // // Parameters: // b: Build instance used to create modules // target: Compilation target (architecture, OS, ABI) // optimize: Optimization mode (Debug, ReleaseSafe, ReleaseFast, ReleaseSmall) // // Returns: Modules struct containing references to libA and libB pub fn addModules( b: *std.Build, target: std.Build.ResolvedTarget, optimize: std.builtin.OptimizeMode, ) Modules { // Create module for the vendored palette library // Located in vendor directory as an external dependency const palette_mod = b.createModule(.{ .root_source_file = b.path("vendor/palette/palette.zig"), .target = target, .optimize = optimize, }); // Create module for libA (analytics functionality) // This is a standalone library with no external dependencies const lib_a = b.addModule("libA", .{ .root_source_file = b.path("packages/libA/analytics.zig"), .target = target, .optimize = optimize, }); // Create module for libB (report functionality) // Depends on both libA and palette, establishing the dependency chain const lib_b = b.addModule("libB", .{ .root_source_file = b.path("packages/libB/report.zig"), .target = target, .optimize = optimize, // Import declarations allow libB to access libA and palette modules .imports = &.{ .{ .name = "libA", .module = lib_a }, .{ .name = "palette", .module = palette_mod }, }, }); // Return configured modules for use in build scripts return Modules{ .libA = lib_a, .libB = lib_b, }; } ``` Run: ```shell $ zig build --build-file build.zig test ``` Output: ```shell no output ``` IMPORTANT: Returning module handles keeps callers honest—only `build.zig` decides which names become public imports, an approach that aligns with the namespace rules from Chapter 25. 25 (25__module-resolution-and-discovery-deep.xml) ### Subsection: Build graph orchestration [section_id: build-orchestration] [section_url: https://zigbook.net/chapters/27__project-multi-package-workspace-and-vendor#build-orchestration] The build script installs the executable, exposes `run`, `test`, and `map` steps, and copies the generated dependency map into `zig-out/workspace-artifacts/`. ```zig const std = @import("std"); const deps = @import("deps.zig"); /// Build script for a multi-package workspace demonstrating dependency management. /// Orchestrates compilation of an executable that depends on local packages (libA, libB) /// and a vendored dependency (palette), plus provides test and documentation steps. pub fn build(b: *std.Build) void { // Parse target platform and optimization level from command-line options const target = b.standardTargetOptions(.{}); const optimize = b.standardOptimizeOption(.{}); // Load all workspace modules (libA, libB, palette) via deps.zig // This centralizes dependency configuration and makes modules available for import const modules = deps.addModules(b, target, optimize); // Create the root module for the main executable // Explicitly declares dependencies on libA and libB, making them importable const root_module = b.createModule(.{ .root_source_file = b.path("app/main.zig"), .target = target, .optimize = optimize, .imports = &.{ // Map import names to actual modules loaded from deps.zig .{ .name = "libA", .module = modules.libA }, .{ .name = "libB", .module = modules.libB }, }, }); // Define the executable artifact using the configured root module const exe = b.addExecutable(.{ .name = "workspace-dashboard", .root_module = root_module, }); // Register the executable for installation into zig-out/bin b.installArtifact(exe); // Create a command to run the built executable const run_cmd = b.addRunArtifact(exe); // Forward any extra command-line arguments to the executable if (b.args) |args| run_cmd.addArgs(args); // Register "zig build run" step to compile and execute the dashboard const run_step = b.step("run", "Run the latency dashboard"); run_step.dependOn(&run_cmd.step); // Create test executables for each library module // These will run any tests defined in the respective library source files const lib_a_tests = b.addTest(.{ .root_module = modules.libA }); const lib_b_tests = b.addTest(.{ .root_module = modules.libB }); // Register "zig build test" step to run all library test suites const tests_step = b.step("test", "Run library test suites"); tests_step.dependOn(&b.addRunArtifact(lib_a_tests).step); tests_step.dependOn(&b.addRunArtifact(lib_b_tests).step); // Generate a text file documenting the workspace module structure // This serves as human-readable documentation of the dependency graph const mapping = b.addNamedWriteFiles("workspace-artifacts"); _ = mapping.add("dependency-map.txt", \\Modules registered in build.zig: \\ libA -> packages/libA/analytics.zig \\ libB -> packages/libB/report.zig (imports libA, palette) \\ palette -> vendor/palette/palette.zig (anonymous) \\ executable -> app/main.zig ); // Install the generated documentation into zig-out/workspace-artifacts const install_map = b.addInstallDirectory(.{ .source_dir = mapping.getDirectory(), .install_dir = .prefix, .install_subdir = "workspace-artifacts", }); // Register "zig build map" step to generate and install dependency documentation const map_step = b.step("map", "Emit dependency map to zig-out"); map_step.dependOn(&install_map.step); } ``` Run: ```shell $ zig build --build-file build.zig run ``` Output: ```shell dataset status mean range samples ------------------------------------------------------ frontend stable 111.80 3.90 5 checkout stable 100.60 6.40 5 analytics alert 77.42 24.00 5 ``` NOTE: The dependency map written by the `map` step renders as: ```text Modules registered in build.zig: libA -> packages/libA/analytics.zig libB -> packages/libB/report.zig (imports libA, palette) palette -> vendor/palette/palette.zig (anonymous) executable -> app/main.zig ``` ## Section: Library modules [section_id: library-modules] [section_url: https://zigbook.net/chapters/27__project-multi-package-workspace-and-vendor#library-modules] Two libraries share responsibility: `libA` performs numeric analysis, `libB` transforms those statistics into colour-coded rows. Tests live alongside each module so the build graph can execute them without additional glue. ### Subsection: Analytics core () [section_id: libA-analytics] [section_url: https://zigbook.net/chapters/27__project-multi-package-workspace-and-vendor#libA-analytics] `libA` implements Welford’s algorithm for stable variance computation and exposes convenience helpers such as `relativeSpread` and `zScore`. math.zig (https://github.com/ziglang/zig/tree/master/lib/std/math.zig) ```zig /// Statistical summary of a numerical dataset. /// Contains computed statistics including central tendency, spread, and sample size. const std = @import("std"); pub const Stats = struct { sample_count: usize, min: f64, max: f64, mean: f64, variance: f64, /// Calculates the range (difference between maximum and minimum values). pub fn range(self: Stats) f64 { return self.max - self.min; } /// Calculates the coefficient of variation (range divided by mean). /// Returns 0 if mean is 0 to avoid division by zero. pub fn relativeSpread(self: Stats) f64 { return if (self.mean == 0) 0 else self.range() / self.mean; } }; /// Computes descriptive statistics for a slice of floating-point values. /// Uses Welford's online algorithm for numerically stable variance calculation. /// Panics if the input slice is empty. pub fn analyze(values: []const f64) Stats { std.debug.assert(values.len > 0); var min_value: f64 = values[0]; var max_value: f64 = values[0]; var mean_value: f64 = 0.0; // M2 is the sum of squares of differences from the current mean (Welford's algorithm) var m2: f64 = 0.0; var index: usize = 0; while (index < values.len) : (index += 1) { const value = values[index]; // Track minimum and maximum values if (value < min_value) min_value = value; if (value > max_value) max_value = value; // Welford's online algorithm for mean and variance const count = index + 1; const delta = value - mean_value; mean_value += delta / @as(f64, @floatFromInt(count)); const delta2 = value - mean_value; m2 += delta * delta2; } // Calculate sample variance using Bessel's correction (n-1) const count_f = @as(f64, @floatFromInt(values.len)); const variance_value = if (values.len > 1) m2 / (count_f - 1.0) else 0.0; return Stats{ .sample_count = values.len, .min = min_value, .max = max_value, .mean = mean_value, .variance = variance_value, }; } /// Computes the sample standard deviation from precomputed statistics. /// Standard deviation is the square root of variance. pub fn sampleStdDev(stats: Stats) f64 { return std.math.sqrt(stats.variance); } /// Calculates the z-score (standard score) for a given value. /// Measures how many standard deviations a value is from the mean. /// Returns 0 if standard deviation is 0 to avoid division by zero. pub fn zScore(value: f64, stats: Stats) f64 { const dev = sampleStdDev(stats); if (dev == 0.0) return 0.0; return (value - stats.mean) / dev; } test "analyze returns correct statistics" { const data = [_]f64{ 12.0, 13.5, 11.8, 12.2, 12.0 }; const stats = analyze(&data); try std.testing.expectEqual(@as(usize, data.len), stats.sample_count); try std.testing.expectApproxEqRel(12.3, stats.mean, 1e-6); try std.testing.expectApproxEqAbs(1.7, stats.range(), 1e-6); } ``` Run: ```shell $ zig test packages/libA/analytics.zig ``` Output: ```shell All 1 tests passed. ``` ### Subsection: Reporting surface () [section_id: libB-report] [section_url: https://zigbook.net/chapters/27__project-multi-package-workspace-and-vendor#libB-report] `libB` depends on `libA` for statistics and the vendored palette for styling. It computes a status label per dataset and renders a compact table suitable for dashboards or CI logs. ```zig // Import standard library for testing utilities const std = @import("std"); // Import analytics package (libA) for statistical analysis const analytics = @import("libA"); // Import palette package for theming and styled output const palette = @import("palette"); /// Represents a named collection of numerical data points for analysis pub const Dataset = struct { name: []const u8, values: []const f64, }; /// Re-export Theme from palette package for consistent theming across reports pub const Theme = palette.Theme; /// Defines threshold values that determine status classification /// based on statistical spread of data pub const Thresholds = struct { watch: f64, // Threshold for watch status (lower severity) alert: f64, // Threshold for alert status (higher severity) }; /// Represents the health status of a dataset based on its statistical spread pub const Status = enum { stable, watch, alert }; /// Determines the status of a dataset by comparing its relative spread /// against defined thresholds pub fn status(stats: analytics.Stats, thresholds: Thresholds) Status { const spread = stats.relativeSpread(); // Check against alert threshold first (highest severity) if (spread >= thresholds.alert) return .alert; // Then check watch threshold (medium severity) if (spread >= thresholds.watch) return .watch; // Default to stable if below all thresholds return .stable; } /// Returns the default theme from the palette package pub fn defaultTheme() Theme { return palette.defaultTheme(); } /// Maps a Status value to its corresponding palette Tone for styling pub fn tone(status_value: Status) palette.Tone { return switch (status_value) { .stable => .stable, .watch => .watch, .alert => .alert, }; } /// Converts a Status value to its string representation pub fn label(status_value: Status) []const u8 { return switch (status_value) { .stable => "stable", .watch => "watch", .alert => "alert", }; } /// Renders a formatted table displaying statistical analysis of multiple datasets /// with color-coded status indicators based on thresholds pub fn renderTable( writer: anytype, data_sets: []const Dataset, thresholds: Thresholds, theme: Theme, ) !void { // Print table header with column names try writer.print("{s: <12} {s: <10} {s: <10} {s: <10} {s}\n", .{ "dataset", "status", "mean", "range", "samples", }); // Print separator line try writer.print("{s}\n", .{"-----------------------------------------------"}); // Process and display each dataset for (data_sets) |data| { // Compute statistics for current dataset const stats = analytics.analyze(data.values); const status_value = status(stats, thresholds); // Print dataset name try writer.print("{s: <12} ", .{data.name}); // Print styled status label with theme-appropriate color try palette.writeStyled(theme, tone(status_value), writer, label(status_value)); // Print statistical values: mean, range, and sample count try writer.print( " {d: <10.2} {d: <10.2} {d: <10}\n", .{ stats.mean, stats.range(), stats.sample_count }, ); } } // Verifies that status classification correctly responds to different // levels of data spread relative to defined thresholds test "status thresholds" { const thresholds = Thresholds{ .watch = 0.05, .alert = 0.12 }; // Test with tightly clustered values (low spread) - should be stable const tight = analytics.analyze(&.{ 99.8, 100.1, 100.0 }); try std.testing.expectEqual(Status.stable, status(tight, thresholds)); // Test with widely spread values (high spread) - should trigger alert const drift = analytics.analyze(&.{ 100.0, 112.0, 96.0 }); try std.testing.expectEqual(Status.alert, status(drift, thresholds)); } ``` Run: ```shell $ zig build --build-file build.zig test ``` Output: ```shell no output ``` TIP: Testing through `zig build test` ensures the module sees `libA` and the palette via the same imports the executable uses, eliminating discrepancies between direct `zig test` runs and build-orchestrated runs. ### Subsection: Vendored theme palette [section_id: vendor-palette] [section_url: https://zigbook.net/chapters/27__project-multi-package-workspace-and-vendor#vendor-palette] The ANSI palette stays private to the workspace—`deps.zig` injects it where needed without registering a public name. This keeps colour codes stable even if the workspace later consumes a registry dependency with conflicting helpers. ```zig // Import the standard library for testing utilities const std = @import("std"); // Defines the three tonal categories for styled output pub const Tone = enum { stable, watch, alert }; // Represents a color theme with ANSI escape codes for different tones // Each tone has a start sequence and there's a shared reset sequence pub const Theme = struct { stable_start: []const u8, watch_start: []const u8, alert_start: []const u8, reset: []const u8, // Returns the appropriate ANSI start sequence for the given tone pub fn start(self: Theme, tone: Tone) []const u8 { return switch (tone) { .stable => self.stable_start, .watch => self.watch_start, .alert => self.alert_start, }; } }; // Creates a default theme with standard terminal colors: // stable (green), watch (yellow), alert (red) pub fn defaultTheme() Theme { return Theme{ .stable_start = "\x1b[32m", // green .watch_start = "\x1b[33m", // yellow .alert_start = "\x1b[31m", // red .reset = "\x1b[0m", }; } // Writes styled text to the provided writer by wrapping it with // ANSI color codes based on the theme and tone pub fn writeStyled(theme: Theme, tone: Tone, writer: anytype, text: []const u8) !void { try writer.print("{s}{s}{s}", .{ theme.start(tone), text, theme.reset }); } // Verifies that the default theme returns correct ANSI escape codes test "default theme colors" { const theme = defaultTheme(); try std.testing.expectEqualStrings("\x1b[32m", theme.start(.stable)); try std.testing.expectEqualStrings("\x1b[0m", theme.reset); } ``` Run: ```shell $ zig test vendor/palette/palette.zig ``` Output: ```shell All 1 tests passed. ``` ## Section: Application entry point [section_id: application-entry] [section_url: https://zigbook.net/chapters/27__project-multi-package-workspace-and-vendor#application-entry] The executable imports only the public modules, builds datasets, and prints the table using the buffered writer API introduced in Zig 0.15.2. ```zig // Main application entry point that demonstrates multi-package workspace usage // by generating a performance report table with multiple datasets. // Import the standard library for I/O operations const std = @import("std"); // Import the reporting library (libB) from the workspace const report = @import("libB"); /// Application entry point that creates and renders a performance monitoring report. /// Demonstrates integration with the libB package for generating formatted tables /// with threshold-based highlighting. pub fn main() !void { // Allocate a fixed buffer for stdout to avoid dynamic allocation var stdout_buffer: [1024]u8 = undefined; // Create a buffered writer for efficient stdout operations var writer_state = std.fs.File.stdout().writer(&stdout_buffer); // Get the generic writer interface for use with the report library const out = &writer_state.interface; // Define sample performance datasets for different system components // Each dataset contains a component name and an array of performance values const datasets = [_]report.Dataset{ .{ .name = "frontend", .values = &.{ 112.0, 109.5, 113.4, 112.2, 111.9 } }, .{ .name = "checkout", .values = &.{ 98.0, 101.0, 104.4, 99.1, 100.5 } }, .{ .name = "analytics", .values = &.{ 67.0, 89.4, 70.2, 91.0, 69.5 } }, }; // Configure monitoring thresholds: 8% variance triggers watch, 20% triggers alert const thresholds = report.Thresholds{ .watch = 0.08, .alert = 0.2 }; // Use the default color theme provided by the report library const theme = report.defaultTheme(); // Render the formatted report table to the buffered writer try report.renderTable(out, &datasets, thresholds, theme); // Flush the buffer to ensure all output is written to stdout try out.flush(); } ``` Run: ```shell $ zig build --build-file build.zig run ``` Output: ```shell dataset status mean range samples ------------------------------------------------------ frontend stable 111.80 3.90 5 checkout stable 100.60 6.40 5 analytics alert 77.42 24.00 5 ``` ## Section: Notes & Caveats [section_id: notes-caveats] [section_url: https://zigbook.net/chapters/27__project-multi-package-workspace-and-vendor#notes-caveats] - The workspace exposes only `libA` and `libB`; vendored modules remain anonymous thanks to `b.createModule`, preventing downstream consumers from relying on internal helpers. - Named write-files produce deterministic artefacts. Pair the `map` step with CI to detect accidental namespace changes before they reach production. - `zig build test` composes multiple module tests under a single command; if you add new packages, remember to thread their modules through `deps.zig` so they join the suite. ## Section: Exercises [section_id: exercises] [section_url: https://zigbook.net/chapters/27__project-multi-package-workspace-and-vendor#exercises] - Extend the dependency map to emit JSON alongside the text file. Hint: add a second `mapping.add("dependency-map.json", …​)` and reuse `std.json` to serialise the structure. 26 (26__build-system-advanced-topics.xml), json.zig (https://github.com/ziglang/zig/tree/master/lib/std/json.zig) - Add a registry dependency via `b.dependency("logger", .{})`, re-export its module in `deps.zig`, and update the map to document the new namespace. 24 (24__zig-package-manager-deep.xml) - Introduce a `-Dalert-spread` option that overrides the default thresholds. Forward the option through `deps.zig` so both the executable and any tests see the same policy. ## Section: Caveats, alternatives, edge cases [section_id: caveats-alternatives-edge-cases] [section_url: https://zigbook.net/chapters/27__project-multi-package-workspace-and-vendor#caveats-alternatives-edge-cases] - When the vendored palette eventually graduates to a standalone package, swap `b.createModule` for `b.addModule` and list it in `build.zig.zon` to ensure consumers fetch it via hashes. - If your workspace grows beyond a handful of modules, consider grouping registries in `deps.zig` by responsibility (`observability`, `storage`, etc.) so the build script stays navigable. 26 (26__build-system-advanced-topics.xml) - Cross-compiling the dashboard requires ensuring each target supports ANSI escapes; gate palette usage behind `builtin.os.tag` checks if you ship to Windows consoles without VT processing. builtin.zig (https://github.com/ziglang/zig/tree/master/lib/std/builtin.zig) ## Section: Summary [section_id: summary] [section_url: https://zigbook.net/chapters/27__project-multi-package-workspace-and-vendor#summary] - `deps.zig` centralises module registration, enabling repeatable workspaces that expose only sanctioned namespaces. - Named write-files and install directories turn build metadata into versionable artefacts ready for CI checks. - A vendored helper can coexist with reusable libraries, keeping internal colour schemes private while the public API remains clean. With this project you now have a concrete template for organising multi-package Zig workspaces, balancing vendored code with reusable libraries while keeping the build graph transparent and testable. # Chapter 28 — Filesystem & I/O [chapter_id: 28__filesystem-and-io] [chapter_slug: filesystem-and-io] [chapter_number: 28] [chapter_url: https://zigbook.net/chapters/28__filesystem-and-io] ## Section: Overview [section_id: overview] [section_url: https://zigbook.net/chapters/28__filesystem-and-io#overview] Workspace builds are only as useful as the data they sling around. After wiring a multi-package dashboard in Chapter 27, we now descend into the filesystem and I/O primitives that back every package install, log collector, and CLI tool you will write. See 27 (27__project-multi-package-workspace-and-vendor.xml). Zig v0.15.2 (https://ziglang.org/download/0.15.1/release-notes.html-) brings a unified `std.fs.File` surface with memoized metadata and a buffered-writer story that the changelog all but shouts about—use it, flush it, and keep handles tidy. See File.zig (https://github.com/ziglang/zig/tree/master/lib/std/fs/File.zig). ### Subsection: The Filesystem Architecture [section_id: _the_filesystem_architecture] [section_url: https://zigbook.net/chapters/28__filesystem-and-io#_the_filesystem_architecture] Before diving into specific operations, it’s essential to understand how Zig’s filesystem APIs are structured. The following diagram shows the layered architecture from high-level `std.fs` operations down to system calls: ```text graph TB subgraph "User Code" APP[Application Code] end subgraph "High-Level APIs (lib/std)" FS["std.fs
(fs.zig)"] NET["std.net
(net.zig)"] PROCESS["std.process
(process.zig)"] FMT["std.fmt
(fmt.zig)"] HEAP["std.heap
(heap.zig)"] end subgraph "Mid-Level Abstractions" POSIX["std.posix
(posix.zig)
Cross-platform POSIX API"] OS["std.os
(os.zig)
OS-specific wrappers"] MEM["std.mem
(mem.zig)
Memory utilities"] DEBUG["std.debug
(debug.zig)
Stack traces, assertions"] end subgraph "Platform Layer" LINUX["std.os.linux
(os/linux.zig)
Direct syscalls"] WINDOWS["std.os.windows
(os/windows.zig)
Win32 APIs"] WASI["std.os.wasi
(os/wasi.zig)
WASI APIs"] LIBC["std.c
(c.zig)
C interop"] end subgraph "System Layer" SYSCALL["System Calls"] KERNEL["Operating System"] end APP --> FS APP --> NET APP --> PROCESS APP --> FMT APP --> HEAP FS --> POSIX NET --> POSIX PROCESS --> POSIX FMT --> MEM HEAP --> MEM POSIX --> OS OS --> LIBC OS --> LINUX OS --> WINDOWS OS --> WASI DEBUG --> OS LINUX --> SYSCALL WINDOWS --> SYSCALL WASI --> SYSCALL LIBC --> SYSCALL SYSCALL --> KERNEL ``` This layered design provides both portability and control. When you call `std.fs.File.read()`, the request flows through `std.posix` for cross-platform compatibility, then through `std.os` which dispatches to platform-specific implementations—either direct system calls on Linux or libc functions when `builtin.link_libc` is true. Understanding this architecture helps you reason about cross-platform behavior, debug issues by knowing which layer to inspect, and make informed decisions about linking libc. The separation of concerns means you can use high-level `std.fs` APIs for portability while still having access to lower layers when you need platform-specific features. ## Section: Learning Goals [section_id: learning-goals] [section_url: https://zigbook.net/chapters/28__filesystem-and-io#learning-goals] - Compose platform-neutral paths, open files safely, and print via buffered writers without leaking handles. path.zig (https://github.com/ziglang/zig/tree/master/lib/std/fs/path.zig) - Stream data between files while inspecting metadata such as byte counts and stat output. - Walk directory trees using `Dir.walk`, filtering on extensions to build discovery and housekeeping tools. Dir.zig (https://github.com/ziglang/zig/tree/master/lib/std/fs/Dir.zig) - Apply ergonomic error handling patterns (`catch`, cleanup defers) when juggling multiple file descriptors. ## Section: Paths, handles, and buffered stdout [section_id: paths-and-writers] [section_url: https://zigbook.net/chapters/28__filesystem-and-io#paths-and-writers] We start with the basics: join a platform-neutral path, create a file, write a CSV header with the buffered stdout guidance from 0.15, and read it back into memory. The example keeps allocations explicit so you can see where buffers live and when they are freed. ### Subsection: Understanding std.fs Module Organization [section_id: _understanding_std_fs_module_organization] [section_url: https://zigbook.net/chapters/28__filesystem-and-io#_understanding_std_fs_module_organization] The `std.fs` namespace is organized around two primary types, each with distinct responsibilities: ```text graph TB subgraph "std.fs Module" FS["fs.zig
cwd, max_path_bytes"] DIR["fs/Dir.zig
openFile, makeDir"] FILE["fs/File.zig
read, write, stat"] end FS --> DIR FS --> FILE ``` The `fs.zig` root module provides entry points like `std.fs.cwd()` which returns a `Dir` handle representing the current working directory, plus platform constants like `max_path_bytes`. The `Dir` type (`fs/Dir.zig`) handles directory-level operations—opening files, creating subdirectories, iterating entries, and managing directory handles. The `File` type (`fs/File.zig`) provides all file-specific operations: reading, writing, seeking, and querying metadata via `stat()`. This separation keeps the API clear: use `Dir` methods to navigate the filesystem tree and `File` methods to manipulate file contents. When you call `dir.openFile()`, you get back a `File` handle that’s independent of the directory—closing the directory doesn’t invalidate the file handle. ```zig const std = @import("std"); pub fn main() !void { // Initialize a general-purpose allocator for dynamic memory allocation var gpa = std.heap.GeneralPurposeAllocator(.{}){}; defer _ = gpa.deinit(); const allocator = gpa.allocator(); // Create a working directory for filesystem operations const dir_name = "fs_walkthrough"; try std.fs.cwd().makePath(dir_name); // Clean up the directory on exit, ignoring errors if it doesn't exist defer std.fs.cwd().deleteTree(dir_name) catch {}; // Construct a platform-neutral path by joining directory and filename const file_path = try std.fs.path.join(allocator, &.{ dir_name, "metrics.log" }); defer allocator.free(file_path); // Create a new file with truncate and read permissions // truncate ensures we start with an empty file var file = try std.fs.cwd().createFile(file_path, .{ .truncate = true, .read = true }); defer file.close(); // Set up a buffered writer for efficient file I/O // The buffer reduces syscall overhead by batching writes var file_writer_buffer: [256]u8 = undefined; var file_writer_state = file.writer(&file_writer_buffer); const file_writer = &file_writer_state.interface; // Write CSV data to the file via the buffered writer try file_writer.print("timestamp,value\n", .{}); try file_writer.print("2025-11-05T09:00Z,42\n", .{}); try file_writer.print("2025-11-05T09:05Z,47\n", .{}); // Flush ensures all buffered data is written to disk try file_writer.flush(); // Resolve the relative path to an absolute filesystem path const absolute_path = try std.fs.cwd().realpathAlloc(allocator, file_path); defer allocator.free(absolute_path); // Rewind the file cursor to the beginning to read back what we wrote try file.seekTo(0); // Read the entire file contents into allocated memory (max 16 KiB) const contents = try file.readToEndAlloc(allocator, 16 * 1024); defer allocator.free(contents); // Extract filename and directory components from the path const file_name = std.fs.path.basename(file_path); const dir_part = std.fs.path.dirname(file_path) orelse "."; // Set up a buffered stdout writer following Zig 0.15.2 best practices // Buffering stdout improves performance for multiple print calls var stdout_buffer: [512]u8 = undefined; var stdout_state = std.fs.File.stdout().writer(&stdout_buffer); const out = &stdout_state.interface; // Display file metadata and contents to stdout try out.print("file name: {s}\n", .{file_name}); try out.print("directory: {s}\n", .{dir_part}); try out.print("absolute path: {s}\n", .{absolute_path}); try out.print("--- file contents ---\n{s}", .{contents}); // Flush the stdout buffer to ensure all output is displayed try out.flush(); } ``` Run: ```shell $ zig run 01_paths_and_io.zig ``` Output: ```shell file name: metrics.log directory: fs_walkthrough absolute path: /home/zkevm/Documents/github/zigbook-net/fs_walkthrough/metrics.log --- file contents --- timestamp,value 2025-11-05T09:00Z,42 2025-11-05T09:05Z,47 ``` ### Subsection: Platform-Specific Path Encoding [section_id: _platform_specific_path_encoding] [section_url: https://zigbook.net/chapters/28__filesystem-and-io#_platform_specific_path_encoding] Path strings in Zig use platform-specific encodings, which is important for cross-platform code: | Platform | Encoding | Notes | | --- | --- | --- | | Windows | WTF-8 | Encodes WTF-16LE in UTF-8 compatible format | | WASI | UTF-8 | Valid UTF-8 required | | Other | Opaque bytes | No particular encoding assumed | On Windows, Zig uses WTF-8 (Wobbly Transformation Format-8) to represent filesystem paths. This is a superset of UTF-8 that can encode unpaired UTF-16 surrogates, allowing Zig to handle any Windows path while still working with `[]const u8` slices. WASI targets enforce strict UTF-8 validation on all paths. On Linux, macOS, and other POSIX systems, paths are treated as opaque byte sequences with no encoding assumptions—they can contain any bytes except null terminators. This means `std.fs.path.join` works identically across platforms by operating on byte slices, while the underlying OS layer handles encoding conversions transparently. When writing cross-platform path manipulation code, stick to `std.fs.path` utilities and avoid assumptions about UTF-8 validity unless targeting WASI specifically. TIP: `readToEndAlloc` works on the current seek position; always rewind with `seekTo(0)` (or reopen) after writing if you plan to reread the same handle. ## Section: Streaming copies with positional writers [section_id: streaming-copy] [section_url: https://zigbook.net/chapters/28__filesystem-and-io#streaming-copy] File copying illustrates how `std.fs.File.read` coexists with buffered writers that honor the changelog’s “please buffer” directive. This snippet streams fixed-size chunks, flushes the destination, and fetches metadata for sanity checks. ```zig const std = @import("std"); pub fn main() !void { // Initialize a general-purpose allocator for dynamic memory allocation var gpa = std.heap.GeneralPurposeAllocator(.{}){}; defer _ = gpa.deinit(); const allocator = gpa.allocator(); // Create a working directory for the stream copy demonstration const dir_name = "fs_stream_copy"; try std.fs.cwd().makePath(dir_name); // Clean up the directory on exit, ignoring errors if it doesn't exist defer std.fs.cwd().deleteTree(dir_name) catch {}; // Construct a platform-neutral path for the source file const source_path = try std.fs.path.join(allocator, &.{ dir_name, "source.txt" }); defer allocator.free(source_path); // Create the source file with truncate and read permissions // truncate ensures we start with an empty file var source_file = try std.fs.cwd().createFile(source_path, .{ .truncate = true, .read = true }); defer source_file.close(); // Set up a buffered writer for the source file // Buffering reduces syscall overhead by batching writes var source_writer_buffer: [128]u8 = undefined; var source_writer_state = source_file.writer(&source_writer_buffer); const source_writer = &source_writer_state.interface; // Write sample data to the source file try source_writer.print("alpha\n", .{}); try source_writer.print("beta\n", .{}); try source_writer.print("gamma\n", .{}); // Flush ensures all buffered data is written to disk try source_writer.flush(); // Rewind the source file cursor to the beginning for reading try source_file.seekTo(0); // Construct a platform-neutral path for the destination file const dest_path = try std.fs.path.join(allocator, &.{ dir_name, "copy.txt" }); defer allocator.free(dest_path); // Create the destination file with truncate and read permissions var dest_file = try std.fs.cwd().createFile(dest_path, .{ .truncate = true, .read = true }); defer dest_file.close(); // Set up a buffered writer for the destination file var dest_writer_buffer: [64]u8 = undefined; var dest_writer_state = dest_file.writer(&dest_writer_buffer); const dest_writer = &dest_writer_state.interface; // Allocate a chunk buffer for streaming copy operations var chunk: [128]u8 = undefined; var total_bytes: usize = 0; // Stream data from source to destination in chunks // This approach is memory-efficient for large files while (true) { const read_len = try source_file.read(&chunk); // A read length of 0 indicates EOF if (read_len == 0) break; // Write the exact number of bytes read to the destination try dest_writer.writeAll(chunk[0..read_len]); total_bytes += read_len; } // Flush the destination writer to ensure all data is persisted try dest_writer.flush(); // Retrieve file metadata to verify the copy operation const info = try dest_file.stat(); // Set up a buffered stdout writer for displaying results var stdout_buffer: [256]u8 = undefined; var stdout_state = std.fs.File.stdout().writer(&stdout_buffer); const out = &stdout_state.interface; // Display copy operation statistics try out.print("copied {d} bytes\n", .{total_bytes}); try out.print("destination size: {d}\n", .{info.size}); // Rewind the destination file to read back the copied contents try dest_file.seekTo(0); const copied = try dest_file.readToEndAlloc(allocator, 16 * 1024); defer allocator.free(copied); // Display the copied file contents for verification try out.print("--- copy.txt ---\n{s}", .{copied}); // Flush stdout to ensure all output is displayed try out.flush(); } ``` Run: ```shell $ zig run 02_stream_copy.zig ``` Output: ```shell copied 17 bytes destination size: 17 --- copy.txt --- alpha beta gamma ``` NOTE: `File.stat()` caches size and kind information on Linux, macOS, and Windows, saving an extra syscall for subsequent queries. Lean on it instead of juggling separate `fs.path` calls. ## Section: Walking directory trees [section_id: dir-walk] [section_url: https://zigbook.net/chapters/28__filesystem-and-io#dir-walk] `Dir.walk` hands you a recursive iterator with pre-opened directories, which means you can call `statFile` on the containing handle and avoid reallocating joined paths. The following demo builds a toy log tree, emits directory and file entries, and summarizes how many `.log` files were spotted. ```zig const std = @import("std"); /// Helper function to create a directory path from multiple path components /// Joins path segments using platform-appropriate separators and creates the full path fn ensurePath(allocator: std.mem.Allocator, parts: []const []const u8) !void { // Join path components into a single platform-neutral path string const joined = try std.fs.path.join(allocator, parts); defer allocator.free(joined); // Create the directory path, including any missing parent directories try std.fs.cwd().makePath(joined); } /// Helper function to create a file and write contents to it /// Constructs the file path from components, creates the file, and writes data using buffered I/O fn writeFile(allocator: std.mem.Allocator, parts: []const []const u8, contents: []const u8) !void { // Join path components into a single platform-neutral path string const joined = try std.fs.path.join(allocator, parts); defer allocator.free(joined); // Create a new file with truncate option to start with an empty file var file = try std.fs.cwd().createFile(joined, .{ .truncate = true }); defer file.close(); // Set up a buffered writer to reduce syscall overhead var buffer: [128]u8 = undefined; var state = file.writer(&buffer); const writer = &state.interface; // Write the contents to the file and ensure all data is persisted try writer.writeAll(contents); try writer.flush(); } pub fn main() !void { // Initialize a general-purpose allocator for dynamic memory allocation var gpa = std.heap.GeneralPurposeAllocator(.{}){}; defer _ = gpa.deinit(); const allocator = gpa.allocator(); // Create a temporary directory structure for the directory walk demonstration const root = "fs_walk_listing"; try std.fs.cwd().makePath(root); // Clean up the directory tree on exit, ignoring errors if it doesn't exist defer std.fs.cwd().deleteTree(root) catch {}; // Create a multi-level directory structure with nested subdirectories try ensurePath(allocator, &.{ root, "logs", "app" }); try ensurePath(allocator, &.{ root, "logs", "jobs" }); try ensurePath(allocator, &.{ root, "notes" }); // Populate the directory structure with sample files try writeFile(allocator, &.{ root, "logs", "app", "today.log" }, "ok 200\n"); try writeFile(allocator, &.{ root, "logs", "app", "errors.log" }, "warn 429\n"); try writeFile(allocator, &.{ root, "logs", "jobs", "batch.log" }, "started\n"); try writeFile(allocator, &.{ root, "notes", "todo.txt" }, "rotate logs\n"); // Open the root directory with iteration capabilities for traversal var root_dir = try std.fs.cwd().openDir(root, .{ .iterate = true }); defer root_dir.close(); // Create a directory walker to recursively traverse the directory tree var walker = try root_dir.walk(allocator); defer walker.deinit(); // Set up a buffered stdout writer for efficient console output var stdout_buffer: [512]u8 = undefined; var stdout_state = std.fs.File.stdout().writer(&stdout_buffer); const out = &stdout_state.interface; // Initialize counters to track directory contents var total_dirs: usize = 0; var total_files: usize = 0; var log_files: usize = 0; // Walk the directory tree recursively, processing each entry while (try walker.next()) |entry| { // Extract the null-terminated path from the entry const path = std.mem.sliceTo(entry.path, 0); // Process entry based on its type (directory, file, etc.) switch (entry.kind) { .directory => { total_dirs += 1; try out.print("DIR {s}\n", .{path}); }, .file => { total_files += 1; // Retrieve file metadata to display size information const info = try entry.dir.statFile(entry.basename); // Check if the file has a .log extension const is_log = std.mem.endsWith(u8, path, ".log"); if (is_log) log_files += 1; // Display file path, size, and mark log files with a tag try out.print("FILE {s} ({d} bytes){s}\n", .{ path, info.size, if (is_log) " [log]" else "", }); }, // Ignore other entry types (symlinks, etc.) else => {}, } } // Display summary statistics of the directory walk try out.print("--- summary ---\n", .{}); try out.print("directories: {d}\n", .{total_dirs}); try out.print("files: {d}\n", .{total_files}); try out.print("log files: {d}\n", .{log_files}); // Flush stdout to ensure all output is displayed try out.flush(); } ``` Run: ```shell $ zig run 03_dir_walk.zig ``` Output: ```shell DIR logs DIR logs/jobs FILE logs/jobs/batch.log (8 bytes) [log] DIR logs/app FILE logs/app/errors.log (9 bytes) [log] FILE logs/app/today.log (7 bytes) [log] DIR notes FILE notes/todo.txt (12 bytes) --- summary --- directories: 4 files: 4 log files: 3 ``` TIP: Each `Walker.Entry` exposes both a zero-terminated `path` and the live `dir` handle. Prefer `statFile` on that handle to dodge `NameTooLong` for deeply nested trees. ## Section: Error handling patterns [section_id: error-patterns] [section_url: https://zigbook.net/chapters/28__filesystem-and-io#error-patterns] ### Subsection: How Filesystem Errors Work [section_id: _how_filesystem_errors_work] [section_url: https://zigbook.net/chapters/28__filesystem-and-io#_how_filesystem_errors_work] The filesystem API returns rich error sets—`error.AccessDenied`, `error.PathAlreadyExists`, `error.NameTooLong`, and friends—but where do these typed errors come from? The following diagram shows the error conversion flow: ```text graph TB SYSCALL["System Call"] RESULT{"Return Value"} subgraph "Error Path" ERRNO["Get errno/Win32Error"] ERRCONV["Convert to Zig error"] RETURN_ERR["Return error"] end subgraph "Success Path" RETURN_OK["Return result"] end SYSCALL --> RESULT RESULT -->|"< 0 or NULL"| ERRNO RESULT -->|">= 0 or valid"| RETURN_OK ERRNO --> ERRCONV ERRCONV --> RETURN_ERR ``` When a filesystem operation fails, the underlying system call returns an error indicator (negative value on POSIX, `NULL` on Windows). The OS abstraction layer then retrieves the error code—`errno` on POSIX systems or `GetLastError()` on Windows—and converts it to a typed Zig error via conversion functions like `errnoFromSyscall` (Linux) or `unexpectedStatus` (Windows). This means `error.AccessDenied` is not a string or enum tag—it’s a distinct error type that the compiler tracks through your call stack. The conversion is deterministic: `EACCES` (errno 13 on Linux) always becomes `error.AccessDenied`, and `ERROR_ACCESS_DENIED` (Win32 error 5) maps to the same Zig error, providing cross-platform error semantics. Use `catch |err|` sparingly to annotate expected failures (e.g. `catch |err| if (err == error.PathAlreadyExists) {}`) and pair it with `defer` for cleanup so partial successes do not leak directories or file descriptors. ### Subsection: The Translation Mechanism [section_id: _the_translation_mechanism] [section_url: https://zigbook.net/chapters/28__filesystem-and-io#_the_translation_mechanism] The error conversion happens through platform-specific functions that map error codes to Zig’s error types: ```text graph LR SYSCALL["System Call
returns error code"] ERRNO["errno or NTSTATUS"] CONVERT["errnoFromSyscall
or unexpectedStatus"] ERROR["Zig Error Union
e.g., error.AccessDenied"] SYSCALL --> ERRNO ERRNO --> CONVERT CONVERT --> ERROR ``` On Linux and POSIX systems, `errnoFromSyscall` in `lib/std/os/linux.zig` performs the errno-to-error mapping. On Windows, `unexpectedStatus` handles the conversion from `NTSTATUS` or Win32 error codes. This abstraction means your error handling code is portable—`catch error.AccessDenied` works identically whether you’re running on Linux (catching `EACCES`), macOS (catching `EACCES`), or Windows (catching `ERROR_ACCESS_DENIED`). The conversion tables are maintained in the standard library and cover hundreds of error codes, mapping them to approximately 80 distinct Zig errors that cover common failure modes. When an unexpected error occurs, the conversion functions return `error.Unexpected`, which typically indicates a serious bug or unsupported platform state. ### Subsection: Practical Error Handling Patterns [section_id: _practical_error_handling_patterns] [section_url: https://zigbook.net/chapters/28__filesystem-and-io#_practical_error_handling_patterns] - When creating throwaway directories (`makePath` + `deleteTree`), wrap deletion in `catch {}` to ignore `FileNotFound` during teardown. - For user-visible tools, map filesystem errors to actionable messages (e.g. "check permissions on …"). Keep the original `err` for logs. - If you must fall back from positional to streaming mode, switch to `File.readerStreaming`/`writerStreaming` or reopen in streaming mode once and reuse the interface. ## Section: Exercises [section_id: exercises] [section_url: https://zigbook.net/chapters/28__filesystem-and-io#exercises] - Extend the copy program so the destination filename comes from `std.process.argsAlloc`, then use `std.fs.path.extension` to refuse overwriting `.log` files. 26 (26__build-system-advanced-topics.xml) - Rewrite the directory walker to emit JSON using `std.json.stringify`, practicing how to stream structured data through buffered writers. See json.zig (https://github.com/ziglang/zig/tree/master/lib/std/json.zig). - Build a “tail” utility that follows a file by combining `File.seekTo` with periodic `read` calls; add `--follow` support by retrying on `error.EndOfStream`. ## Section: Notes & Caveats [section_id: notes-caveats] [section_url: https://zigbook.net/chapters/28__filesystem-and-io#notes-caveats] - `readToEndAlloc` guards against runaway files via its `max_bytes` argument—set it thoughtfully when parsing user-controlled input. - On Windows, opening directories for iteration requires `OpenOptions{ .iterate = true }`; the sample code does this implicitly via `openDir` with that flag. - ANSI escape sequences in samples assume a color-capable terminal; wrap prints in `if (std.io.isTty())` when shipping cross-platform tools. See tty.zig (https://github.com/ziglang/zig/tree/master/lib/std/Io/tty.zig). ### Subsection: Under the Hood: System Call Dispatch [section_id: _under_the_hood_system_call_dispatch] [section_url: https://zigbook.net/chapters/28__filesystem-and-io#_under_the_hood_system_call_dispatch] For readers curious about how filesystem operations reach the kernel, Zig’s `std.posix` layer uses a compile-time decision to choose between libc and direct system calls: ```text graph TB APP["posix.open(path, flags, mode)"] USELIBC{"use_libc?"} subgraph "libc Path" COPEN["std.c.open()"] LIBCOPEN["libc open()"] end subgraph "Direct Syscall Path (Linux)" LINUXOPEN["std.os.linux.open()"] SYSCALL["syscall3(.open, ...)"] KERNEL["Linux Kernel"] end ERRCONV["errno → Zig Error"] APP --> USELIBC USELIBC -->|"true"| COPEN USELIBC -->|"false (Linux)"| LINUXOPEN COPEN --> LIBCOPEN LINUXOPEN --> SYSCALL SYSCALL --> KERNEL LIBCOPEN --> ERRCONV KERNEL --> ERRCONV ``` When `builtin.link_libc` is true, Zig routes filesystem calls through the C standard library’s functions (`open`, `read`, `write`, etc.). This ensures compatibility with systems where direct syscalls aren’t available or well-defined. On Linux, when libc is not linked, Zig uses direct system calls via `std.os.linux.syscall3` and friends—this eliminates libc overhead and provides a smaller binary, at the cost of depending on the Linux syscall ABI stability. The decision happens at compile time based on your build configuration, meaning there’s zero runtime overhead for the dispatch. This architecture is why Zig can produce tiny, static binaries on Linux (no libc dependency) while still supporting traditional libc-based builds for maximum compatibility. When debugging filesystem issues, knowing which path your build uses helps you understand stack traces and performance characteristics. ## Section: Summary [section_id: summary] [section_url: https://zigbook.net/chapters/28__filesystem-and-io#summary] - Buffer writes, flush deliberately, and lean on `std.fs.File` helpers like `readToEndAlloc` and `stat` to reduce manual bookkeeping. - `Dir.walk` keeps directory handles open so your tooling can operate on basenames without rebuilding absolute paths. - With solid error handling and cleanup defers, these primitives form the foundation for everything from log shippers to workspace installers. # Chapter 29 — Threads & Atomics [chapter_id: 29__threads-and-atomics] [chapter_slug: threads-and-atomics] [chapter_number: 29] [chapter_url: https://zigbook.net/chapters/29__threads-and-atomics] ## Section: Overview [section_id: overview] [section_url: https://zigbook.net/chapters/29__threads-and-atomics#overview] Filesystem plumbing from the previous chapter set the stage for applications that produce and consume data in parallel. Now we focus on how Zig launches OS threads, coordinates work across cores, and keeps shared state consistent with atomic operations (see 28 (28__filesystem-and-io.xml) and Thread.zig (https://github.com/ziglang/zig/tree/master/lib/std/Thread.zig)). Zig 0.15.2’s thread primitives combine lightweight spawn APIs with explicit memory ordering, so you decide when a store becomes visible and when contention should block. Understanding these tools now will make the upcoming parallel wordcount project far less mysterious (see atomic.zig (https://github.com/ziglang/zig/tree/master/lib/std/atomic.zig) and 30 (30__project-parallel-wordcount.xml)). ## Section: Learning Goals [section_id: learning-goals] [section_url: https://zigbook.net/chapters/29__threads-and-atomics#learning-goals] - Spawn and join worker threads responsibly, selecting stack sizes and allocators only when necessary. - Choose memory orderings for atomic loads, stores, and compare-and-swap loops when protecting shared state. - Detect single-threaded builds at compile time and fall back to synchronous execution paths. ## Section: Orchestrating Work with [section_id: thread-model] [section_url: https://zigbook.net/chapters/29__threads-and-atomics#thread-model] Zig models kernel threads through `std.Thread`, exposing helpers to query CPU counts, configure stack sizes, and join handles deterministically. Unlike async I/O, these are real kernel threads—every spawn consumes OS resources, so batching units of work matters. ### Subsection: Thread Pool Pattern [section_id: _thread_pool_pattern] [section_url: https://zigbook.net/chapters/29__threads-and-atomics#_thread_pool_pattern] Before diving into manual thread spawning, it’s valuable to understand the thread pool pattern that Zig’s own compiler uses for parallel work. The following diagram shows how `std.Thread.Pool` distributes work across workers: ```text graph TB ThreadPool["ThreadPool
(std.Thread.Pool)"] AstGen1["AstGen
(File 1)"] AstGen2["AstGen
(File 2)"] AstGen3["AstGen
(File 3)"] Sema1["Sema
(Function 1)"] Sema2["Sema
(Function 2)"] Codegen1["CodeGen
(Function 1)"] Codegen2["CodeGen
(Function 2)"] ThreadPool --> AstGen1 ThreadPool --> AstGen2 ThreadPool --> AstGen3 ThreadPool --> Sema1 ThreadPool --> Sema2 ThreadPool --> Codegen1 ThreadPool --> Codegen2 ZcuPerThread["Zcu.PerThread
(per-thread state)"] Sema1 -.->|"uses"| ZcuPerThread Sema2 -.->|"uses"| ZcuPerThread ``` A thread pool maintains a fixed number of worker threads that pull work items from a queue, avoiding the overhead of repeatedly spawning and joining threads. The Zig compiler uses this pattern extensively: `std.Thread.Pool` dispatches AST generation, semantic analysis, and code generation tasks to workers. Each worker has per-thread state (`Zcu.PerThread`) to minimize synchronization—only the final results need mutex protection when merging into shared data structures like `InternPool.shards`. This architecture demonstrates key concurrent design principles: work units should be independent, shared state should be sharded or protected by mutexes, and per-thread caches reduce contention. When your workload involves many small tasks, prefer `std.Thread.Pool` over manual spawning; when you need a few long-running workers with specific responsibilities, manual `spawn`/`join` is appropriate. ### Subsection: Chunking data with spawn/join [section_id: thread-model-chunk] [section_url: https://zigbook.net/chapters/29__threads-and-atomics#thread-model-chunk] The example below partitions an array of integers across a dynamic number of workers, using an atomic fetch-add to accumulate the total of even numbers without locks. It adapts to the host CPU count but never spawns more threads than there are elements to process. ```zig // This example demonstrates parallel computation using threads and atomic operations in Zig. // It calculates the sum of even numbers in an array by distributing work across multiple threads. const std = @import("std"); // Arguments passed to each worker thread for parallel processing const WorkerArgs = struct { slice: []const u64, // The subset of numbers this worker should process sum: *std.atomic.Value(u64), // Shared atomic counter for thread-safe accumulation }; // Worker function that accumulates even numbers from its assigned slice // Each thread runs this function independently on its own data partition fn accumulate(args: WorkerArgs) void { // Use a local variable to minimize atomic operations (performance optimization) var local_total: u64 = 0; for (args.slice) |value| { if (value % 2 == 0) { local_total += value; } } // Atomically add the local result to the shared sum using sequentially consistent ordering // This ensures all threads see a consistent view of the shared state _ = args.sum.fetchAdd(local_total, .seq_cst); } pub fn main() !void { // Set up memory allocator with automatic leak detection var gpa = std.heap.GeneralPurposeAllocator(.{}){}; defer _ = gpa.deinit(); const allocator = gpa.allocator(); // Allocate array of 64 numbers for demonstration var numbers = try allocator.alloc(u64, 64); defer allocator.free(numbers); // Initialize array with values following the pattern: index * 7 + 3 for (numbers, 0..) |*slot, index| { slot.* = @as(u64, @intCast(index * 7 + 3)); } // Initialize shared atomic counter that all threads will safely update var shared_sum = std.atomic.Value(u64).init(0); // Determine optimal number of worker threads based on available CPU cores const cpu_count = std.Thread.getCpuCount() catch 1; const desired = if (cpu_count == 0) 1 else cpu_count; // Don't create more threads than we have numbers to process const worker_limit = @min(numbers.len, desired); // Allocate thread handles for parallel workers var threads = try allocator.alloc(std.Thread, worker_limit); defer allocator.free(threads); // Calculate chunk size, rounding up to ensure all elements are covered const chunk = (numbers.len + worker_limit - 1) / worker_limit; // Spawn worker threads, distributing the array into roughly equal chunks var start: usize = 0; var spawned: usize = 0; while (start < numbers.len and spawned < worker_limit) : (spawned += 1) { const remaining = numbers.len - start; // Give the last thread all remaining elements to handle uneven divisions const take = if (worker_limit - spawned == 1) remaining else @min(chunk, remaining); const end = start + take; // Spawn thread with its assigned slice and shared accumulator threads[spawned] = try std.Thread.spawn(.{}, accumulate, .{WorkerArgs{ .slice = numbers[start..end], .sum = &shared_sum, }}); start = end; } // Track how many threads were actually spawned (may be less than worker_limit) const used_threads = spawned; // Wait for all worker threads to complete their work for (threads[0..used_threads]) |thread| { thread.join(); } // Read the final accumulated result from the atomic shared sum const even_sum = shared_sum.load(.seq_cst); // Perform sequential calculation to verify correctness of parallel computation var sequential: u64 = 0; for (numbers) |value| { if (value % 2 == 0) { sequential += value; } } // Set up buffered stdout writer for efficient output var stdout_buffer: [256]u8 = undefined; var stdout_state = std.fs.File.stdout().writer(&stdout_buffer); const out = &stdout_state.interface; // Display results: thread count and both parallel and sequential sums try out.print("spawned {d} worker(s)\n", .{used_threads}); try out.print("even sum (threads): {d}\n", .{even_sum}); try out.print("even sum (sequential check): {d}\n", .{sequential}); try out.flush(); } ``` Run: ```shell $ zig run 01_parallel_even_sum.zig ``` Output: ```shell spawned 8 worker(s) even sum (threads): 7264 even sum (sequential check): 7264 ``` TIP: `std.atomic.Value` wraps plain integers and routes every access through `@atomicLoad`, `@atomicStore`, or `@atomicRmw`, shielding you from accidentally mixing atomic and non-atomic access to the same memory location. ### Subsection: Spawn configuration and scheduling hints [section_id: thread-model-config] [section_url: https://zigbook.net/chapters/29__threads-and-atomics#thread-model-config] `std.Thread.SpawnConfig` lets you override stack sizes or supply a custom allocator if the defaults are unsuitable (for example, deep recursion or pre-allocated arenas). Catch `Thread.getCpuCount()` errors to provide a safe fallback, and remember to use `Thread.yield()` or `Thread.sleep()` when you need cooperative scheduling while waiting on other threads to progress. ## Section: Atomic state machines [section_id: atomics] [section_url: https://zigbook.net/chapters/29__threads-and-atomics#atomics] Zig exposes LLVM’s atomic intrinsics directly: you pick an order such as `.acquire`, `.release`, or `.seq_cst`, and the compiler emits the matching fences. That clarity is valuable when you design small state machines—like a one-time initializer—that multiple threads must observe consistently. ### Subsection: Implementing a once guard with atomic builtins [section_id: atomics-once] [section_url: https://zigbook.net/chapters/29__threads-and-atomics#atomics-once] This program builds a lock-free "call once" helper around `@cmpxchgStrong`. Threads spin only while another thread is running the initializer, then read the published value via an acquire load. ```zig // This example demonstrates thread-safe one-time initialization using atomic operations. // Multiple threads attempt to initialize a shared resource, but only one succeeds in // performing the expensive initialization exactly once. const std = @import("std"); // Represents the initialization state using atomic operations const State = enum(u8) { idle, busy, ready }; // Global state tracking the initialization lifecycle var once_state: State = .idle; // The shared configuration value that will be initialized once var config_value: i32 = 0; // Counter to verify that initialization only happens once var init_calls: u32 = 0; // Simulates an expensive initialization operation that should only run once. // Uses atomic operations to safely increment the call counter and set the config value. fn expensiveInit() void { // Simulate expensive work with a sleep std.Thread.sleep(2 * std.time.ns_per_ms); // Atomically increment the initialization call counter _ = @atomicRmw(u32, &init_calls, .Add, 1, .seq_cst); // Atomically store the initialized value with release semantics @atomicStore(i32, &config_value, 9157, .release); } // Ensures expensiveInit() is called exactly once across multiple threads. // Uses a state machine with compare-and-swap to coordinate thread access. fn callOnce() void { while (true) { // Check the current state with acquire semantics to see initialization results switch (@atomicLoad(State, &once_state, .acquire)) { // Initialization complete, return immediately .ready => return, // Another thread is initializing, yield and retry .busy => { std.Thread.yield() catch {}; continue; }, // Not yet initialized, attempt to claim initialization responsibility .idle => { // Try to atomically transition from idle to busy // If successful (returns null), this thread wins and will initialize // If it fails (returns the actual value), another thread won, so retry if (@cmpxchgStrong(State, &once_state, .idle, .busy, .acq_rel, .acquire)) |_| { continue; } // This thread successfully claimed the initialization break; }, } } // Perform the one-time initialization expensiveInit(); // Mark initialization as complete with release semantics @atomicStore(State, &once_state, .ready, .release); } // Arguments passed to each worker thread const WorkerArgs = struct { results: []i32, index: usize, }; // Worker thread function that calls the once-initialization and reads the result. fn worker(args: WorkerArgs) void { // Ensure initialization happens (blocks until complete if another thread is initializing) callOnce(); // Read the initialized value with acquire semantics const value = @atomicLoad(i32, &config_value, .acquire); // Store the observed value in the thread's result slot args.results[args.index] = value; } pub fn main() !void { // Reset global state for demonstration once_state = .idle; config_value = 0; init_calls = 0; // Set up memory allocation var gpa = std.heap.GeneralPurposeAllocator(.{}){}; defer _ = gpa.deinit(); const allocator = gpa.allocator(); const worker_count: usize = 4; // Allocate array to collect results from each thread const results = try allocator.alloc(i32, worker_count); defer allocator.free(results); // Initialize all result slots to -1 to detect if any thread fails for (results) |*slot| slot.* = -1; // Allocate array to hold thread handles const threads = try allocator.alloc(std.Thread, worker_count); defer allocator.free(threads); // Spawn all worker threads for (threads, 0..) |*thread, index| { thread.* = try std.Thread.spawn(.{}, worker, .{WorkerArgs{ .results = results, .index = index, }}); } // Wait for all threads to complete for (threads) |thread| { thread.join(); } // Read final values after all threads complete const final_value = @atomicLoad(i32, &config_value, .acquire); const called = @atomicLoad(u32, &init_calls, .seq_cst); // Set up buffered output var stdout_buffer: [256]u8 = undefined; var stdout_state = std.fs.File.stdout().writer(&stdout_buffer); const out = &stdout_state.interface; // Print the value observed by each thread (should all be 9157) for (results, 0..) |value, index| { try out.print("thread {d} observed {d}\n", .{ index, value }); } // Verify initialization was called exactly once try out.print("init calls: {d}\n", .{called}); // Display the final configuration value try out.print("config value: {d}\n", .{final_value}); try out.flush(); } ``` Run: ```shell $ zig run 02_atomic_once.zig ``` Output: ```shell thread 0 observed 9157 thread 1 observed 9157 thread 2 observed 9157 thread 3 observed 9157 init calls: 1 config value: 9157 ``` NOTE: `@cmpxchgStrong` returns `null` on success, so looping while it yields a value is a concise way to retry the CAS without allocating a mutex. Pair the final `@atomicStore` with `.release` to publish the results before any waiter performs its `.acquire` load. ## Section: Single-threaded builds & fallbacks [section_id: single-threaded] [section_url: https://zigbook.net/chapters/29__threads-and-atomics#single-threaded] Passing `-Dsingle-threaded=true` forces the compiler to reject any attempt to spawn OS threads. Code that might run in both configurations should branch on `builtin.single_threaded` at compile time and substitute an inline execution path. See builtin.zig (https://github.com/ziglang/zig/tree/master/lib/std/builtin.zig). ### Subsection: Understanding the Single-Threaded Flag [section_id: _understanding_the_single_threaded_flag] [section_url: https://zigbook.net/chapters/29__threads-and-atomics#_understanding_the_single_threaded_flag] The `single_threaded` flag is part of the compiler’s feature configuration system, affecting code generation and optimization: ```text graph TB subgraph "Code Generation Features" Features["Feature Flags"] Features --> UnwindTables["unwind_tables: bool"] Features --> StackProtector["stack_protector: bool"] Features --> StackCheck["stack_check: bool"] Features --> RedZone["red_zone: ?bool"] Features --> OmitFramePointer["omit_frame_pointer: bool"] Features --> Valgrind["valgrind: bool"] Features --> SingleThreaded["single_threaded: bool"] UnwindTables --> EHFrame["Generate .eh_frame
for exception handling"] StackProtector --> CanaryCheck["Stack canary checks
buffer overflow detection"] StackCheck --> ProbeStack["Stack probing
prevents overflow"] RedZone --> RedZoneSpace["Red zone optimization
(x86_64, AArch64)"] OmitFramePointer --> NoFP["Omit frame pointer
for performance"] Valgrind --> ValgrindSupport["Valgrind client requests
for memory debugging"] SingleThreaded --> NoThreading["Assume single-threaded
enable optimizations"] end ``` When `single_threaded` is true, the compiler assumes no concurrent access to memory, enabling several optimizations: atomic operations can be lowered to plain loads and stores (eliminating fence instructions), thread-local storage becomes regular globals, and synchronization primitives can be elided entirely. This flag is set via `-Dsingle-threaded=true` at build time and flows through `Compilation.Config` into code generation. Importantly, this is not just an API restriction—it fundamentally changes the generated code. Atomics compiled in single-threaded mode have weaker guarantees than atomics in multi-threaded builds, so you must ensure your code paths remain consistent across both modes to avoid subtle bugs when toggling the flag. ### Subsection: Gating thread usage at compile time [section_id: single-threaded-guard] [section_url: https://zigbook.net/chapters/29__threads-and-atomics#single-threaded-guard] The guard below resets an atomic state machine, then either spawns a worker or executes the task inline based on the build mode. Because the branch is compile-time, the single-threaded configuration never instantiates `Thread.spawn`, avoiding a compile error altogether. ```zig const std = @import("std"); const builtin = @import("builtin"); // Enum representing the possible states of task execution // Uses explicit u8 backing to ensure consistent size across platforms const TaskState = enum(u8) { idle, threaded_done, inline_done }; // Global atomic state tracking whether task ran inline or in a separate thread // Atomics ensure thread-safe access even though single-threaded builds won't spawn threads var task_state = std.atomic.Value(TaskState).init(.idle); // Simulates a task that runs in a separate thread // Includes a small delay to demonstrate asynchronous execution fn threadedTask() void { std.Thread.sleep(1 * std.time.ns_per_ms); // Release ordering ensures all prior writes are visible to threads that acquire this value task_state.store(.threaded_done, .release); } // Simulates a task that runs inline in the main thread // Used as fallback when threading is disabled at compile time fn inlineTask() void { // Release ordering maintains consistency with the threaded path task_state.store(.inline_done, .release); } pub fn main() !void { // Set up buffered stdout writer for efficient output var stdout_buffer: [256]u8 = undefined; var stdout_state = std.fs.File.stdout().writer(&stdout_buffer); const out = &stdout_state.interface; // Reset state to idle with sequential consistency // seq_cst provides strongest ordering guarantees for initialization task_state.store(.idle, .seq_cst); // Check compile-time flag to determine execution strategy // builtin.single_threaded is true when compiled with -fsingle-threaded if (builtin.single_threaded) { try out.print("single-threaded build; running task inline\n", .{}); // Execute task directly without spawning a thread inlineTask(); } else { try out.print("multi-threaded build; spawning worker\n", .{}); // Spawn separate thread to execute task concurrently var worker = try std.Thread.spawn(.{}, threadedTask, .{}); // Block until worker thread completes worker.join(); } // Acquire ordering ensures we observe all writes made before the release store const final_state = task_state.load(.acquire); // Convert enum state to human-readable string for output const label = switch (final_state) { .idle => "idle", .threaded_done => "threaded_done", .inline_done => "inline_done", }; // Display final execution state and flush buffer to ensure output is visible try out.print("task state: {s}\n", .{label}); try out.flush(); } ``` Run: ```shell $ zig run 03_single_thread_guard.zig ``` Output: ```shell multi-threaded build; spawning worker task state: threaded_done ``` IMPORTANT: When you build with `-Dsingle-threaded=true`, the inline branch is the only one compiled, so keep the logic symmetrical and make sure any shared state is still set via the same atomic helpers to avoid diverging semantics. ## Section: Notes & Caveats [section_id: notes-caveats] [section_url: https://zigbook.net/chapters/29__threads-and-atomics#notes-caveats] - Threads must be joined or detached exactly once; leaking handles leads to resource exhaustion. `Thread.join` consumes the handle, so store it in a slice you can iterate later. - Atomics operate on raw memory—never mix atomic and non-atomic accesses to the same location, even if you 'know' the race cannot happen. Wrap shared scalars in `std.atomic.Value` to keep your intent obvious. - Compare-and-swap loops may live-spin; consider `Thread.yield()` or event primitives like `Thread.ResetEvent` when a wait might last longer than a few cycles. ### Subsection: Debugging Concurrent Code with ThreadSanitizer [section_id: _debugging_concurrent_code_with_threadsanitizer] [section_url: https://zigbook.net/chapters/29__threads-and-atomics#_debugging_concurrent_code_with_threadsanitizer] Zig provides built-in race detection through ThreadSanitizer, a powerful tool for finding data races, deadlocks, and other concurrency bugs: | Sanitizer | Config Field | Purpose | Requirements | | --- | --- | --- | --- | | Thread Sanitizer | `any_sanitize_thread` | Data race detection | LLVM backend | | UBSan | `any_sanitize_c` | C undefined behavior | LLVM backend, C code | | Fuzzing | `any_fuzz` | Fuzzing instrumentation | libfuzzer integration | ```text graph TB subgraph "Sanitizer Configuration" Sanitizers["Sanitizer Flags"] Sanitizers --> TSan["any_sanitize_thread"] Sanitizers --> UBSan["any_sanitize_c"] Sanitizers --> Fuzz["any_fuzz"] TSan --> TSanLib["tsan_lib: ?CrtFile"] TSan --> TSanRuntime["ThreadSanitizer runtime
linked into binary"] UBSan --> UBSanLib["ubsan_rt_lib: ?CrtFile
ubsan_rt_obj: ?CrtFile"] UBSan --> UBSanRuntime["UBSan runtime
C undefined behavior checks"] Fuzz --> FuzzLib["fuzzer_lib: ?CrtFile"] Fuzz --> FuzzRuntime["libFuzzer integration
for fuzz testing"] end ``` Enable ThreadSanitizer with `-Dsanitize-thread` when building your program. TSan instruments all memory accesses and synchronization operations, tracking happens-before relationships to detect races. When a race is detected, TSan prints detailed reports showing the conflicting accesses and their stack traces. The instrumentation adds significant runtime overhead (2-5x slowdown, 5-10x memory usage), so use it during development and testing, not in production. TSan is particularly valuable for validating atomic code: even if your logic appears correct, TSan can catch subtle ordering issues or missing synchronization. For the examples in this chapter, try running them with `-Dsanitize-thread` to verify they’re race-free—the parallel sum and atomic once patterns should pass cleanly, demonstrating proper synchronization. ## Section: Exercises [section_id: exercises] [section_url: https://zigbook.net/chapters/29__threads-and-atomics#exercises] - Extend the parallel sum to accept a predicate callback so you can swap "even numbers" for any classification you like; measure the effect of `.acquire` vs `.monotonic` loads on contention. - Rework the `callOnce` demo to stage errors: have the initializer return `!void` and store the failure in an atomic slot so callers can rethrow the same error consistently. - Introduce a `std.Thread.WaitGroup` around the once-guard code so you can wait for arbitrary numbers of worker threads without storing handles manually. ## Section: Caveats, alternatives, edge cases [section_id: caveats-alternatives-edge-cases] [section_url: https://zigbook.net/chapters/29__threads-and-atomics#caveats-alternatives-edge-cases] - On platforms without pthreads or Win32 threads Zig emits a compile error; plan to fall back to event loops or async when targeting WASI without `--threading` support. - Atomics operate on plain integers and enums; for composite state consider using a mutex or designing an array of atomics to avoid torn updates. - Single-threaded builds can still use atomics, but the instructions compile to ordinary loads/stores. Keep the code paths consistent so you do not rely accidentally on the stronger ordering in multi-threaded builds. ### Subsection: Platform-Specific Threading Constraints [section_id: _platform_specific_threading_constraints] [section_url: https://zigbook.net/chapters/29__threads-and-atomics#_platform_specific_threading_constraints] Not all platforms support threading, and some have special requirements for thread-local storage: ```text graph TB subgraph "Threading Configuration" TARG["Target Platform"] TARG --> SINGLETHREAD["defaultSingleThreaded()
WASM, Haiku"] TARG --> EMULATETLS["useEmulatedTls()
OpenBSD, old Android"] SINGLETHREAD --> NOTHREAD["No thread support"] EMULATETLS --> TLSEMU["Software TLS"] end ``` Certain targets default to single-threaded mode because they lack OS thread support: WebAssembly (without the `--threading` flag) and Haiku OS both fall into this category. On these platforms, attempting to spawn threads results in a compile error unless you’ve explicitly enabled threading support in your build configuration. A related concern is thread-local storage (TLS): OpenBSD and older Android versions don’t provide native TLS, so Zig uses emulated TLS—a software implementation that’s slower but portable. When writing cross-platform concurrent code, check `target.defaultSingleThreaded()` and `target.useEmulatedTls()` to understand platform constraints. For WASM, you can enable threading with the `atomics` and `bulk-memory` features plus the `--import-memory --shared-memory` linker flags, but not all WASM runtimes support this. Design your code to gracefully degrade: use `builtin.single_threaded` to provide synchronous fallbacks, and avoid assuming TLS is zero-cost on all platforms. ## Section: Summary [section_id: summary] [section_url: https://zigbook.net/chapters/29__threads-and-atomics#summary] - `std.Thread` offers lightweight spawn/join semantics, but you remain responsible for scheduling and cleanup. - Atomic intrinsics such as `@atomicLoad`, `@atomicStore`, and `@cmpxchgStrong` make small lock-free state machines practical when you match the orderings to your invariant. - Using `builtin.single_threaded` keeps shared components working across single-threaded builds and multi-core deployments without forking the codebase. # Chapter 30 — Project [chapter_id: 30__project-parallel-wordcount] [chapter_slug: project-parallel-wordcount] [chapter_number: 30] [chapter_url: https://zigbook.net/chapters/30__project-parallel-wordcount] ## Section: Overview [section_id: overview] [section_url: https://zigbook.net/chapters/30__project-parallel-wordcount#overview] Armed with the concurrency primitives from the last chapter, we’ll build a small but useful tool: a parallel word counter that reads a file, shards it into contiguous segments along whitespace, spins up worker threads to tokenize and tally, then merges thread-local maps into a final frequency table. See 29 (29__threads-and-atomics.xml), Thread.zig (https://github.com/ziglang/zig/tree/master/lib/std/Thread.zig), and atomic.zig (https://github.com/ziglang/zig/tree/master/lib/std/atomic.zig). Why this project? It exercises common systems patterns—work decomposition, avoiding false sharing, ownership of string keys, and memory order discipline—without drowning in boilerplate. The result is a robust skeleton you can adapt to log crunching, grep-like indexing, or lightweight analytics. ## Section: Learning Goals [section_id: learning-goals] [section_url: https://zigbook.net/chapters/30__project-parallel-wordcount#learning-goals] - Partition inputs into shards while respecting token boundaries. - Use `std.Thread.spawn` safely and fall back to inline execution in single‑threaded builds. - Maintain per‑thread `std.StringHashMap` instances and merge them without dangling pointers. - Present a deterministic “Top N” by sorting a vector of key/value pairs. ## Section: Project layout and build [section_id: project-setup] [section_url: https://zigbook.net/chapters/30__project-parallel-wordcount#project-setup] We keep this sample as a tiny package with a local build. The 0.15.2 build API constructs an explicit module and passes it to `addExecutable`—note the `root_module` field rather than the old `root_source_file`. ```zig const std = @import("std"); /// Build script for the parallel wordcount project. /// Configures and compiles the executable with standard build options. pub fn build(b: *std.Build) void { // Parse target triple from command line (--target flag) const target = b.standardTargetOptions(.{}); // Parse optimization level from command line (-Doptimize flag) const optimize = b.standardOptimizeOption(.{}); // Create a module representing our application's entry point. // In Zig 0.15.2, modules are explicitly created before being passed to executables. const root = b.createModule(.{ .root_source_file = b.path("src/main.zig"), .target = target, .optimize = optimize, }); // Define the executable artifact, linking it to the root module. const exe = b.addExecutable(.{ .name = "parallel-wc", .root_module = root, }); // Register the executable to be installed in zig-out/bin b.installArtifact(exe); // Create a run command that executes the compiled binary const run_cmd = b.addRunArtifact(exe); // Forward any arguments passed after '--' to the executable if (b.args) |args| run_cmd.addArgs(args); // Define a 'run' step that users can invoke with 'zig build run' const run_step = b.step("run", "Run parallel wordcount"); run_step.dependOn(&run_cmd.step); } ``` See Build.zig (https://github.com/ziglang/zig/tree/master/lib/std/Build.zig). ## Section: Implementation [section_id: implementation] [section_url: https://zigbook.net/chapters/30__project-parallel-wordcount#implementation] The program reads an entire file into memory (bounded by a reasonable cap), creates shard boundaries at whitespace, and then launches N workers (N=CPU count unless in single‑threaded mode). Each worker tokenizes and lowercases ASCII, strips punctuation, and inserts into its own map backed by an arena to avoid per‑token frees. At merge time, we duplicate keys into the final map’s allocator so deinitializing arenas doesn’t invalidate keys. ```zig const std = @import("std"); const builtin = @import("builtin"); /// Map type alias: word → frequency count const Map = std.StringHashMap(u64); /// Normalize a raw token by converting ASCII letters to lowercase and /// stripping non-alphanumeric characters from both ends. /// Returns a slice into the provided buffer; caller owns the buffer. fn normalizeWord(allocator: std.mem.Allocator, raw: []const u8) ![]const u8 { // Allocate a buffer large enough to hold the entire input var buf = try allocator.alloc(u8, raw.len); var n: usize = 0; // Convert uppercase ASCII to lowercase (A-Z → a-z) for (raw) |c| { var ch = c; if (ch >= 'A' and ch <= 'Z') ch = ch + 32; buf[n] = ch; n += 1; } // Strip leading non-alphanumeric characters var start: usize = 0; while (start < n and (buf[start] < '0' or (buf[start] > '9' and buf[start] < 'a') or buf[start] > 'z')) : (start += 1) {} // Strip trailing non-alphanumeric characters var end: usize = n; while (end > start and (buf[end - 1] < '0' or (buf[end - 1] > '9' and buf[end - 1] < 'a') or buf[end - 1] > 'z')) : (end -= 1) {} // If nothing remains after stripping, return empty slice if (end <= start) return buf[0..0]; return buf[start..end]; } /// Tokenize text on whitespace and populate the provided map with /// normalized word frequencies. Keys are normalized copies allocated /// from the provided allocator. fn tokenizeAndCount(allocator: std.mem.Allocator, text: []const u8, map: *Map) !void { // Split on any whitespace character var it = std.mem.tokenizeAny(u8, text, " \t\r\n"); while (it.next()) |raw| { const word = try normalizeWord(allocator, raw); if (word.len == 0) continue; // skip empty tokens // Insert or update the word count const gop = try map.getOrPut(word); if (!gop.found_existing) { gop.value_ptr.* = 1; } else { gop.value_ptr.* += 1; } } } /// Arguments passed to each worker thread const WorkerArgs = struct { slice: []const u8, // segment of text to process counts: *Map, // thread-local frequency map arena: *std.heap.ArenaAllocator, // arena for temporary allocations }; /// Worker function executed by each thread; tokenizes and counts words /// in its assigned text segment without shared state. fn countWorker(args: WorkerArgs) void { // Each worker writes only to its own map instance; merge happens later tokenizeAndCount(args.arena.allocator(), args.slice, args.counts) catch |err| { std.debug.print("worker error: {s}\n", .{@errorName(err)}); }; } /// Read an entire file into a newly allocated buffer, capped at 64 MiB. fn readAllAlloc(path: []const u8, allocator: std.mem.Allocator) ![]u8 { var file = try std.fs.cwd().openFile(path, .{}); defer file.close(); return try file.readToEndAlloc(allocator, 64 * 1024 * 1024); } /// Partition text into roughly equal segments, ensuring shard boundaries /// fall at whitespace to avoid splitting words. Returns owned slice of slices. fn shard(text: []const u8, shards: usize, allocator: std.mem.Allocator) ![]const []const u8 { // If only one shard requested or text is empty, return single segment if (shards <= 1 or text.len == 0) { var single = try allocator.alloc([]const u8, 1); single[0] = text; return single; } const approx = text.len / shards; // approximate bytes per shard var parts = std.array_list.Managed([]const u8).init(allocator); defer parts.deinit(); var i: usize = 0; while (i < text.len) { var end = @min(text.len, i + approx); // Push shard boundary forward to the next whitespace character while (end < text.len and text[end] != ' ' and text[end] != '\n' and text[end] != '\t' and text[end] != '\r') : (end += 1) {} // If no whitespace found, fall back to approximate boundary if (end == i) end = @min(text.len, i + approx); try parts.append(text[i..end]); i = end; } return try parts.toOwnedSlice(); } pub fn main() !void { var gpa = std.heap.GeneralPurposeAllocator(.{}){}; defer _ = gpa.deinit(); const allocator = gpa.allocator(); // Set up buffered stdout for efficient printing var stdout_buf: [1024]u8 = undefined; var stdout_state = std.fs.File.stdout().writer(&stdout_buf); const out = &stdout_state.interface; // Parse command-line arguments var args_it = try std.process.argsWithAllocator(allocator); defer args_it.deinit(); _ = args_it.next(); // skip program name const path = args_it.next() orelse { try out.print("usage: parallel-wc \n", .{}); try out.flush(); return; }; // Read entire file into memory const text = try readAllAlloc(path, allocator); defer allocator.free(text); // Determine shard count: use CPU count unless single-threaded build const cpu = std.Thread.getCpuCount() catch 1; const shard_count = if (builtin.single_threaded) 1 else if (cpu < 1) 1 else cpu; // Partition text into shards at whitespace boundaries const parts = try shard(text, shard_count, allocator); defer allocator.free(parts); // Allocate per-shard arenas and maps var arenas = try allocator.alloc(std.heap.ArenaAllocator, parts.len); defer allocator.free(arenas); var maps = try allocator.alloc(Map, parts.len); defer allocator.free(maps); // Allocate thread handles if multi-threaded var threads = if (builtin.single_threaded) &[_]std.Thread{} else try allocator.alloc(std.Thread, parts.len); defer if (!builtin.single_threaded) allocator.free(threads); // Spawn worker threads (or execute inline if single-threaded) for (parts, 0..) |seg, i| { arenas[i] = std.heap.ArenaAllocator.init(allocator); maps[i] = Map.init(allocator); try maps[i].ensureTotalCapacity(1024); // pre-size to reduce rehashing if (builtin.single_threaded) { // Execute worker inline countWorker(.{ .slice = seg, .counts = &maps[i], .arena = &arenas[i] }); } else { // Spawn a thread for this shard threads[i] = try std.Thread.spawn(.{}, countWorker, .{WorkerArgs{ .slice = seg, .counts = &maps[i], .arena = &arenas[i] }}); } } // Wait for all threads to complete if (!builtin.single_threaded) { for (threads) |t| t.join(); } // Merge per-thread maps into a single global map var total = Map.init(allocator); defer total.deinit(); try total.ensureTotalCapacity(4096); // pre-size for merged data for (maps, 0..) |*m, i| { var it = m.iterator(); while (it.next()) |e| { const key_bytes = e.key_ptr.*; // Duplicate key into total's allocator to take ownership, // since arenas will be freed shortly const dup = try allocator.dupe(u8, key_bytes); const gop = try total.getOrPut(dup); if (!gop.found_existing) { gop.value_ptr.* = e.value_ptr.*; } else { // Key already exists; free the duplicate and accumulate count allocator.free(dup); gop.value_ptr.* += e.value_ptr.*; } } // Free per-thread arena and map arenas[i].deinit(); m.deinit(); } // Build a sortable list of (word, count) entries const Entry = struct { k: []const u8, v: u64 }; var entries = std.array_list.Managed(Entry).init(allocator); defer entries.deinit(); var it = total.iterator(); while (it.next()) |e| { try entries.append(.{ .k = e.key_ptr.*, .v = e.value_ptr.* }); } // Sort by count descending, then alphabetically std.sort.pdq(Entry, entries.items, {}, struct { fn lessThan(_: void, a: Entry, b: Entry) bool { if (a.v == b.v) return std.mem.lessThan(u8, a.k, b.k); return a.v > b.v; // descending by count } }.lessThan); // Print top 10 most frequent words const to_show = @min(entries.items.len, 10); try out.print("top {d} words in {d} shards:\n", .{ to_show, parts.len }); for (entries.items[0..to_show]) |e| { try out.print("{s} {d}\n", .{ e.k, e.v }); } // Free duplicated keys now that we are done with the map var free_it = total.iterator(); while (free_it.next()) |e| allocator.free(e.key_ptr.*); try out.flush(); } ``` See hash_map.zig (https://github.com/ziglang/zig/tree/master/lib/std/hash_map.zig) and tokenize.zig (https://github.com/ziglang/zig/tree/master/lib/std/mem/tokenize.zig). Run: ```shell $ zig build --build-file chapters-data/code/30__project-parallel-wordcount/build.zig run -- chapters-data/code/30__project-parallel-wordcount/data/lines.txt ``` Output: ```shell top 10 words in 8 shards: and 2 i 2 little 2 me 2 the 2 a 1 about 1 ago—never 1 call 1 how 1 ``` TIP: `StringHashMap` stores string slices by reference; it does not copy bytes. When merging maps that point into short‑lived arenas, duplicate the key bytes into the destination allocator, then free them when you’re done. The example iterates and frees keys just before exit. ## Section: Notes & Caveats [section_id: notes-caveats] [section_url: https://zigbook.net/chapters/30__project-parallel-wordcount#notes-caveats] - Sharding pushes shard ends forward to the next whitespace to avoid splitting tokens mid‑word. That means shards may be uneven; that’s fine for an I/O‑bound tool. - The sample lowercases ASCII and strips punctuation crudely to stay focused on threading. If you need Unicode segmentation, integrate `std.unicode` and a more faithful normalization. - In single‑threaded builds (`-Dsingle-threaded=true`), we execute workers inline and skip spawning entirely, mirroring the pattern from Chapter 29. ## Section: Exercises [section_id: exercises] [section_url: https://zigbook.net/chapters/30__project-parallel-wordcount#exercises] - Add `-n ` to print the top N words, parsing flags with `std.process.argsWithAllocator`. - Switch the merge phase to a parallel reduction: pairwise merge per CPU until one map remains; measure scalability. - Replace the arena with a bump allocator sized via `file size / shards` and reason about fragmentation vs. peak footprint. ## Section: Summary [section_id: summary] [section_url: https://zigbook.net/chapters/30__project-parallel-wordcount#summary] This project distills a practical, fast path from bytes on disk to a sorted frequency table while respecting Zig’s ownership and threading model. It consolidates sharding, per‑thread maps, and safe merging—a minimal template ready for larger pipelines. # Chapter 31 — Networking, HTTP, and JSON [chapter_id: 31__networking-http-and-json] [chapter_slug: networking-http-and-json] [chapter_number: 31] [chapter_url: https://zigbook.net/chapters/31__networking-http-and-json] ## Section: Overview [section_id: overview] [section_url: https://zigbook.net/chapters/31__networking-http-and-json#overview] This chapter graduates from local files and threads to sockets, using Zig’s `std.net` and `std.http` packages to move data between processes in a disciplined way. For background, see net.zig (https://github.com/ziglang/zig/tree/master/lib/std/net.zig). We will build a minimal loopback server, explore handshakes, and layer an HTTP/JSON workflow on top to demonstrate how these pieces compose. Zig 0.15.2’s I/O redesign removes legacy buffered helpers, so we will adopt the modern `std.Io.Reader`/`std.Io.Writer` interface and show how to manage framing manually when necessary. See Reader.zig (https://github.com/ziglang/zig/tree/master/lib/std/Io/Reader.zig) and v0.15.2 (https://ziglang.org/download/0.15.1/release-notes.html). ### Subsection: The Network Stack Architecture [section_id: _the_network_stack_architecture] [section_url: https://zigbook.net/chapters/31__networking-http-and-json#_the_network_stack_architecture] Before diving into socket code, it’s essential to understand how `std.net` fits into Zig’s standard library architecture. The following diagram shows the complete layering from high-level networking APIs down to system calls: ```text graph TB subgraph "User Code" APP[Application Code] end subgraph "High-Level APIs (lib/std)" FS["std.fs
(fs.zig)"] NET["std.net
(net.zig)"] PROCESS["std.process
(process.zig)"] FMT["std.fmt
(fmt.zig)"] HEAP["std.heap
(heap.zig)"] end subgraph "Mid-Level Abstractions" POSIX["std.posix
(posix.zig)
Cross-platform POSIX API"] OS["std.os
(os.zig)
OS-specific wrappers"] MEM["std.mem
(mem.zig)
Memory utilities"] DEBUG["std.debug
(debug.zig)
Stack traces, assertions"] end subgraph "Platform Layer" LINUX["std.os.linux
(os/linux.zig)
Direct syscalls"] WINDOWS["std.os.windows
(os/windows.zig)
Win32 APIs"] WASI["std.os.wasi
(os/wasi.zig)
WASI APIs"] LIBC["std.c
(c.zig)
C interop"] end subgraph "System Layer" SYSCALL["System Calls"] KERNEL["Operating System"] end APP --> FS APP --> NET APP --> PROCESS APP --> FMT APP --> HEAP FS --> POSIX NET --> POSIX PROCESS --> POSIX FMT --> MEM HEAP --> MEM POSIX --> OS OS --> LIBC OS --> LINUX OS --> WINDOWS OS --> WASI DEBUG --> OS LINUX --> SYSCALL WINDOWS --> SYSCALL WASI --> SYSCALL LIBC --> SYSCALL SYSCALL --> KERNEL ``` This layered design mirrors the filesystem architecture from Chapter 28: `std.net` provides high-level, portable networking abstractions (Address, Stream, Server), which flow through `std.posix` for cross-platform POSIX socket compatibility, which then dispatches to platform-specific implementations—either direct syscalls on Linux (`socket`, `bind`, `listen`, `accept`) or Win32 Winsock APIs on Windows. When you call `Address.listen()`, the request traverses these layers: `std.net.Address` → `std.posix.socket()` → `std.os.linux.socket()` (or `std.os.windows.WSASocketW()`) → kernel. This explains why WASI builds fail on socket operations—the WASI layer lacks socket support in most runtimes. Understanding this architecture helps you reason about error handling (errors bubble up from syscalls), debug platform-specific issues, and make informed decisions about libc linking for maximum portability. ## Section: Learning Goals [section_id: learning-goals] [section_url: https://zigbook.net/chapters/31__networking-http-and-json#learning-goals] The goals in this module revolve around the networking primitives in `std.net` and the HTTP stack built on them (Server.zig (https://github.com/ziglang/zig/tree/master/lib/std/http/Server.zig), Client.zig (https://github.com/ziglang/zig/tree/master/lib/std/http/Client.zig)). You will learn how to: - Compose a loopback service with `std.net.Address.listen` that promptly accepts connections and coordinates readiness with `std.Thread.ResetEvent`. - Implement newline-oriented framing using the new `std.Io.Reader` helpers rather than deprecated buffered adapters. - Call `std.http.Client.fetch`, capture the response stream, and parse JSON payloads with `std.json` utilities. json.zig (https://github.com/ziglang/zig/tree/master/lib/std/json.zig) ## Section: Socket building blocks [section_id: socket-building-blocks] [section_url: https://zigbook.net/chapters/31__networking-http-and-json#socket-building-blocks] `std.net` exposes cross-platform TCP primitives that mirror the POSIX socket lifecycle while integrating with Zig’s error semantics and resource management. Pairing them with `std.Thread.ResetEvent` lets us synchronise a server thread’s readiness with a client, without resorting to polling. ResetEvent.zig (https://github.com/ziglang/zig/tree/master/lib/std/Thread/ResetEvent.zig) ### Subsection: Loopback handshake walkthrough [section_id: loopback-handshake] [section_url: https://zigbook.net/chapters/31__networking-http-and-json#loopback-handshake] The following example binds to `127.0.0.1`, accepts a single client, and echoes the trimmed line it received. Because Zig’s reader API no longer offers convenience line readers, the sample implements a `readLine` helper with `Reader.takeByte`, demonstrating how to build that functionality directly. ```zig const std = @import("std"); /// Arguments passed to the server thread so it can accept exactly one client and reply. const ServerTask = struct { server: *std.net.Server, ready: *std.Thread.ResetEvent, }; /// Reads a single line from a `std.Io.Reader`, stripping the trailing newline. /// Returns `null` when the stream ends before any bytes are read. fn readLine(reader: *std.Io.Reader, buffer: []u8) !?[]const u8 { var len: usize = 0; while (true) { // Attempt to read a single byte from the stream const byte = reader.takeByte() catch |err| switch (err) { error.EndOfStream => { // Stream ended: return null if no data was read, otherwise return what we have if (len == 0) return null; return buffer[0..len]; }, else => return err, }; // Complete the line when newline is encountered if (byte == '\n') return buffer[0..len]; // Skip carriage returns to handle both Unix (\n) and Windows (\r\n) line endings if (byte == '\r') continue; // Guard against buffer overflow if (len == buffer.len) return error.StreamTooLong; buffer[len] = byte; len += 1; } } /// Blocks waiting for a single client, echoes what the client sent, then exits. fn serveOne(task: ServerTask) void { // Signal the main thread that the server thread reached the accept loop. // This synchronization prevents the client from attempting connection before the server is ready. task.ready.set(); // Block until a client connects; handle connection errors gracefully const connection = task.server.accept() catch |err| { std.debug.print("accept failed: {s}\n", .{@errorName(err)}); return; }; // Ensure the connection is closed when this function exits defer connection.stream.close(); // Set up a buffered reader to receive data from the client var inbound_storage: [128]u8 = undefined; var net_reader = connection.stream.reader(&inbound_storage); const conn_reader = net_reader.interface(); // Read one line from the client using our custom line-reading logic var line_storage: [128]u8 = undefined; const maybe_line = readLine(conn_reader, &line_storage) catch |err| { std.debug.print("receive failed: {s}\n", .{@errorName(err)}); return; }; // Handle case where connection closed without sending data const line = maybe_line orelse { std.debug.print("connection closed before any data arrived\n", .{}); return; }; // Clean up any trailing whitespace from the received line const trimmed = std.mem.trimRight(u8, line, "\r\n"); // Build a response message that echoes what the server observed var response_storage: [160]u8 = undefined; const response = std.fmt.bufPrint(&response_storage, "server observed \"{s}\"\n", .{trimmed}) catch |err| { std.debug.print("format failed: {s}\n", .{@errorName(err)}); return; }; // Send the response back to the client using a buffered writer var outbound_storage: [128]u8 = undefined; var net_writer = connection.stream.writer(&outbound_storage); net_writer.interface.writeAll(response) catch |err| { std.debug.print("write error: {s}\n", .{@errorName(err)}); return; }; // Ensure all buffered data is transmitted before the connection closes net_writer.interface.flush() catch |err| { std.debug.print("flush error: {s}\n", .{@errorName(err)}); return; }; } pub fn main() !void { // Initialize allocator for dynamic memory needs var gpa = std.heap.GeneralPurposeAllocator(.{}){}; defer _ = gpa.deinit(); const allocator = gpa.allocator(); // Create a loopback server on 127.0.0.1 with an OS-assigned port (port 0) const address = try std.net.Address.parseIp("127.0.0.1", 0); var server = try address.listen(.{ .reuse_address = true }); defer server.deinit(); // Create a synchronization primitive to coordinate server readiness var ready = std.Thread.ResetEvent{}; // Spawn the server thread that will accept and handle one connection const server_thread = try std.Thread.spawn(.{}, serveOne, .{ServerTask{ .server = &server, .ready = &ready, }}); // Ensure the server thread completes before main() exits defer server_thread.join(); // Block until the server thread signals it has reached accept() // This prevents a race condition where the client tries to connect too early ready.wait(); // Retrieve the dynamically assigned port number and connect as a client const port = server.listen_address.in.getPort(); var stream = try std.net.tcpConnectToHost(allocator, "127.0.0.1", port); defer stream.close(); // Send a test message to the server using a buffered writer var outbound_storage: [64]u8 = undefined; var client_writer = stream.writer(&outbound_storage); const payload = "ping over loopback\n"; try client_writer.interface.writeAll(payload); // Force transmission of buffered data try client_writer.interface.flush(); // Receive the server's response using a buffered reader var inbound_storage: [128]u8 = undefined; var client_reader = stream.reader(&inbound_storage); const client_reader_iface = client_reader.interface(); var reply_storage: [128]u8 = undefined; const maybe_reply = try readLine(client_reader_iface, &reply_storage); const reply = maybe_reply orelse return error.EmptyReply; // Strip any trailing whitespace from the server's reply const trimmed = std.mem.trimRight(u8, reply, "\r\n"); // Display the results to stdout using a buffered writer for efficiency var stdout_storage: [256]u8 = undefined; var stdout_state = std.fs.File.stdout().writer(&stdout_storage); const out = &stdout_state.interface; try out.writeAll("loopback handshake succeeded\n"); try out.print("client received: {s}\n", .{trimmed}); // Ensure all output is visible before program exits try out.flush(); } ``` Run: ```shell $ zig run 01_loopback_ping.zig ``` Output: ```shell loopback handshake succeeded client received: server observed "ping over loopback" ``` TIP: `std.Thread.ResetEvent` provides an inexpensive latch for announcing that the server thread reached `accept`, ensuring the client connection attempt does not race ahead. ### Subsection: Managing framing explicitly [section_id: manual-framing] [section_url: https://zigbook.net/chapters/31__networking-http-and-json#manual-framing] Reading a line requires awareness of how the new reader interface delivers bytes: `takeByte` yields one byte at a time and reports `error.EndOfStream`, which we convert to either `null` (no data) or a completed slice. This manual framing encourages you to think about protocol boundaries rather than relying on an implicit buffered reader, and mirrors the intent of the 0.15.2 I/O overhaul. ## Section: HTTP pipelines in Zig [section_id: http-pipeline] [section_url: https://zigbook.net/chapters/31__networking-http-and-json#http-pipeline] With sockets in hand, we can step up a level: Zig’s standard library ships an HTTP server and client implemented entirely in Zig, letting you serve endpoints and perform requests without third-party dependencies. ### Subsection: Serving JSON from a loopback listener [section_id: http-serve] [section_url: https://zigbook.net/chapters/31__networking-http-and-json#http-serve] The server thread in the next example wraps the accepted stream with `std.http.Server`, parses one request, and emits a compact JSON body. Notice how we pre-render the response into a fixed buffer, so `request.respond` can advertise the content length accurately. Writer.zig (https://github.com/ziglang/zig/tree/master/lib/std/Io/Writer.zig) ### Subsection: Fetching and decoding with [section_id: http-fetch-json] [section_url: https://zigbook.net/chapters/31__networking-http-and-json#http-fetch-json] The companion client uses `std.http.Client.fetch` to perform a GET request, collects the body via a fixed writer and decodes it into a strongly typed struct using `std.json.parseFromSlice`. The same routine can be extended to follow redirects, stream large payloads, or negotiate TLS, depending on your needs. static.zig (https://github.com/ziglang/zig/tree/master/lib/std/json/static.zig) ```zig const std = @import("std"); /// Arguments passed to the HTTP server thread so it can respond to a single request. const HttpTask = struct { server: *std.net.Server, ready: *std.Thread.ResetEvent, }; /// Minimal HTTP handler: accept one client, reply with a JSON document, and exit. fn serveJson(task: HttpTask) void { // Signal the main thread that the server thread reached the accept loop. // This synchronization prevents the client from attempting connection before the server is ready. task.ready.set(); // Block until a client connects; handle connection errors gracefully const connection = task.server.accept() catch |err| { std.debug.print("accept failed: {s}\n", .{@errorName(err)}); return; }; // Ensure the connection is closed when this function exits defer connection.stream.close(); // Allocate buffers for receiving HTTP request and sending HTTP response var recv_buffer: [4096]u8 = undefined; var send_buffer: [4096]u8 = undefined; // Create buffered reader and writer for the TCP connection var conn_reader = connection.stream.reader(&recv_buffer); var conn_writer = connection.stream.writer(&send_buffer); // Initialize HTTP server state machine with the buffered connection interfaces var server = std.http.Server.init(conn_reader.interface(), &conn_writer.interface); // Parse the HTTP request headers (method, path, version, etc.) var request = server.receiveHead() catch |err| { std.debug.print("receive head failed: {s}\n", .{@errorName(err)}); return; }; // Define the shape of our JSON response payload const Body = struct { service: []const u8, message: []const u8, method: []const u8, path: []const u8, sequence: u32, }; // Build a response that echoes request details back to the client const payload = Body{ .service = "loopback-api", .message = "hello from Zig HTTP server", .method = @tagName(request.head.method), // Convert HTTP method enum to string .path = request.head.target, // Echo the requested path .sequence = 1, }; // Allocate a buffer for the JSON-encoded response body var json_buffer: [256]u8 = undefined; // Create a fixed-size writer that writes into our buffer var body_writer = std.Io.Writer.fixed(json_buffer[0..]); // Serialize the payload struct into JSON format std.json.Stringify.value(payload, .{}, &body_writer) catch |err| { std.debug.print("json encode failed: {s}\n", .{@errorName(err)}); return; }; // Get the slice containing the actual JSON bytes written const body = std.Io.Writer.buffered(&body_writer); // Send HTTP 200 response with the JSON body and appropriate content-type header request.respond(body, .{ .extra_headers = &.{ .{ .name = "content-type", .value = "application/json" }, }, }) catch |err| { std.debug.print("respond failed: {s}\n", .{@errorName(err)}); return; }; } pub fn main() !void { // Initialize allocator for dynamic memory needs (HTTP client requires allocation) var gpa = std.heap.GeneralPurposeAllocator(.{}){}; defer _ = gpa.deinit(); const allocator = gpa.allocator(); // Create a loopback server on 127.0.0.1 with an OS-assigned port (port 0) const address = try std.net.Address.parseIp("127.0.0.1", 0); var server = try address.listen(.{ .reuse_address = true }); defer server.deinit(); // Create a synchronization primitive to coordinate server readiness var ready = std.Thread.ResetEvent{}; // Spawn the server thread that will accept and handle one HTTP request const server_thread = try std.Thread.spawn(.{}, serveJson, .{HttpTask{ .server = &server, .ready = &ready, }}); // Ensure the server thread completes before main() exits defer server_thread.join(); // Block until the server thread signals it has reached accept() // This prevents a race condition where the client tries to connect too early ready.wait(); // Retrieve the dynamically assigned port number for the client connection const port = server.listen_address.in.getPort(); // Initialize HTTP client with our allocator var client = std.http.Client{ .allocator = allocator }; defer client.deinit(); // Construct the full URL for the HTTP request var url_buffer: [64]u8 = undefined; const url = try std.fmt.bufPrint(&url_buffer, "http://127.0.0.1:{d}/stats", .{port}); // Allocate buffer to receive the HTTP response body var response_buffer: [512]u8 = undefined; // Create a fixed-size writer that will capture the response var response_writer = std.Io.Writer.fixed(response_buffer[0..]); // Perform the HTTP GET request with custom User-Agent header const fetch_result = try client.fetch(.{ .location = .{ .url = url }, .response_writer = &response_writer, // Where to write response body .headers = .{ .user_agent = .{ .override = "zigbook-demo/0.15.2" }, }, }); // Get the slice containing the actual response body bytes const body = std.Io.Writer.buffered(&response_writer); // Define the expected structure of the JSON response const ResponseShape = struct { service: []const u8, message: []const u8, method: []const u8, path: []const u8, sequence: u32, }; // Parse the JSON response into a typed struct var parsed = try std.json.parseFromSlice(ResponseShape, allocator, body, .{}); // Free the memory allocated during JSON parsing defer parsed.deinit(); // Set up a buffered writer for stdout to efficiently output results var stdout_storage: [256]u8 = undefined; var stdout_state = std.fs.File.stdout().writer(&stdout_storage); const out = &stdout_state.interface; // Display the HTTP response status code try out.print("status: {d}\n", .{@intFromEnum(fetch_result.status)}); // Display the parsed JSON fields try out.print("service: {s}\n", .{parsed.value.service}); try out.print("method: {s}\n", .{parsed.value.method}); try out.print("path: {s}\n", .{parsed.value.path}); try out.print("message: {s}\n", .{parsed.value.message}); // Ensure all output is visible before program exits try out.flush(); } ``` Run: ```shell $ zig run 02_http_fetch_and_json.zig ``` Output: ```shell status: 200 service: loopback-api method: GET path: /stats message: hello from Zig HTTP server ``` NOTE: `Client.fetch` defaults to keep-alive connections and automatically reuses sockets from its pool. When you feed it a fixed writer, the writer returns `error.WriteFailed` if your buffer is too small. Size it to cover the payload you expect, or fall back to an allocator-backed writer. ## Section: JSON tooling essentials [section_id: json-tooling] [section_url: https://zigbook.net/chapters/31__networking-http-and-json#json-tooling] `std.json.Stringify` and `std.json.parseFromSlice` let you stay in typed Zig data while emitting or consuming JSON text, provided you pay attention to allocation strategy. In these examples, we employ `std.Io.Writer.fixed` to build bodies without heap activity, and we release parse results with `Parsed.deinit()` once done. Stringify.zig (https://github.com/ziglang/zig/tree/master/lib/std/json/Stringify.zig) ### Subsection: Understanding the Writer Abstraction [section_id: _understanding_the_writer_abstraction] [section_url: https://zigbook.net/chapters/31__networking-http-and-json#_understanding_the_writer_abstraction] Both HTTP response generation and JSON serialization rely on Zig’s Writer interface. The following diagram shows the writer abstraction and its key implementations: ```text graph TB WRITER["Writer"] subgraph "Writer Types" FIXED["fixed(buffer)"] ALLOC["Allocating"] DISCARD["Discarding"] end WRITER --> FIXED WRITER --> ALLOC WRITER --> DISCARD subgraph "Write Methods" PRINT["print(fmt, args)"] PRINTVAL["printValue(specifier, options, value, depth)"] PRINTINT["printInt(value, base, case, options)"] WRITEBYTE["writeByte(byte)"] WRITEALL["writeAll(bytes)"] end WRITER --> PRINT WRITER --> PRINTVAL WRITER --> PRINTINT WRITER --> WRITEBYTE WRITER --> WRITEALL ``` The Writer abstraction provides a unified interface for output operations, with three main implementation strategies. Fixed buffer writers (`std.Io.Writer.fixed(buffer)`) write to a pre-allocated buffer and return `error.WriteFailed` when the buffer is full—this is what the HTTP example uses to build response bodies with zero heap allocation. Allocating writers dynamically grow their buffer using an allocator, suitable for unbounded output like streaming large JSON documents. Discarding writers count bytes without storing them, useful for calculating content length before actually writing. The write methods provide a consistent API regardless of the underlying implementation: `writeAll` for raw bytes, `print` for formatted output, `writeByte` for single bytes, and specialized methods like `printInt` for numeric formatting. When you call `std.json.stringify(value, .{}, writer)`, the JSON serializer doesn’t care whether `writer` is fixed, allocating, or discarding—it just calls `writeAll` and the writer implementation handles the details. This is why the chapter mentions 'size it to cover the payload you expect or fall back to an allocator-backed writer'—you’re choosing between bounded fixed buffers (fast, no allocation, can overflow) and dynamic allocating buffers (flexible, heap overhead, no size limit). ## Section: Notes & Caveats [section_id: notes-caveats] [section_url: https://zigbook.net/chapters/31__networking-http-and-json#notes-caveats] - TCP loopback servers still block the current thread on `accept`; when targeting single-threaded builds, you must branch on `builtin.single_threaded` to avoid spawning. builtin.zig (https://github.com/ziglang/zig/tree/master/lib/std/builtin.zig) - The HTTP client rescans system trust stores the first time you make an HTTPS request; if you vend your own certificate bundle, toggle `client.next_https_rescan_certs` accordingly. - The new I/O APIs expose raw buffers, so ensure your fixed writers and readers have enough capacity before reusing them across requests. ## Section: Exercises [section_id: exercises] [section_url: https://zigbook.net/chapters/31__networking-http-and-json#exercises] - Extend the loopback handshake to accept multiple clients by storing handles in a slice and joining them after broadcasting a shutdown message. Thread.zig (https://github.com/ziglang/zig/tree/master/lib/std/Thread.zig) - Add a `--head` flag to the HTTP example that issues a `HEAD` request and prints the negotiated headers, inspecting `Response.head` for metadata. - Replace the manual `readLine` helper with `Reader.discardDelimiterLimit` to compare behaviour and error handling under the new I/O contracts. ## Section: Caveats, alternatives, edge cases [section_id: caveats-alternatives-edge-cases] [section_url: https://zigbook.net/chapters/31__networking-http-and-json#caveats-alternatives-edge-cases] - Not every Zig target supports sockets; WASI builds, for instance, will fail during `Address.listen`, so guard availability by inspecting the target OS tag. - TLS requests require a certificate bundle; embed one with `Client.ca_bundle` when running in environments without system stores (CI, containers, early boot environments). - `std.json.parseFromSlice` loads the whole document into memory; for large payloads prefer the streaming `std.json.Scanner` API to process tokens incrementally. Scanner.zig (https://github.com/ziglang/zig/tree/master/lib/std/json/Scanner.zig) ## Section: Summary [section_id: summary] [section_url: https://zigbook.net/chapters/31__networking-http-and-json#summary] - `std.net` and `std.Io.Reader` give you the raw tools to accept connections, manage framing, and synchronise readiness across threads in a predictable way. - `std.http.Server` and `std.http.Client` sit naturally atop `std.net`, providing composable building blocks for REST-style services without external dependencies. - `std.json` rounds out the story by turning on-wire data into typed structs and back, keeping ownership explicit so you can choose between fixed buffers and heap-backed writers. # Chapter 32 — Project [chapter_id: 32__project-http-json-client] [chapter_slug: project-http-json-client] [chapter_number: 32] [chapter_url: https://zigbook.net/chapters/32__project-http-json-client] ## Section: Overview [section_id: overview] [section_url: https://zigbook.net/chapters/32__project-http-json-client#overview] This project chapter extends the networking primitives from 31 (31__networking-http-and-json.xml) into a self-contained client that polls a service, parses JSON, and prints a health report. Whereas the prior chapter focused on raw socket handshakes and minimal HTTP examples, this one combines `std.http.Client.fetch`, `std.json.parseFromSlice`, and formatted terminal output to build a user-facing workflow (see Client.zig (https://github.com/ziglang/zig/tree/master/lib/std/http/Client.zig) and static.zig (https://github.com/ziglang/zig/tree/master/lib/std/json/static.zig)). The example intentionally stands up a local server inside the same process so the client can run offline and under test. That fixture makes it easy to iterate on request framing and parsing logic while using the safer Reader and Writer APIs introduced in Zig 0.15.2 (see v0.15.2 (https://ziglang.org/download/0.15.1/release-notes.html)). ## Section: Learning Goals [section_id: learning-goals] [section_url: https://zigbook.net/chapters/32__project-http-json-client#learning-goals] - Launch a lightweight HTTP fixture with `std.net.Address.listen` and coordinate readiness with `std.Thread.ResetEvent`. - Capture and decode a JSON payload into typed Zig structs and tagged unions by layering a wire representation over `std.json.parseFromSlice`. - Present the results in a table, using the modern Writer API to manage buffers explicitly and highlight impacted services. Each goal builds directly on the client primitives introduced in the previous chapter and the HTTP components provided in Zig’s standard library (see 31 (31__networking-http-and-json.xml) and Server.zig (https://github.com/ziglang/zig/tree/master/lib/std/http/Server.zig)). ## Section: Project architecture [section_id: project-architecture] [section_url: https://zigbook.net/chapters/32__project-http-json-client#project-architecture] We structure the program into three pieces: a local HTTP server that exposes a status endpoint, a decoding layer that models the response as typed data, and a presentation layer that prints a concise summary. This mirrors the "fetch → parse → report" workflow mentioned in the content plan while keeping the entire project inside a single Zig executable. link (00__content_plan.adoc) ### Subsection: Local service fixture [section_id: local-fixture] [section_url: https://zigbook.net/chapters/32__project-http-json-client#local-fixture] The fixture thread binds to `127.0.0.1`, accepts a single client, and answers `GET /api/status` with a canned JSON document. It reuses the `std.http.Server` adapter from the previous chapter, so all TCP details remain within the standard library, and the rest of the program can treat the service as though it were running elsewhere (see net.zig (https://github.com/ziglang/zig/tree/master/lib/std/net.zig)). ### Subsection: Typed decoding strategy [section_id: typed-decoding] [section_url: https://zigbook.net/chapters/32__project-http-json-client#typed-decoding] The JSON document uses optional fields to describe different incident types, so the program first parses it into a "wire" struct that mirrors those optional fields, then promotes the data into a Zig `union(enum)` based on the `kind` property. This pattern keeps `std.json` parsing straightforward while still yielding an ergonomic domain model for downstream logic (see meta.zig (https://github.com/ziglang/zig/tree/master/lib/std/meta.zig)). ## Section: Fetch, decode, and present [section_id: fetch-decode-present] [section_url: https://zigbook.net/chapters/32__project-http-json-client#fetch-decode-present] The full program below wires the fixture, decoder, and renderer together. It can be run directly with `zig run`, and it prints a service table followed by any active incidents. ```zig const std = @import("std"); // Mock JSON response containing service health data for multiple regions. // In a real application, this would come from an actual API endpoint. const summary_payload = "{\n" ++ " \"regions\": [\n" ++ " {\n" ++ " \"name\": \"us-east\",\n" ++ " \"uptime\": 0.99983,\n" ++ " \"services\": [\n" ++ " {\"name\":\"auth\",\"state\":\"up\",\"latency_ms\":2.7},\n" ++ " {\"name\":\"billing\",\"state\":\"degraded\",\"latency_ms\":184.0},\n" ++ " {\"name\":\"search\",\"state\":\"up\",\"latency_ms\":5.1}\n" ++ " ],\n" ++ " \"incidents\": [\n" ++ " {\"kind\":\"maintenance\",\"window_start\":\"2025-11-06T01:00Z\",\"expected_minutes\":45}\n" ++ " ]\n" ++ " },\n" ++ " {\n" ++ " \"name\": \"eu-central\",\n" ++ " \"uptime\": 0.99841,\n" ++ " \"services\": [\n" ++ " {\"name\":\"auth\",\"state\":\"up\",\"latency_ms\":3.1},\n" ++ " {\"name\":\"billing\",\"state\":\"outage\",\"latency_ms\":0.0}\n" ++ " ],\n" ++ " \"incidents\": [\n" ++ " {\"kind\":\"outage\",\"started\":\"2025-11-05T08:12Z\",\"severity\":\"critical\"}\n" ++ " ]\n" ++ " }\n" ++ " ]\n" ++ "}\n"; // Coordination structure for passing server state between threads. // The ResetEvent enables the main thread to wait until the server is ready to accept connections. const ServerTask = struct { server: *std.net.Server, ready: *std.Thread.ResetEvent, }; // Runs a minimal HTTP server fixture on a background thread. // Responds to /api/status with the canned JSON payload above, // and returns 404 for all other paths. fn serveStatus(task: ServerTask) void { // Signal to the main thread that the server is listening and ready. task.ready.set(); const connection = task.server.accept() catch |err| { std.log.err("accept failed: {s}", .{@errorName(err)}); return; }; defer connection.stream.close(); // Allocate fixed buffers for HTTP protocol I/O. // The Reader and Writer interfaces wrap these buffers to manage state. var recv_buf: [4096]u8 = undefined; var send_buf: [4096]u8 = undefined; var reader = connection.stream.reader(&recv_buf); var writer = connection.stream.writer(&send_buf); var server = std.http.Server.init(reader.interface(), &writer.interface); // Handle incoming requests until the connection closes. while (server.reader.state == .ready) { var request = server.receiveHead() catch |err| switch (err) { error.HttpConnectionClosing => return, else => { std.log.err("receive head failed: {s}", .{@errorName(err)}); return; }, }; // Route based on request target (path). if (std.mem.eql(u8, request.head.target, "/api/status")) { request.respond(summary_payload, .{ .extra_headers = &.{ .{ .name = "content-type", .value = "application/json" }, }, }) catch |err| { std.log.err("respond failed: {s}", .{@errorName(err)}); return; }; } else { request.respond("not found\n", .{ .status = .not_found, .extra_headers = &.{ .{ .name = "content-type", .value = "text/plain" }, }, }) catch |err| { std.log.err("respond failed: {s}", .{@errorName(err)}); return; }; } } } // Domain model representing the final, typed structure of the service health data. // All slices are owned by an arena allocator tied to the request lifetime. const Summary = struct { regions: []Region, }; const Region = struct { name: []const u8, uptime: f64, services: []Service, incidents: []Incident, }; const Service = struct { name: []const u8, state: ServiceState, latency_ms: f64, }; const ServiceState = enum { up, degraded, outage }; // Tagged union modeling the two kinds of incidents. // Each variant carries its own payload structure. const Incident = union(enum) { maintenance: Maintenance, outage: Outage, }; const Maintenance = struct { window_start: []const u8, expected_minutes: u32, }; const Outage = struct { started: []const u8, severity: Severity, }; const Severity = enum { info, warning, critical }; // Wire format structures mirror the JSON shape exactly. // All fields are optional to match the loose JSON schema; // we promote them to the typed domain model after validation. const SummaryWire = struct { regions: []RegionWire, }; const RegionWire = struct { name: []const u8, uptime: f64, services: []ServiceWire, incidents: []IncidentWire, }; const ServiceWire = struct { name: []const u8, state: []const u8, latency_ms: f64, }; // All incident fields are optional because different incident kinds use different fields. const IncidentWire = struct { kind: []const u8, window_start: ?[]const u8 = null, expected_minutes: ?u32 = null, started: ?[]const u8 = null, severity: ?[]const u8 = null, }; // Custom error set for decoding and validation failures. const DecodeError = error{ UnknownServiceState, UnknownIncidentKind, UnknownSeverity, MissingField, }; // Allocates a copy of the input slice in the target allocator. // Used to transfer ownership of JSON strings from the parser's temporary buffers // into the arena allocator so they remain valid after parsing completes. fn dupeSlice(allocator: std.mem.Allocator, bytes: []const u8) ![]const u8 { const copy = try allocator.alloc(u8, bytes.len); @memcpy(copy, bytes); return copy; } // Maps a service state string to the corresponding enum variant. // Case-insensitive to handle variations in JSON formatting. fn parseServiceState(text: []const u8) DecodeError!ServiceState { if (std.ascii.eqlIgnoreCase(text, "up")) return .up; if (std.ascii.eqlIgnoreCase(text, "degraded")) return .degraded; if (std.ascii.eqlIgnoreCase(text, "outage")) return .outage; return error.UnknownServiceState; } // Parses severity strings into the Severity enum. fn parseSeverity(text: []const u8) DecodeError!Severity { if (std.ascii.eqlIgnoreCase(text, "info")) return .info; if (std.ascii.eqlIgnoreCase(text, "warning")) return .warning; if (std.ascii.eqlIgnoreCase(text, "critical")) return .critical; return error.UnknownSeverity; } // Promotes wire format data into the typed domain model. // Validates required fields, parses enums, and copies strings into the arena. // All allocations use the arena so cleanup is automatic when the arena is freed. fn buildSummary( arena: std.mem.Allocator, parsed: SummaryWire, ) (DecodeError || std.mem.Allocator.Error)!Summary { const regions = try arena.alloc(Region, parsed.regions.len); for (parsed.regions, regions) |wire, *region| { region.name = try dupeSlice(arena, wire.name); region.uptime = wire.uptime; // Convert each service from wire format to typed model. region.services = try arena.alloc(Service, wire.services.len); for (wire.services, region.services) |service_wire, *service| { service.name = try dupeSlice(arena, service_wire.name); service.state = try parseServiceState(service_wire.state); service.latency_ms = service_wire.latency_ms; } // Promote incidents into the tagged union based on the `kind` field. region.incidents = try arena.alloc(Incident, wire.incidents.len); for (wire.incidents, region.incidents) |incident_wire, *incident| { if (std.ascii.eqlIgnoreCase(incident_wire.kind, "maintenance")) { const window_start = incident_wire.window_start orelse return error.MissingField; const expected = incident_wire.expected_minutes orelse return error.MissingField; incident.* = .{ .maintenance = .{ .window_start = try dupeSlice(arena, window_start), .expected_minutes = expected, } }; } else if (std.ascii.eqlIgnoreCase(incident_wire.kind, "outage")) { const started = incident_wire.started orelse return error.MissingField; const severity_text = incident_wire.severity orelse return error.MissingField; const severity = try parseSeverity(severity_text); incident.* = .{ .outage = .{ .started = try dupeSlice(arena, started), .severity = severity, } }; } else { return error.UnknownIncidentKind; } } } return .{ .regions = regions }; } // Fetches the status endpoint via HTTP and decodes the JSON response into a Summary. // Uses a fixed buffer for the HTTP response; for larger payloads, switch to a streaming approach. fn fetchSummary(arena: std.mem.Allocator, client: *std.http.Client, url: []const u8) !Summary { var response_buffer: [4096]u8 = undefined; var response_writer = std.Io.Writer.fixed(response_buffer[0..]); // Perform the HTTP fetch with a custom User-Agent header. const result = try client.fetch(.{ .location = .{ .url = url }, .response_writer = &response_writer, .headers = .{ .user_agent = .{ .override = "zigbook-http-json-client/0.1" }, }, }); _ = result; // Extract the response body from the fixed writer's buffer. const body = response_writer.buffer[0..response_writer.end]; // Parse JSON into the wire format structures. var parsed = try std.json.parseFromSlice(SummaryWire, arena, body, .{}); defer parsed.deinit(); // Promote wire format to typed domain model. return buildSummary(arena, parsed.value); } // Renders the service summary as a formatted table followed by an incident list. // Uses a buffered writer for efficient output to stdout. fn renderSummary(summary: Summary) !void { var stdout_buffer: [1024]u8 = undefined; var stdout_writer = std.fs.File.stdout().writer(&stdout_buffer); const out = &stdout_writer.interface; // Print service table header. try out.writeAll("SERVICE SUMMARY\n"); try out.writeAll("Region Service State Latency (ms)\n"); try out.writeAll("-----------------------------------------------------\n"); // Print each service, grouped by region. for (summary.regions) |region| { for (region.services) |service| { try out.print("{s:<13}{s:<14}{s:<12}{d:7.1}\n", .{ region.name, service.name, @tagName(service.state), service.latency_ms, }); } } // Print incident section header. try out.writeAll("\nACTIVE INCIDENTS\n"); var incident_count: usize = 0; // Iterate all incidents across all regions and format based on kind. for (summary.regions) |region| { for (region.incidents) |incident| { incident_count += 1; switch (incident) { .maintenance => |m| try out.print("- {s}: maintenance window starts {s}, {d} min\n", .{ region.name, m.window_start, m.expected_minutes, }), .outage => |o| try out.print("- {s}: outage since {s} (severity: {s})\n", .{ region.name, o.started, @tagName(o.severity), }), } } } if (incident_count == 0) { try out.writeAll("- No active incidents reported.\n"); } try out.writeAll("\n"); try out.flush(); } pub fn main() !void { // Set up a general-purpose allocator for long-lived allocations (client, server). var gpa = std.heap.GeneralPurposeAllocator(.{}){}; defer _ = gpa.deinit(); const allocator = gpa.allocator(); // Bind to localhost on an OS-assigned port (port 0 → automatic selection). const address = try std.net.Address.parseIp("127.0.0.1", 0); var server = try address.listen(.{ .reuse_address = true }); defer server.deinit(); // Spin up the server fixture on a background thread. var ready = std.Thread.ResetEvent{}; const server_thread = try std.Thread.spawn(.{}, serveStatus, .{ServerTask{ .server = &server, .ready = &ready, }}); defer server_thread.join(); // Wait for the server thread to signal that it's ready to accept connections. ready.wait(); // Retrieve the actual port chosen by the OS. const port = server.listen_address.in.getPort(); // Initialize the HTTP client with the main allocator. var client = std.http.Client{ .allocator = allocator }; defer client.deinit(); // Create an arena allocator for all parsed data. // The arena owns all slices in the Summary; they're freed when the arena is destroyed. var arena_inst = std.heap.ArenaAllocator.init(allocator); defer arena_inst.deinit(); const arena = arena_inst.allocator(); // Set up buffered stdout for logging. var stdout_buffer: [256]u8 = undefined; var stdout_writer = std.fs.File.stdout().writer(&stdout_buffer); const log_out = &stdout_writer.interface; // Construct the full URL with the dynamically assigned port. var url_buffer: [128]u8 = undefined; const url = try std.fmt.bufPrint(&url_buffer, "http://127.0.0.1:{d}/api/status", .{port}); try log_out.print("Fetching {s}...\n", .{url}); // Fetch and decode the status endpoint. const summary = try fetchSummary(arena, &client, url); try log_out.print("Parsed {d} regions.\n\n", .{summary.regions.len}); try log_out.flush(); // Render the final report to stdout. try renderSummary(summary); } ``` This program relies on the modern Reader/Writer APIs and the HTTP client components introduced in Zig 0.15.2 (see Writer.zig (https://github.com/ziglang/zig/tree/master/lib/std/Io/Writer.zig)). Run: ```shell $ zig run main.zig ``` Output: ```shell Fetching http://127.0.0.1:46211/api/status... Parsed 2 regions. SERVICE SUMMARY Region Service State Latency (ms) ----------------------------------------------------- us-east auth up 2.7 us-east billing degraded 184.0 us-east search up 5.1 eu-central auth up 3.1 eu-central billing outage 0.0 ACTIVE INCIDENTS - us-east: maintenance window starts 2025-11-06T01:00Z, 45 min - eu-central: outage since 2025-11-05T08:12Z (severity: critical) ``` NOTE: Your port number will change each run because the server listens on `0` and lets the OS choose a free socket. The client constructs the URL dynamically from `server.listen_address.in.getPort()`. ### Subsection: Walkthrough [section_id: walkthrough] [section_url: https://zigbook.net/chapters/32__project-http-json-client#walkthrough] 1. Server bootstrap.`serveStatus` spins up `std.http.Server` on an accepted TCP stream, compares the request target, and responds with JSON or a 404. The summary payload lives in a multiline string, but you could just as easily emit it through `std.json.Stringify`. 2. Wire decoding and promotion. After fetching, the client parses it into `SummaryWire`, a structure of slices and optionals that reflect the JSON shape. `buildSummary` then allocates typed slices inside an arena and maps incident `kind` strings to union variants. Both the arena and fixed writer leverage the post-Writergate I/O APIs to control allocation explicitly. 3. Rendering.`renderSummary` prints the service table via `Writer.print` and iterates incidents, surfacing severity and scheduling details for each region. ## Section: Notes & Caveats [section_id: notes-caveats] [section_url: https://zigbook.net/chapters/32__project-http-json-client#notes-caveats] - `std.http.Client.fetch` buffers the entire response into the fixed writer; for larger payloads, swap in an arena-backed builder or stream tokens with `std.json.Scanner` (see Scanner.zig (https://github.com/ziglang/zig/tree/master/lib/std/json/Scanner.zig)). - The decoding logic assumes incident objects include the fields required for their `kind`. Validation failures bubble out as `error.MissingField`; adjust the error handling to downgrade or log if you expect partially populated data. - The arena allocator keeps all decoded slices alive for the lifetime of the report. If you need long-lived ownership, replace the arena with a longer-lived allocator and free slices manually when the report expires. arena_allocator.zig (https://github.com/ziglang/zig/tree/master/lib/std/heap/arena_allocator.zig) ## Section: Exercises [section_id: exercises] [section_url: https://zigbook.net/chapters/32__project-http-json-client#exercises] - Add a `--region` flag that filters the printed table to a specific region. Reuse the argument-parsing patterns from earlier CLI chapters before the networking section (see 05 (05__project-tempconv-cli.xml)). - Extend the JSON payload with historical latency percentiles and draw a textual sparkline or a min/median/max summary. Consult `std.fmt` for formatting helpers (see fmt.zig (https://github.com/ziglang/zig/tree/master/lib/std/fmt.zig)). - Replace the canned data with a live endpoint of your choosing, but wrap it with a timeout and fall back to the fixture to keep tests deterministic. ## Section: Caveats, alternatives, edge cases [section_id: caveats-alternatives-edge-cases] [section_url: https://zigbook.net/chapters/32__project-http-json-client#caveats-alternatives-edge-cases] - If the response grows beyond the `response_buffer` size, `client.fetch` reports `error.WriteFailed`. Handle that case by retrying with a heap-backed writer or by streaming the body to disk. - For union promotion, consider storing the original `SummaryWire` alongside your typed data so you can expose raw JSON fields in diagnostics without re-parsing. - In production code, you may want to reuse a single `std.http.Client` across multiple fetches; this example drops it after one request, but the API exposes a connection pool ready for reuse. # Chapter 33 — C Interop [chapter_id: 33__c-interop-import-export-abi] [chapter_slug: c-interop-import-export-abi] [chapter_number: 33] [chapter_url: https://zigbook.net/chapters/33__c-interop-import-export-abi] ## Section: Overview [section_id: overview] [section_url: https://zigbook.net/chapters/33__c-interop-import-export-abi#overview] Our HTTP client from the previous chapter consumed data authored in Zig (32 (32__project-http-json-client.xml)); real systems often have to lean on years of C code instead. This chapter shows how Zig 0.15.2 treats C as a first-class citizen: we pull in headers with `@cImport`, export Zig functions back to C, and verify that records keep their ABI promises. c.zig (https://github.com/ziglang/zig/tree/master/lib/std/c.zig) The standard library now routes both `std.c` and `std.builtin.CallingConvention` through the same modernization that touched the I/O stack, so this chapter highlights the most relevant changes while keeping the examples runnable with nothing more than `zig run`. builtin.zig (https://github.com/ziglang/zig/tree/master/lib/std/builtin.zig), v0.15.2 (https://ziglang.org/download/0.15.1/release-notes.html) ### Subsection: The C Interoperability Architecture [section_id: _the_c_interoperability_architecture] [section_url: https://zigbook.net/chapters/33__c-interop-import-export-abi#_the_c_interoperability_architecture] Before diving into `@cImport` mechanics, it’s valuable to understand how Zig’s C interop layer is organized. The following diagram shows the complete architecture from user code down to libc and system calls: ```text graph TB subgraph "User Code Layer" USER["User Application Code"] end subgraph "Standard Library Abstractions" OS["std.os
OS-specific wrappers"] POSIX["std.posix
POSIX API layer"] FS["std.fs
Filesystem API"] NET["std.net
Networking API"] PROCESS["std.process
Process management"] end subgraph "C Interoperability Layer" C["std.c
Main C types module"] subgraph "Platform-Specific Modules" DARWIN["c/darwin.zig
macOS/iOS types"] FREEBSD["c/freebsd.zig
FreeBSD types"] LINUX["os/linux.zig
Linux syscalls"] WINDOWS["os/windows.zig
Windows API"] NETBSD["c/netbsd.zig
NetBSD types"] OPENBSD["c/openbsd.zig
OpenBSD types"] SOLARIS["c/solaris.zig
Solaris types"] HAIKU["c/haiku.zig
Haiku types"] DRAGONFLY["c/dragonfly.zig
DragonflyBSD types"] end C --> DARWIN C --> FREEBSD C --> LINUX C --> WINDOWS C --> NETBSD C --> OPENBSD C --> SOLARIS C --> HAIKU C --> DRAGONFLY end subgraph "System Layer" LIBC["libc
C Standard Library"] SYSCALL["System Calls
Direct syscall interface"] WINAPI["Windows API
kernel32/ntdll"] end USER --> OS USER --> POSIX USER --> FS USER --> NET USER --> PROCESS OS --> C POSIX --> C FS --> POSIX NET --> POSIX PROCESS --> POSIX DARWIN --> LIBC FREEBSD --> LIBC NETBSD --> LIBC OPENBSD --> LIBC SOLARIS --> LIBC HAIKU --> LIBC DRAGONFLY --> LIBC LINUX --> LIBC LINUX --> SYSCALL WINDOWS --> WINAPI ``` This architecture reveals that `std.c` is not a monolithic module—it’s a dispatcher that uses compile-time logic (`builtin.os.tag`) to import platform-specific C type definitions. When you write Zig code for macOS, `std.c` pulls types from `c/darwin.zig`; on FreeBSD, it uses `c/freebsd.zig`; on Windows, `os/windows.zig`; and so forth. These platform-specific modules define C types like `c_int`, `timespec`, `fd_t`, and platform constants, then interface with either libc (when `-lc` is specified) or direct system calls (on Linux). Importantly, Zig’s own standard library (`std.fs`, `std.net`, `std.process`) uses this same C interop layer—when you call `std.posix.open()`, it resolves to `std.c.open()` internally. Understanding this architecture helps you reason about why certain C types are available on some platforms but not others, why `-lc` is needed for linking libc symbols, and how your `@cImport` code sits alongside Zig’s built-in C interop. ## Section: Learning Goals [section_id: learning-goals] [section_url: https://zigbook.net/chapters/33__c-interop-import-export-abi#learning-goals] - Wire a Zig executable to C headers and companion source using `@cImport` and the built-in C toolchain. - Export Zig functions with a C ABI so existing C code can invoke them without glue. - Map C structs onto Zig `extern` structs and confirm that layout, size, and call semantics align. ## Section: Importing C APIs into Zig [section_id: importing-c-apis] [section_url: https://zigbook.net/chapters/33__c-interop-import-export-abi#importing-c-apis] `@cImport` compiles a slice of C code alongside your Zig module, honoring include paths, defines, and extra C sources you pass on the command line. This lets one executable take dependencies on both languages without a separate build system. ### Subsection: Round-tripping through [section_id: c-roundtrip] [section_url: https://zigbook.net/chapters/33__c-interop-import-export-abi#c-roundtrip] The first example pulls a header and C source that multiply two integers, then demonstrates calling a Zig-exported function from inline C in the same header. ```zig // Import the Zig standard library for basic functionality const std = @import("std"); // Import C header file using @cImport to interoperate with C code // This creates a namespace 'c' containing all declarations from "bridge.h" const c = @cImport({ @cInclude("bridge.h"); }); // Export a Zig function with C calling convention so it can be called from C // The 'export' keyword makes this function visible to C code // callconv(.c) ensures it uses the platform's C ABI for parameter passing and stack management export fn zig_add(a: c_int, b: c_int) callconv(.c) c_int { return a + b; } pub fn main() !void { // Create a fixed-size buffer for stdout to avoid heap allocations var stdout_buffer: [128]u8 = undefined; var stdout_writer = std.fs.File.stdout().writer(&stdout_buffer); const out = &stdout_writer.interface; // Call C function c_mul from the imported header // This demonstrates Zig calling into C code seamlessly const mul = c.c_mul(6, 7); // Call C function that internally calls back into our exported zig_add function // This demonstrates the round-trip: Zig -> C -> Zig const sum = c.call_zig_add(19, 23); // Print the result from the C multiplication function try out.print("c_mul(6, 7) = {d}\n", .{mul}); // Print the result from the C function that called our Zig function try out.print("call_zig_add(19, 23) = {d}\n", .{sum}); // Flush the buffered output to ensure all data is written try out.flush(); } ``` This program includes `bridge.h` via `@cInclude`, links the accompanying `bridge.c`, and exports `zig_add` with the platform’s C calling convention so inline C can call back into Zig. Run: ```shell $ zig run \ -Ichapters-data/code/33__c-interop-import-export-abi \ chapters-data/code/33__c-interop-import-export-abi/01_c_roundtrip.zig \ chapters-data/code/33__c-interop-import-export-abi/bridge.c ``` Output: ```shell c_mul(6, 7) = 42 call_zig_add(19, 23) = 42 ``` IMPORTANT: Passing `-I` keeps the header discoverable, and listing the C file on the same command line instructs the Zig compiler to compile and link it into the run artifact. build.zig (https://github.com/ziglang/zig/tree/master/lib/std/build.zig) ### Subsection: Exporting Zig functions to C [section_id: exporting-zig] [section_url: https://zigbook.net/chapters/33__c-interop-import-export-abi#exporting-zig] Zig functions gain a C ABI when you mark them `export` and select `callconv(.c)`, which expands to the target’s default C calling convention. Anything callable from inline C via `@cImport` can also be called from a separately compiled C object with the same prototype, so this pattern works equally well when you ship a shared library. ### Subsection: Understanding C Calling Conventions [section_id: _understanding_c_calling_conventions] [section_url: https://zigbook.net/chapters/33__c-interop-import-export-abi#_understanding_c_calling_conventions] The `callconv(.c)` annotation is not a single universal calling convention—it resolves to platform-specific conventions based on the target architecture. The following diagram shows how this resolution works: ```text graph LR subgraph "C Calling Convention Resolution" TARGET["target.cCallingConvention()"] TARGET --> X86["x86_64: SysV or Win64"] TARGET --> ARM["aarch64: AAPCS"] TARGET --> WASM["wasm32/64: C"] TARGET --> RISCV["riscv64: C"] TARGET --> SPIRV["spirv: unsupported"] end subgraph "Platform Specifics" X86 --> SYSV["SysV
Linux, macOS, BSD"] X86 --> WIN64["Win64
Windows"] ARM --> AAPCS["AAPCS
standard ARM ABI"] end ``` When you write `callconv(.c)`, Zig automatically selects the appropriate C calling convention for your target. On x86_64 Linux, macOS, or BSD systems, this resolves to System V ABI—arguments pass in registers `rdi`, `rsi`, `rdx`, `rcx`, `r8`, `r9`, then stack; return values use `rax`. On x86_64 Windows, it becomes Win64 calling convention—arguments pass in `rcx`, `rdx`, `r8`, `r9`, then stack; the caller must reserve shadow space. On ARM (aarch64), it’s AAPCS (ARM Architecture Procedure Call Standard) with its own register usage rules. This automatic resolution is why the same `export fn zig_add(a: i32, b: i32) callconv(.c) i32` works correctly across platforms without modification—Zig generates the right prologue, epilogue, and register usage for each target. When debugging calling convention mismatches or writing assembly interop, knowing which convention is active helps you match register assignments and stack layouts correctly. ## Section: Matching data layouts and ABI guarantees [section_id: abi-compatibility] [section_url: https://zigbook.net/chapters/33__c-interop-import-export-abi#abi-compatibility] Being callable is only half the work; you also need to agree on layout rules so that structs and aggregates have the same size, alignment, and field ordering on both sides of the boundary. ### Subsection: Understanding ABIs and Object Formats [section_id: _understanding_abis_and_object_formats] [section_url: https://zigbook.net/chapters/33__c-interop-import-export-abi#_understanding_abis_and_object_formats] The Application Binary Interface (ABI) defines calling conventions, name mangling, struct layout rules, and how types are passed between functions. Different ABIs have different rules, which affect C interop compatibility: ```text graph TB subgraph "Common ABIs" ABI["Abi enum"] ABI --> GNU["gnu
GNU toolchain"] ABI --> MUSL["musl
musl libc"] ABI --> MSVC["msvc
Microsoft Visual C++"] ABI --> NONE["none
freestanding"] ABI --> ANDROID["android, gnueabi, etc
platform variants"] end subgraph "Object Formats" OFMT["ObjectFormat enum"] OFMT --> ELF["elf
Linux, BSD"] OFMT --> MACHO["macho
Darwin systems"] OFMT --> COFF["coff
Windows PE"] OFMT --> WASM["wasm
WebAssembly"] OFMT --> C["c
C source output"] OFMT --> SPIRV["spirv
Shaders"] end ``` The ABI choice affects how `extern struct` fields are laid out. The gnu ABI (GNU toolchain, used on most Linux systems) follows specific struct padding and alignment rules from GCC. The msvc ABI (Microsoft Visual C++) has different rules—for example, `long` is 32-bit on Windows x64 but 64-bit on Linux x64. The musl ABI targets musl libc with slightly different calling conventions than glibc. The none ABI is for freestanding environments with no libc. When you declare `extern struct SensorData`, Zig uses the target’s ABI rules to compute field offsets and padding, ensuring they match what C would produce. The object format (ELF, Mach-O, COFF, WASM) determines which linker is used and how symbols are encoded, but the ABI determines the actual memory layout. This is why the chapter emphasizes `@sizeOf` checks—if Zig and C disagree about struct size, you likely have an ABI mismatch or wrong target specification. ### Subsection: for shared layouts [section_id: extern-structs] [section_url: https://zigbook.net/chapters/33__c-interop-import-export-abi#extern-structs] This example mirrors a C struct that the sensor firmware publishes. We import the header, declare an `extern struct` with matching fields, and double-check that Zig and C agree about the size before calling helper routines compiled from C. ```zig // Import the Zig standard library for basic functionality const std = @import("std"); // Import C header file using @cImport to interoperate with C code // This creates a namespace 'c' containing all declarations from "abi.h" const c = @cImport({ @cInclude("abi.h"); }); // Define a Zig struct with 'extern' keyword to match C ABI layout // The 'extern' keyword ensures the struct uses C-compatible memory layout // without Zig's automatic padding optimizations const SensorSample = extern struct { temperature_c: f32, // Temperature reading in Celsius (32-bit float) status_bits: u16, // Status flags packed into 16 bits port_id: u8, // Port identifier (8-bit unsigned) reserved: u8 = 0, // Reserved byte for alignment/future use, default to 0 }; // Convert a C struct to its Zig equivalent using pointer casting // This demonstrates type-punning between C and Zig representations // @ptrCast reinterprets the memory layout without copying data fn fromC(sample: c.struct_SensorSample) SensorSample { return @as(*const SensorSample, @ptrCast(&sample)).*; } pub fn main() !void { // Create a fixed-size buffer for stdout to avoid allocations var stdout_buffer: [256]u8 = undefined; var stdout_writer = std.fs.File.stdout().writer(&stdout_buffer); const out = &stdout_writer.interface; // Print size comparison between C and Zig struct representations // Both should be identical due to 'extern' struct attribute try out.print("sizeof(C struct) = {d}\n", .{@sizeOf(c.struct_SensorSample)}); try out.print("sizeof(Zig extern struct) = {d}\n", .{@sizeOf(SensorSample)}); // Call C functions to create sensor samples with specific values const left = c.make_sensor_sample(42.5, 0x0102, 7); const right = c.make_sensor_sample(38.0, 0x0004, 9); // Call C function that operates on C structs and returns a computed value const total = c.combined_voltage(left, right); // Convert C structs to Zig structs for idiomatic Zig access const zig_left = fromC(left); const zig_right = fromC(right); // Print sensor data from the left port with formatted output try out.print( "left port {d}: {d} status bits, {d:.2} °C\n", .{ zig_left.port_id, zig_left.status_bits, zig_left.temperature_c }, ); // Print sensor data from the right port with formatted output try out.print( "right port {d}: {d} status bits, {d:.2} °C\n", .{ zig_right.port_id, zig_right.status_bits, zig_right.temperature_c }, ); // Print the combined voltage result computed by C function try out.print("combined_voltage = {d:.3}\n", .{total}); // Flush the buffered output to ensure all data is written try out.flush(); } ``` The helper functions originate from `abi.c`, so the run command links both files and exposes the C aggregation routine to Zig. Run: ```shell $ zig run \ -Ichapters-data/code/33__c-interop-import-export-abi \ chapters-data/code/33__c-interop-import-export-abi/02_abi_layout.zig \ chapters-data/code/33__c-interop-import-export-abi/abi.c ``` Output: ```shell sizeof(C struct) = 8 sizeof(Zig extern struct) = 8 left port 7: 258 status bits, 42.50 °C right port 9: 4 status bits, 38.00 °C combined_voltage = 1.067 ``` TIP: If the `@sizeOf` assertions disagree, double-check padding bytes and prefer `extern struct` over `packed` unless you have an explicit reason to change ABI rules. ### Subsection: translate-c and build integration [section_id: tooling-translate-c] [section_url: https://zigbook.net/chapters/33__c-interop-import-export-abi#tooling-translate-c] For larger headers, consider running `zig translate-c` to snapshot them into Zig source. The build system can also register C objects and headers via `addCSourceFile` and `addIncludeDir`, making the `zig run` invocations above part of a repeatable package instead of ad-hoc commands. ## Section: Notes & Caveats [section_id: notes-caveats] [section_url: https://zigbook.net/chapters/33__c-interop-import-export-abi#notes-caveats] - Zig does not automatically link platform libraries; pass `-lc` or add the appropriate build options when importing APIs that live outside your project. - `@cImport` emits one translation unit; wrap headers in `#pragma once` or include guards to avoid duplicate definitions just as you would in pure C projects. - Avoid `packed` unless you control both compilers and targets; packed fields can change alignment guarantees and lead to unaligned loads on architectures that forbid them. ## Section: Exercises [section_id: exercises] [section_url: https://zigbook.net/chapters/33__c-interop-import-export-abi#exercises] - Extend `bridge.h` with a function that returns a struct by value and demonstrate consuming it from Zig without copying through pointers. - Export a Zig function that fills a caller-provided C buffer and inspect its symbol with `zig build-obj` plus `llvm-nm` or your platform’s equivalent. - Swap `extern struct` for a `packed struct` in the ABI example and run it on a target with strict alignment to observe the differences in emitted machine code. ## Section: Caveats, alternatives, edge cases [section_id: caveats-alternatives-edge-cases] [section_url: https://zigbook.net/chapters/33__c-interop-import-export-abi#caveats-alternatives-edge-cases] - Some C ABIs mangle names (e.g., Windows `__stdcall`); override the calling convention or use `@export` with an explicit symbol name when interoperating with non-default ABIs. - `@cImport` cannot compile C—translate headers with `extern "C"` wrappers or use a C shim when binding C libraries. - When bridging variadic functions, prefer writing a Zig wrapper that marshals arguments explicitly; Zig’s variadics only cover C’s default promotions, not custom ellipsis semantics. # Chapter 34 — GPU Fundamentals [chapter_id: 34__gpu-fundamentals] [chapter_slug: gpu-fundamentals] [chapter_number: 34] [chapter_url: https://zigbook.net/chapters/34__gpu-fundamentals] ## Section: Overview [section_id: overview] [section_url: https://zigbook.net/chapters/34__gpu-fundamentals#overview] The C interop bridge from the previous chapter lets Zig speak to decades of native code (see 33 (33__c-interop-import-export-abi.xml)); the next frontier is harnessing massively parallel devices without abandoning Zig’s ergonomics. We will map GPU execution models onto Zig’s language primitives, examine how address spaces and calling conventions constrain kernel code, and learn the build flags that tame the still-evolving SPIR-V toolchain (see v0.15.2 (https://ziglang.org/download/0.15.1/release-notes.html)). Along the way, we will contrast compute-first design with graphics pipelines, highlight where Zig’s standard library already understands GPU targets, and outline pragmatic host/device coordination patterns for projects that still need to run on pure CPU hardware (see Target.zig (https://github.com/ziglang/zig/tree/master/lib/std/Target.zig)). ## Section: Learning Goals [section_id: learning-goals] [section_url: https://zigbook.net/chapters/34__gpu-fundamentals#learning-goals] - Relate Zig’s compilation model to GPU execution hierarchies and memory classes. - Declare and compile GPU kernels with explicit calling conventions and address spaces. - Plan launch parameters that gracefully degrade to CPU fallbacks when accelerators are absent. See builtin.zig (https://github.com/ziglang/zig/tree/master/lib/std/builtin.zig) and math.zig (https://github.com/ziglang/zig/tree/master/lib/std/math.zig) for related definitions. ## Section: GPU Architecture Foundations [section_id: gpu-architecture-foundations] [section_url: https://zigbook.net/chapters/34__gpu-fundamentals#gpu-architecture-foundations] GPUs expose thousands of lightweight threads arranged into hierarchies of work items, work groups, and grids; Zig surfaces those indices through builtins like `@workGroupId`, `@workGroupSize`, and `@workItemId`, keeping the model explicit so kernels remain predictable. Because GPU compilers penalize implicit global state, Zig’s bias toward explicit parameters and result locations naturally fits the deterministic flow demanded by SIMT hardware. ### Subsection: Execution model: SIMT and thread groups [section_id: simt-execution-model] [section_url: https://zigbook.net/chapters/34__gpu-fundamentals#simt-execution-model] Single-instruction, multiple-thread (SIMT) execution bundles lanes into warps or wavefronts that run the same opcode stream until divergence. When you compile for targets such as `.spirv32`, `.spirv64`, `.nvptx`, or `.amdgcn`, Zig swaps its default calling convention for specialized GPU variants, so `callconv(.kernel)` emits code that satisfies each platform’s scheduler expectations. Divergence is handled explicitly: branching on per-lane values results in predicate masks that stall inactive threads, so structuring kernels with coarse branching keeps throughput predictable. ### Subsection: Memory hierarchies and address spaces [section_id: memory-hierarchy-address-spaces] [section_url: https://zigbook.net/chapters/34__gpu-fundamentals#memory-hierarchy-address-spaces] Zig models GPU memories through first-class address spaces — `.global`, `.shared`, `.local`, `.constant`, `.storage_buffer`, and more — each with its own coherence and lifetime rules. The compiler refuses pointer arithmetic that crosses into disallowed spaces, forcing kernel authors to acknowledge when data lives in shared memory versus device-global buffers. Use explicit casts like `@addrSpaceCast` only when you can prove the access rules remain valid, and prefer `extern struct` payloads for data shared with host APIs to guarantee layout stability. ### Subsection: Compute vs graphics pipelines [section_id: compute-vs-graphics] [section_url: https://zigbook.net/chapters/34__gpu-fundamentals#compute-vs-graphics] Compute kernels are just SPIR-V or PTX entry points that you enqueue from host code; graphics shaders traverse a fixed pipeline that Zig currently treats as external binaries you author in shading languages or translated SPIR-V blobs. Zig’s `@import` system does not yet generate render pipelines, but you can embed precompiled SPIR-V and dispatch it through Vulkan or WebGPU hosts written in Zig, integrating with the same allocator and error handling discipline you rely on elsewhere in the standard library. ## Section: Targeting GPUs with Zig [section_id: targeting-gpus-with-zig] [section_url: https://zigbook.net/chapters/34__gpu-fundamentals#targeting-gpus-with-zig] The compiler’s view of a build is captured by `builtin.target`, which records the architecture, OS tag, ABI, and permitted address spaces; toggling `-target` at the CLI level is enough to retarget code for host CPUs, SPIR-V, or CUDA backends. Zig 0.15.2 ships both the self-hosted SPIR-V backend and an LLVM-based fallback selectable with `-fllvm`, letting you experiment with whichever pipeline better matches your downstream drivers. ### Subsection: Understanding the Target Structure [section_id: _understanding_the_target_structure] [section_url: https://zigbook.net/chapters/34__gpu-fundamentals#_understanding_the_target_structure] Before working with GPU-specific compilation, it’s valuable to understand how Zig represents compilation targets internally. The following diagram shows the complete `std.Target` structure: ```text graph TB subgraph "std.Target Structure" TARGET["std.Target"] CPU["cpu: Cpu"] OS["os: Os"] ABI["abi: Abi"] OFMT["ofmt: ObjectFormat"] DYNLINKER["dynamic_linker: DynamicLinker"] TARGET --> CPU TARGET --> OS TARGET --> ABI TARGET --> OFMT TARGET --> DYNLINKER end subgraph "Cpu Components" CPU --> ARCH["arch: Cpu.Arch"] CPU --> MODEL["model: *const Cpu.Model"] CPU --> FEATURES["features: Feature.Set"] ARCH --> ARCHEX["x86_64, aarch64, wasm32, etc"] MODEL --> MODELEX["generic, native, specific variants"] FEATURES --> FEATEX["CPU feature flags"] end subgraph "Os Components" OS --> OSTAG["tag: Os.Tag"] OS --> VERSION["version_range: VersionRange"] OSTAG --> OSEX["linux, windows, macos, wasi, etc"] VERSION --> VERUNION["linux: LinuxVersionRange
windows: WindowsVersion.Range
semver: SemanticVersion.Range
none: void"] end subgraph "Abi and Format" ABI --> ABIEX["gnu, musl, msvc, none, etc"] OFMT --> OFMTEX["elf, macho, coff, wasm, c, spirv"] end ``` This target structure reveals how GPU compilation integrates with Zig’s type system. When you specify `-target spirv32-vulkan-none`, you’re setting: CPU arch to `spirv32` (32-bit SPIR-V), OS tag to `vulkan` (Vulkan environment), ABI to `none` (freestanding, no C runtime), and implicitly ObjectFormat to `spirv`. The target fully determines code generation behavior: `builtin.target.cpu.arch.isSpirV()` returns true, address space support is enabled, and the compiler selects the SPIR-V backend instead of x86_64 or ARM code generation. This same structure handles all targets—CPU, GPU, WebAssembly, bare metal—with uniform semantics. The ObjectFormat field (`ofmt`) tells the linker which binary format to produce: `elf` for Linux executables, `macho` for Darwin, `coff` for Windows, `wasm` for WebAssembly, and `spirv` for GPU shaders. Understanding this architecture helps you decode target triples, predict which builtins are available (like `@workGroupId` on GPU targets), and troubleshoot cross-compilation issues. ### Subsection: Inspecting targets and address spaces [section_id: inspecting-targets] [section_url: https://zigbook.net/chapters/34__gpu-fundamentals#inspecting-targets] This first example introspects the native build target, reports which GPU address spaces the compiler allows, and synthesizes a cross-compilation triple for SPIR-V. Running it on non-GPU hosts still teaches the vocabulary Zig uses to describe accelerators (see Query.zig (https://github.com/ziglang/zig/tree/master/lib/std/Target/Query.zig)). ```zig const std = @import("std"); const builtin = @import("builtin"); pub fn main() !void { // Query the compile-time target information to inspect the environment // this binary is being compiled for (host or cross-compilation target) const target = builtin.target; // Display basic target information: CPU architecture, OS, and object format std.debug.print("host architecture: {s}\n", .{@tagName(target.cpu.arch)}); std.debug.print("host operating system: {s}\n", .{@tagName(target.os.tag)}); std.debug.print("default object format: {s}\n", .{@tagName(target.ofmt)}); // Check if we're compiling for a GPU backend by examining the target CPU architecture. // GPU architectures include AMD GCN, NVIDIA PTX variants, and SPIR-V targets. const is_gpu_backend = switch (target.cpu.arch) { .amdgcn, .nvptx, .nvptx64, .spirv32, .spirv64 => true, else => false, }; std.debug.print("compiling as GPU backend: {}\n", .{is_gpu_backend}); // Import address space types for querying GPU-specific memory capabilities const AddressSpace = std.builtin.AddressSpace; const Context = AddressSpace.Context; // Query whether the target supports GPU-specific address spaces: // - shared: memory shared within a workgroup/threadblock // - constant: read-only memory optimized for uniform access across threads const shared_ok = target.cpu.supportsAddressSpace(AddressSpace.shared, null); const constant_ok = target.cpu.supportsAddressSpace(AddressSpace.constant, Context.constant); std.debug.print("supports shared address space: {}\n", .{shared_ok}); std.debug.print("supports constant address space: {}\n", .{constant_ok}); // Construct a custom target query for SPIR-V 64-bit targeting Vulkan const gpa = std.heap.page_allocator; const query = std.Target.Query{ .cpu_arch = .spirv64, .os_tag = .vulkan, .abi = .none, }; // Convert the target query to a triple string (e.g., "spirv64-vulkan") const triple = try query.zigTriple(gpa); defer gpa.free(triple); std.debug.print("example SPIR-V triple: {s}\n", .{triple}); } ``` Run: ```shell $ zig run 01_target_introspection.zig ``` Output: ```shell host architecture: x86_64 host operating system: linux default object format: elf compiling as GPU backend: false supports shared address space: false supports constant address space: false example SPIR-V triple: spirv64-vulkan-none ``` TIP: Even when the native arch is a CPU, synthesizing a SPIR-V triple helps you wire up build steps that emit GPU binaries without switching machines. ### Subsection: Declaring kernels and dispatch metadata [section_id: declaring-kernels] [section_url: https://zigbook.net/chapters/34__gpu-fundamentals#declaring-kernels] The kernel below stores its dispatch coordinates in a storage-buffer struct, illustrating how GPU-specific calling conventions, address spaces, and builtins compose. Compiling requires a SPIR-V target and the self-hosted backend (`-fno-llvm`) so Zig emits binary modules ready for Vulkan or WebGPU queue submission. ```zig //! GPU Kernel: Coordinate Capture //! //! This module demonstrates a minimal SPIR-V compute kernel that captures GPU dispatch //! coordinates into a storage buffer. It shows how to use Zig's GPU-specific builtins //! and address space annotations to write kernels that compile to SPIR-V. const builtin = @import("builtin"); /// Represents GPU dispatch coordinates for a single invocation /// /// Uses `extern` layout to guarantee memory layout matches host-side expectations, /// ensuring the kernel's output can be safely interpreted by CPU code reading the buffer. const Coordinates = extern struct { /// Work group ID (which group this invocation belongs to) group: u32, /// Work group size (number of invocations per group in this dimension) group_size: u32, /// Local invocation ID within the work group (0 to group_size-1) local: u32, /// Global linear ID across all invocations (group * group_size + local) linear: u32, }; /// GPU kernel entry point that captures dispatch coordinates /// /// This function must be exported so the SPIR-V compiler generates an entry point. /// The `callconv(.kernel)` calling convention tells Zig to emit GPU-specific function /// attributes and handle parameter passing according to compute shader ABI. /// /// Parameters: /// - out: Pointer to storage buffer where coordinates will be written. /// The `.storage_buffer` address space annotation ensures proper /// memory access patterns for device-visible GPU memory. pub export fn captureCoordinates(out: *addrspace(.storage_buffer) Coordinates) callconv(.kernel) void { // Query the work group ID in the X dimension (first dimension) // @workGroupId is a GPU-specific builtin that returns the current work group's coordinate const group = @workGroupId(0); // Query the work group size (how many invocations per group in this dimension) // This is set at dispatch time by the host and queried here for completeness const group_size = @workGroupSize(0); // Query the local invocation ID within this work group (0 to group_size-1) // @workItemId is the per-work-group thread index const local = @workItemId(0); // Calculate global linear index across all invocations // This formula converts 2D coordinates (group, local) to a flat 1D index const linear = group * group_size + local; // Write all captured coordinates to the output buffer // The GPU will ensure this write is visible to the host after synchronization out.* = .{ .group = group, .group_size = group_size, .local = local, .linear = linear, }; } // Compile-time validation to ensure this module is only compiled for SPIR-V targets // This prevents accidental compilation for CPU architectures where GPU builtins are unavailable comptime { switch (builtin.target.cpu.arch) { // Accept both 32-bit and 64-bit SPIR-V architectures .spirv32, .spirv64 => {}, // Reject all other architectures with a helpful error message else => @compileError("captureCoordinates must be compiled with a SPIR-V target, e.g. -target spirv32-vulkan-none"), } } ``` Run: ```shell $ zig build-obj -fno-llvm -O ReleaseSmall -target spirv32-vulkan-none \ -femit-bin=chapters-data/code/34__gpu-fundamentals/capture_coordinates.spv \ chapters-data/code/34__gpu-fundamentals/02_spirv_fill_kernel.zig ``` Output: ```shell no output (binary module generated) ``` NOTE: The emitted `.spv` blob slots directly into Vulkan’s `vkCreateShaderModule` or WebGPU’s `wgpuDeviceCreateShaderModule`, and the `extern struct` ensures host descriptors match the kernel’s expected layout. ### Subsection: Toolchain choices and binary formats [section_id: toolchain-selection] [section_url: https://zigbook.net/chapters/34__gpu-fundamentals#toolchain-selection] Zig’s build system can register GPU artifacts via `addObject` or `addLibrary`, allowing you to tuck SPIR-V modules alongside CPU executables in a single workspace. When SPIR-V validation demands specific environments (Vulkan versus OpenCL), set the OS tag in your `-target` triple accordingly, and pin optimization modes (`-O ReleaseSmall` for shaders) to control instruction counts and register pressure (see build.zig (https://github.com/ziglang/zig/tree/master/lib/std/build.zig)). Fallbacks like `-fllvm` unlock vendor-specific features when the self-hosted backend trails the latest SPIR-V extensions. ### Subsection: Object Formats and ABI for GPU Targets [section_id: _object_formats_and_abi_for_gpu_targets] [section_url: https://zigbook.net/chapters/34__gpu-fundamentals#_object_formats_and_abi_for_gpu_targets] SPIR-V is a first-class object format in Zig, sitting alongside traditional executable formats. The following diagram shows how object formats and ABIs are organized: ```text graph TB subgraph "Common ABIs" ABI["Abi enum"] ABI --> GNU["gnu
GNU toolchain"] ABI --> MUSL["musl
musl libc"] ABI --> MSVC["msvc
Microsoft Visual C++"] ABI --> NONE["none
freestanding"] ABI --> ANDROID["android, gnueabi, etc
platform variants"] end subgraph "Object Formats" OFMT["ObjectFormat enum"] OFMT --> ELF["elf
Linux, BSD"] OFMT --> MACHO["macho
Darwin systems"] OFMT --> COFF["coff
Windows PE"] OFMT --> WASM["wasm
WebAssembly"] OFMT --> C["c
C source output"] OFMT --> SPIRV["spirv
Shaders"] end ``` GPU kernels typically use `abi = none` because they run in freestanding environments without a C runtime—no libc, no standard library initialization, just raw compute. The SPIR-V object format produces `.spv` binaries that bypass traditional linking: instead of resolving relocations and merging sections like ELF or Mach-O linkers do, SPIR-V modules are complete, self-contained shader programs ready for consumption by Vulkan’s `vkCreateShaderModule` or WebGPU’s shader creation APIs. This is why you don’t need a separate linking step for GPU code—the compiler emits final binaries directly. When you specify `-target spirv32-vulkan-none`, the `none` ABI tells Zig to skip all C runtime setup, and the `spirv` object format ensures the output is valid SPIR-V bytecode rather than an executable with entry points and program headers. ### Subsection: Code Generation Backend Architecture [section_id: _code_generation_backend_architecture] [section_url: https://zigbook.net/chapters/34__gpu-fundamentals#_code_generation_backend_architecture] Zig supports multiple code generation backends, giving you flexibility in how SPIR-V is produced: ```text graph TB subgraph "Code Generation" CG["Code Generation"] CG --> LLVM["LLVM Backend
use_llvm flag"] CG --> NATIVE["Native Backends
x86_64, aarch64, wasm, riscv64"] CG --> CBACK["C Backend
ofmt == .c"] end ``` The LLVM Backend (`-fllvm`) routes through LLVM’s SPIR-V target, which supports vendor-specific extensions and newer SPIR-V versions. Use this when you need features the self-hosted backend hasn’t implemented yet, or when debugging compiler issues—LLVM’s mature SPIR-V support provides a known-good reference. The Native Backends (`-fno-llvm`, the default) use Zig’s self-hosted code generation for SPIR-V, which is faster to compile and produces smaller binaries but may lag behind LLVM in extension support. For SPIR-V, the self-hosted backend emits bytecode directly without intermediate representations. The C Backend isn’t applicable to GPU targets, but demonstrates Zig’s multi-backend flexibility. When experimenting with GPU code, start with `-fno-llvm` for faster iteration; switch to `-fllvm` if you encounter missing SPIR-V features or need to compare output against a reference implementation. The choice affects compilation speed and feature availability but not the API you write—your kernel code remains identical. ## Section: Launch Planning and Data Parallel Patterns [section_id: launch-planning-data-parallel] [section_url: https://zigbook.net/chapters/34__gpu-fundamentals#launch-planning-data-parallel] Choosing launch sizes involves balancing GPU occupancy with shared-memory budgets, while CPU fallbacks should reuse the same arithmetic so correctness stays identical across devices. Zig’s strong typing makes these calculations explicit, encouraging reusable helpers for both host planners and kernels. ### Subsection: Choosing workgroup sizes [section_id: workgroup-sizing] [section_url: https://zigbook.net/chapters/34__gpu-fundamentals#workgroup-sizing] This helper computes how many work groups you need for a problem size, how much padding the final group introduces, and models the same computation for CPU-side chunking. Using one routine eliminates off-by-one desynchronization between host and device scheduling. ```zig //! GPU Dispatch Planning Utility //! //! This module demonstrates how to calculate workgroup dispatch parameters for GPU compute shaders. //! It shows the relationship between total work items, workgroup size, and the resulting dispatch //! configuration, including handling of "tail" elements that don't fill a complete workgroup. const std = @import("std"); /// Represents a complete dispatch configuration for parallel execution /// Contains all necessary parameters to launch a compute kernel or parallel task const DispatchPlan = struct { /// Size of each workgroup (number of threads/invocations per group) workgroup_size: u32, /// Number of workgroups needed to cover all items group_count: u32, /// Total invocations including padding (always a multiple of workgroup_size) padded_invocations: u32, /// Number of padded/unused invocations in the last workgroup tail: u32, }; /// Computes optimal dispatch parameters for a given problem size and workgroup configuration /// /// Calculates how many workgroups are needed to process all items, accounting for the fact /// that the last workgroup may be partially filled. This is essential for GPU compute shaders /// where work must be dispatched in multiples of the workgroup size. fn computeDispatch(total_items: u32, workgroup_size: u32) DispatchPlan { // Ensure workgroup size is valid (GPU workgroups cannot be empty) std.debug.assert(workgroup_size > 0); // Calculate number of workgroups needed, rounding up to ensure all items are covered const groups = std.math.divCeil(u32, total_items, workgroup_size) catch unreachable; // Calculate total invocations including padding (GPU always launches complete workgroups) const padded = groups * workgroup_size; return .{ .workgroup_size = workgroup_size, .group_count = groups, .padded_invocations = padded, // Tail represents wasted invocations that must be handled with bounds checks .tail = padded - total_items, }; } /// Simulates CPU-side parallel execution planning using the same dispatch logic /// /// Demonstrates that the workgroup dispatch formula applies equally to CPU thread batching, /// ensuring consistent behavior between GPU and CPU fallback implementations. fn simulateCpuFallback(total_items: u32, lanes: u32) DispatchPlan { // Reuse the GPU formula so host-side chunking matches device scheduling. return computeDispatch(total_items, lanes); } pub fn main() !void { // Define a sample problem: processing 1000 items const problem_size: u32 = 1000; // Typical GPU workgroup size (often 32, 64, or 256 depending on hardware) const workgroup_size: u32 = 64; // Calculate GPU dispatch configuration const plan = computeDispatch(problem_size, workgroup_size); std.debug.print( "gpu dispatch: {d} groups × {d} lanes => {d} invocations (tail {d})\n", .{ plan.group_count, plan.workgroup_size, plan.padded_invocations, plan.tail }, ); // Simulate CPU fallback with fewer parallel lanes const fallback_threads: u32 = 16; const cpu = simulateCpuFallback(problem_size, fallback_threads); std.debug.print( "cpu chunks: {d} batches × {d} lanes => {d} logical tasks (tail {d})\n", .{ cpu.group_count, cpu.workgroup_size, cpu.padded_invocations, cpu.tail }, ); } ``` Run: ```shell $ zig run 03_dispatch_planner.zig ``` Output: ```shell gpu dispatch: 16 groups × 64 lanes => 1024 invocations (tail 24) cpu chunks: 63 batches × 16 lanes => 1008 logical tasks (tail 8) ``` TIP: Feed the planner’s output back into both kernel launch descriptors and CPU task schedulers so instrumentation stays consistent across platforms. ### Subsection: CPU fallbacks and unified code paths [section_id: cpu-fallbacks] [section_url: https://zigbook.net/chapters/34__gpu-fundamentals#cpu-fallbacks] Modern applications often ship CPU implementations for capability-limited machines; by sharing dispatch planners and `extern` payloads, you can reuse validation code that checks GPU outputs against CPU recomputations before trusting results in production. Pair this with Zig’s build options (`-Dgpu=false`) to conditionally exclude kernel modules when packaging for environments without accelerators. ## Section: Notes & Caveats [section_id: notes-caveats] [section_url: https://zigbook.net/chapters/34__gpu-fundamentals#notes-caveats] - Always gate GPU-specific code behind feature checks so CPU-only builds still pass CI. - Vulkan validation layers catch many mistakes early; enable them whenever compiling SPIR-V from Zig until your kernel suite stabilizes. - Prefer release-small optimization for kernels: it minimizes instruction count, easing pressure on instruction caches and register files. ## Section: Exercises [section_id: exercises] [section_url: https://zigbook.net/chapters/34__gpu-fundamentals#exercises] - Extend the kernel to write multiple dimensions (XYZ) into the coordinate struct and verify the emitted SPIR-V with `spirv-dis`. - Add a CPU-side validator that maps the SPIR-V output buffer back into Zig and cross-checks runtimes against `simulateCpuFallback`. - Modify the build script to emit both SPIR-V and PTX variants by flipping the `-target` triple and swapping address-space annotations accordingly. ## Section: Alternatives & Edge Cases [section_id: caveats-alternatives-edge-cases] [section_url: https://zigbook.net/chapters/34__gpu-fundamentals#caveats-alternatives-edge-cases] - Some GPU drivers demand specialized calling conventions (e.g., AMD’s `.amdgcn.kernel`), so parameter order and types must match vendor documentation precisely. - `@workGroupSize` returns compile-time constants only when you mark the function `inline` and supply size literals; otherwise, assume runtime values and guard dynamic paths. - OpenCL targets prefer `.param` address spaces; when cross-compiling, audit every pointer parameter and adjust `addrspace` annotations to maintain correctness. # Chapter 35 — Project [chapter_id: 35__project-gpu-compute-in-zig] [chapter_slug: project-gpu-compute-in-zig] [chapter_number: 35] [chapter_url: https://zigbook.net/chapters/35__project-gpu-compute-in-zig] ## Section: Overview [section_id: overview] [section_url: https://zigbook.net/chapters/35__project-gpu-compute-in-zig#overview] Chapter 34 (34__gpu-fundamentals.xml) outlined the GPU execution model, address spaces, and dispatch planning; now we build an end-to-end workload that starts with Zig source and ends with a validated binary dump ready for submission to Vulkan or WebGPU queue families. Target.zig (https://github.com/ziglang/zig/tree/master/lib/std/Target.zig) The project stitches together three pieces: a SPIR-V kernel authored in pure Zig, a host-side orchestration CLI with a CPU fallback, and a diff utility for comparing captured GPU buffers against expected results. build.zig (https://github.com/ziglang/zig/tree/master/lib/std/build.zig) ## Section: Learning Goals [section_id: learning-goals] [section_url: https://zigbook.net/chapters/35__project-gpu-compute-in-zig#learning-goals] - Translate a Zig compute kernel into SPIR-V with the self-hosted backend and understand the resource layouts it expects. - Coordinate buffers, dispatch geometry, and validation paths from a host application that can run with or without GPU access. - Build lightweight diagnostics that evaluate GPU output against a deterministic CPU reference. Refs: , ## Section: Building the Compute Pipeline [section_id: pipeline-topology] [section_url: https://zigbook.net/chapters/35__project-gpu-compute-in-zig#pipeline-topology] Our workload squares elements of a vector. The host creates submission metadata and data buffers, the kernel squares each lane, and the diff tool verifies device captures. The static lane capacity mirrors the GPU storage-buffer layout, while the host enforces logical bounds so the kernel can bail out when extra threads are scheduled. builtin.zig (https://github.com/ziglang/zig/tree/master/lib/std/builtin.zig) ### Subsection: Topology and Data Flow [section_id: pipeline-topology-flow] [section_url: https://zigbook.net/chapters/35__project-gpu-compute-in-zig#pipeline-topology-flow] The dispatch is intentionally modest (1000 elements in blocks of 64 threads), so you can focus on correctness rather than occupancy tuning. The host injects random floating-point values, records a checksum for observability, and emits a binary blob that downstream tooling—or a real GPU driver—can reuse. Because storage buffers operate on raw bytes, we pair every pointer parameter with an `extern struct` facade to guarantee layout parity with descriptor tables. ## Section: Authoring the SPIR-V Kernel [section_id: authoring-kernel] [section_url: https://zigbook.net/chapters/35__project-gpu-compute-in-zig#authoring-kernel] The kernel receives three storage buffers: a submission header describing the logical length, an input vector, and an output vector. Each invocation reads one lane, squares it, and writes the result back if it falls within bounds. Defensive checks prevent stray workgroups from touching memory past the logical length, a common hazard when the host pads the dispatch to match wavefront granularity. ```zig //! SPIR-V Kernel: element-wise vector squaring //! //! This kernel expects three storage buffers: an input vector (`in_values`), an //! output vector (`out_values`), and a descriptor struct `Submission` that //! communicates the logical element count. Each invocation squares one element //! and writes the result back to `out_values`. const builtin = @import("builtin"); /// Maximum number of elements the kernel will touch. pub const lane_capacity: u32 = 1024; /// Submission header shared between the host and the kernel. /// /// The `extern` layout ensures the struct matches bindings created by Vulkan or /// WebGPU descriptor tables. const Submission = extern struct { /// Logical element count requested by the host. len: u32, _padding: u32 = 0, }; /// Storage buffer layout expected by the kernel. const VectorPayload = extern struct { values: [lane_capacity]f32, }; /// Squares each element of `in_values` and writes the result to `out_values`. /// /// The kernel is written defensively: it checks both the logical length passed /// by the host and the static `lane_capacity` to avoid out-of-bounds writes when /// dispatched with more threads than necessary. pub export fn squareVector( submission: *addrspace(.storage_buffer) const Submission, in_values: *addrspace(.storage_buffer) const VectorPayload, out_values: *addrspace(.storage_buffer) VectorPayload, ) callconv(.kernel) void { const group_index = @workGroupId(0); const group_width = @workGroupSize(0); const local_index = @workItemId(0); const linear = group_index * group_width + local_index; const logical_len = submission.len; if (linear >= logical_len or linear >= lane_capacity) return; const value = in_values.*.values[linear]; out_values.*.values[linear] = value * value; } // Guard compilation so this file is only compiled when targeting SPIR-V. comptime { switch (builtin.target.cpu.arch) { .spirv32, .spirv64 => {}, else => @compileError("squareVector must be compiled with a SPIR-V target, e.g. -target spirv32-vulkan-none"), } } ``` Run: ```shell $ zig build-obj -fno-llvm -O ReleaseSmall -target spirv32-vulkan-none \ -femit-bin=kernels/vector_square.spv \ 01_vector_square_kernel.zig ``` Output: ```shell no output (binary module generated) ``` NOTE: Delete `kernels/vector_square.spv` when you finish experimenting so repeated runs always rebuild the shader from source. fs.zig (https://github.com/ziglang/zig/tree/master/lib/std/fs.zig) ## Section: Host Orchestration and CPU Fallback [section_id: host-orchestration] [section_url: https://zigbook.net/chapters/35__project-gpu-compute-in-zig#host-orchestration] The host CLI plans the dispatch, seeds deterministic input, runs a CPU fallback, and—when requested—writes a reference dump to `out/reference.bin`. It also validates the SPIR-V header (0x07230203) so broken builds surface immediately instead of failing deep inside a graphics API. Optional hooks let you drop in a captured GPU buffer (`out/gpu_result.bin`) for post-run comparison. ```zig // Project host pipeline for the vector-square kernel. // // This program demonstrates the CPU orchestration that pairs with the // `squareVector` SPIR-V kernel. It prepares input data, plans a dispatch, // validates the compiled shader module, and runs a CPU fallback that mirrors the // GPU algorithm. When requested via `--emit-binary`, it also writes the CPU // output to `out/reference.bin` so external GPU runs can be compared bit-for-bit. const std = @import("std"); /// Must match `lane_capacity` in 01_vector_square_kernel.zig. const lane_capacity: u32 = 1024; const default_problem_len: u32 = 1000; const workgroup_size: u32 = 64; const spirv_path = "kernels/vector_square.spv"; const gpu_dump_path = "out/gpu_result.bin"; const cpu_dump_path = "out/reference.bin"; /// Encapsulates the GPU workgroup dispatch geometry, accounting for padding /// when the total workload doesn't evenly divide into workgroup boundaries. const DispatchPlan = struct { workgroup_size: u32, group_count: u32, /// Total invocations including padding to fill complete workgroups padded_invocations: u32, /// Number of unused lanes in the final workgroup tail: u32, }; /// Tracks a validated SPIR-V module alongside its filesystem path for diagnostics. const ModuleInfo = struct { path: []const u8, bytes: []u8, }; pub fn main() !void { // Initialize allocator with leak detection for development builds var gpa = std.heap.GeneralPurposeAllocator(.{}){}; defer switch (gpa.deinit()) { .ok => {}, .leak => std.log.err("general-purpose allocator detected a leak", .{}), }; const allocator = gpa.allocator(); // Parse command-line arguments for optional flags var args = try std.process.argsWithAllocator(allocator); defer args.deinit(); _ = args.next(); // skip program name var emit_binary = false; var logical_len: u32 = default_problem_len; while (args.next()) |arg| { if (std.mem.eql(u8, arg, "--emit-binary")) { emit_binary = true; } else if (std.mem.eql(u8, arg, "--length")) { const value = args.next() orelse return error.MissingLengthValue; logical_len = try std.fmt.parseInt(u32, value, 10); } else { return error.UnknownFlag; } } // Clamp user-provided length to prevent buffer overruns in the kernel if (logical_len == 0 or logical_len > lane_capacity) { std.log.warn("clamping problem length to lane capacity ({d})", .{lane_capacity}); logical_len = @min(lane_capacity, logical_len); if (logical_len == 0) logical_len = @min(lane_capacity, default_problem_len); } // Calculate how many workgroups we need to process this many elements const plan = computeDispatch(logical_len, workgroup_size); std.debug.print( "launch plan: {d} groups × {d} lanes => {d} invocations (tail {d})\n", .{ plan.group_count, plan.workgroup_size, plan.padded_invocations, plan.tail }, ); // Use deterministic PRNG for reproducible test runs across environments var prng = std.Random.DefaultPrng.init(0xBEEFFACE); const random = prng.random(); // Generate input data with a predictable pattern plus random noise var input = try allocator.alloc(f32, logical_len); defer allocator.free(input); for (input, 0..input.len) |*slot, idx| { const base: f32 = @floatFromInt(idx); slot.* = base * 0.5 + random.float(f32); } // Execute CPU reference implementation to produce expected results var cpu_output = try allocator.alloc(f32, logical_len); defer allocator.free(cpu_output); runCpuFallback(input, cpu_output); // Compute simple checksum for quick sanity verification const checksum = checksumSlice(cpu_output); std.debug.print("cpu fallback checksum: {d:.6}\n", .{checksum}); // Attempt to load and validate the compiled SPIR-V shader module const module = try loadSpirvIfPresent(allocator, spirv_path); defer if (module) |info| allocator.free(info.bytes); if (module) |info| { std.debug.print( "gpu module: {s} ({d} bytes, header ok)\n", .{ info.path, info.bytes.len }, ); } else { std.debug.print( "gpu module: missing ({s}); run kernel build command to generate it\n", .{spirv_path}, ); } // Check if a GPU execution captured output for comparison const maybe_gpu_dump = try loadBinaryIfPresent(allocator, gpu_dump_path); defer if (maybe_gpu_dump) |blob| allocator.free(blob); if (maybe_gpu_dump) |blob| { // Compare GPU results against CPU reference lane-by-lane const mismatches = compareF32Slices(cpu_output, blob); std.debug.print( "gpu capture diff: {d} mismatched lanes\n", .{mismatches}, ); } else { std.debug.print( "gpu capture diff: skipped (no {s} file found)\n", .{gpu_dump_path}, ); } // Display first few lanes for manual inspection const sample_count = @min(input.len, 6); for (input[0..sample_count], cpu_output[0..sample_count], 0..) |original, squared, idx| { std.debug.print( "lane {d:>3}: in={d:.5} out={d:.5}\n", .{ idx, original, squared }, ); } // Write reference dump if requested for external GPU validation tools if (emit_binary) { try emitCpuDump(cpu_output); std.debug.print("cpu reference written to {s}\n", .{cpu_dump_path}); } } /// Computes dispatch geometry by rounding up to complete workgroups. /// Returns the number of groups, total padded invocations, and unused tail lanes. fn computeDispatch(total_items: u32, group_size: u32) DispatchPlan { std.debug.assert(group_size > 0); // Divide ceiling to ensure all items are covered const groups = std.math.divCeil(u32, total_items, group_size) catch unreachable; const padded = groups * group_size; return .{ .workgroup_size = group_size, .group_count = groups, .padded_invocations = padded, .tail = padded - total_items, }; } /// Executes the squaring operation on the CPU, mirroring the GPU kernel logic. /// Each output element is the square of its corresponding input. fn runCpuFallback(input: []const f32, output: []f32) void { std.debug.assert(input.len == output.len); for (input, output) |value, *slot| { slot.* = value * value; } } /// Calculates a simple sum of all f32 values in double precision for observability. fn checksumSlice(values: []const f32) f64 { var total: f64 = 0.0; for (values) |value| { total += @as(f64, @floatCast(value)); } return total; } /// Attempts to read and validate a SPIR-V binary module from disk. /// Returns null if the file doesn't exist; validates the magic number (0x07230203). fn loadSpirvIfPresent(allocator: std.mem.Allocator, path: []const u8) !?ModuleInfo { var file = std.fs.cwd().openFile(path, .{}) catch |err| switch (err) { error.FileNotFound => return null, else => return err, }; defer file.close(); const bytes = try file.readToEndAlloc(allocator, 1 << 20); errdefer allocator.free(bytes); // Validate minimum size for SPIR-V header if (bytes.len < 4) return error.SpirvTooSmall; // Check little-endian magic number const magic = std.mem.readInt(u32, bytes[0..4], .little); if (magic != 0x0723_0203) return error.InvalidSpirvMagic; return ModuleInfo{ .path = path, .bytes = bytes }; } /// Loads raw binary data if the file exists; returns null for missing files. fn loadBinaryIfPresent(allocator: std.mem.Allocator, path: []const u8) !?[]u8 { var file = std.fs.cwd().openFile(path, .{}) catch |err| switch (err) { error.FileNotFound => return null, else => return err, }; defer file.close(); const bytes = try file.readToEndAlloc(allocator, 1 << 24); return bytes; } /// Compares two f32 slices for approximate equality within 1e-6 tolerance. /// Returns the count of mismatched lanes; returns expected.len if sizes differ. fn compareF32Slices(expected: []const f32, blob_bytes: []const u8) usize { // Ensure blob size aligns with f32 boundaries if (blob_bytes.len % @sizeOf(f32) != 0) return expected.len; const actual = std.mem.bytesAsSlice(f32, blob_bytes); if (actual.len != expected.len) return expected.len; var mismatches: usize = 0; for (expected, actual) |lhs, rhs| { // Use floating-point tolerance to account for minor GPU precision differences if (!std.math.approxEqAbs(f32, lhs, rhs, 1e-6)) { mismatches += 1; } } return mismatches; } /// Writes CPU-computed f32 array to disk as raw bytes for external comparison tools. fn emitCpuDump(values: []const f32) !void { // Ensure output directory exists before writing try std.fs.cwd().makePath("out"); var file = try std.fs.cwd().createFile(cpu_dump_path, .{ .truncate = true }); defer file.close(); // Convert f32 slice to raw bytes for binary serialization const bytes = std.mem.sliceAsBytes(values); try file.writeAll(bytes); } ``` math.zig (https://github.com/ziglang/zig/tree/master/lib/std/math.zig) Run: ```shell $ zig build-obj -fno-llvm -O ReleaseSmall -target spirv32-vulkan-none \ -femit-bin=kernels/vector_square.spv \ 01_vector_square_kernel.zig $ zig run 02_host_pipeline.zig -- --emit-binary ``` Output: ```shell launch plan: 16 groups × 64 lanes => 1024 invocations (tail 24) cpu fallback checksum: 83467485.758038 gpu module: kernels/vector_square.spv (5368 bytes, header ok) gpu capture diff: skipped (no out/gpu_result.bin file found) lane 0: in=0.10821 out=0.01171 lane 1: in=1.07972 out=1.16579 lane 2: in=1.03577 out=1.07281 lane 3: in=2.33225 out=5.43938 lane 4: in=2.92146 out=8.53491 lane 5: in=2.89332 out=8.37133 cpu reference written to out/reference.bin ``` TIP: Keep the generated `out/reference.bin` around if you plan to capture GPU buffers; otherwise, delete it to leave the workspace clean. ## Section: Validating Device Dumps [section_id: validation-diff] [section_url: https://zigbook.net/chapters/35__project-gpu-compute-in-zig#validation-diff] The diff tool consumes two binary dumps (expected versus captured) and reports mismatched lanes, previewing the first few discrepancies to help you spot data-dependent bugs quickly. It assumes little-endian `f32` values, matching how most host APIs expose raw mapped buffers. mem.zig (https://github.com/ziglang/zig/tree/master/lib/std/mem.zig) ```zig // Utility to compare two float32 binary dumps. // // The files are expected to be raw little-endian 32-bit float arrays. The // program prints the number of mismatched lanes (based on absolute tolerance) // and highlights the first few differences for quick diagnostics. const std = @import("std"); /// Maximum number of mismatched differences to display in diagnostic output const max_preview = 5; pub fn main() !void { // Initialize allocator with leak detection for development builds var gpa = std.heap.GeneralPurposeAllocator(.{}){}; defer switch (gpa.deinit()) { .ok => {}, .leak => std.log.warn("compare_dump leaked memory", .{}), }; const allocator = gpa.allocator(); // Parse command-line arguments expecting exactly two file paths var args = try std.process.argsWithAllocator(allocator); defer args.deinit(); _ = args.next(); // Skip program name const expected_path = args.next() orelse return usageError(); const actual_path = args.next() orelse return usageError(); if (args.next()) |_| return usageError(); // Reject extra arguments // Load both binary dumps into memory for comparison const expected_bytes = try readAll(allocator, expected_path); defer allocator.free(expected_bytes); const actual_bytes = try readAll(allocator, actual_path); defer allocator.free(actual_bytes); // Reinterpret raw bytes as f32 slices for element-wise comparison const expected = std.mem.bytesAsSlice(f32, expected_bytes); const actual = std.mem.bytesAsSlice(f32, actual_bytes); // Early exit if array lengths differ if (expected.len != actual.len) { std.debug.print( "length mismatch: expected {d} elements, actual {d} elements\n", .{ expected.len, actual.len }, ); return; } // Track total mismatches and collect first few for detailed reporting var mismatches: usize = 0; var first_few: [max_preview]?Diff = .{null} ** max_preview; // Compare each lane using floating-point tolerance to account for minor precision differences for (expected, actual, 0..) |lhs, rhs, idx| { if (!std.math.approxEqAbs(f32, lhs, rhs, 1e-6)) { // Store first N differences for diagnostic display if (mismatches < max_preview) { first_few[mismatches] = Diff{ .index = idx, .expected = lhs, .actual = rhs }; } mismatches += 1; } } // Print summary of comparison results std.debug.print("mismatched lanes: {d}\n", .{mismatches}); // Display detailed information for first few mismatches to aid debugging for (first_few) |maybe_diff| { if (maybe_diff) |diff| { std.debug.print( " lane {d}: expected={d:.6} actual={d:.6}\n", .{ diff.index, diff.expected, diff.actual }, ); } } } /// Prints usage information and returns an error when invocation is invalid fn usageError() !void { std.debug.print("usage: compare_dump \n", .{}); return error.InvalidInvocation; } /// Reads entire file contents into allocated memory with a 64 MiB size limit fn readAll(allocator: std.mem.Allocator, path: []const u8) ![]u8 { var file = try std.fs.cwd().openFile(path, .{}); defer file.close(); return try file.readToEndAlloc(allocator, 1 << 26); } /// Captures a single floating-point mismatch with its location and values const Diff = struct { index: usize, expected: f32, actual: f32, }; ``` Run: ```shell $ zig run 03_compare_dump.zig -- out/reference.bin out/reference.bin ``` Output: ```shell mismatched lanes: 0 ``` NOTE: To validate a real GPU run, save the device buffer as `out/gpu_result.bin` and rerun `03_compare_dump.zig` against that file to surface any divergence. Io.zig (https://github.com/ziglang/zig/tree/master/lib/std/Io.zig) ## Section: Notes & Caveats [section_id: notes-caveats] [section_url: https://zigbook.net/chapters/35__project-gpu-compute-in-zig#notes-caveats] - Storage buffers require explicit alignment; keep your `extern struct` definitions in lockstep with host descriptor bindings to avoid silent padding bugs. - The self-hosted SPIR-V backend rejects unsupported address spaces on CPU targets, so isolate kernel source files from host builds (no `@import` from CPU binaries). - Deterministic PRNG seeding keeps CPU and GPU executions comparable across runs and CI environments. ## Section: Exercises [section_id: exercises] [section_url: https://zigbook.net/chapters/35__project-gpu-compute-in-zig#exercises] - Extend the kernel to fuse multiplication and addition (`a * a + b`) by binding a second input buffer; update the host and diff tool accordingly. - Teach the host CLI to emit JSON metadata describing the dispatch plan, so external profilers can ingest the run configuration. json.zig (https://github.com/ziglang/zig/tree/master/lib/std/json.zig) - Capture real GPU output (via Vulkan, WebGPU, or wgpu-native) and feed the binary into `03_compare_dump.zig`, noting any tolerance adjustments required for your hardware. ## Section: Alternatives & Edge Cases [section_id: caveats-alternatives-edge-cases] [section_url: https://zigbook.net/chapters/35__project-gpu-compute-in-zig#caveats-alternatives-edge-cases] - Vendors map storage buffers differently; check for required minimum alignments (for example, 16 bytes on some drivers) before assuming `f32` arrays are densely packed. - For very large buffers, stream comparisons instead of loading entire dumps into memory to avoid allocator pressure on low-end machines. - When targeting CUDA (`nvptx64`), swap the calling convention to `.kernel` and adjust address spaces (`.global`/`.shared`) to satisfy PTX expectations. # Chapter 36 — Style & Best Practices [chapter_id: 36__style-and-best-practices] [chapter_slug: style-and-best-practices] [chapter_number: 36] [chapter_url: https://zigbook.net/chapters/36__style-and-best-practices] ## Section: Overview [section_id: overview] [section_url: https://zigbook.net/chapters/36__style-and-best-practices#overview] Finishing the GPU compute project left us with a multi-file workspace that depends on consistent naming, predictable formatting, and steadfast tests (see 35 (35__project-gpu-compute-in-zig.xml)). This chapter explains how to keep that discipline as codebases evolve. We will pair `zig fmt` conventions with documentation hygiene, surface the idiomatic error-handling patterns that Zig expects, and lean on targeted invariants to keep future refactors safe (see v0.15.2 (https://ziglang.org/download/0.15.1/release-notes.html)). ## Section: Learning Goals [section_id: learning-goals] [section_url: https://zigbook.net/chapters/36__style-and-best-practices#learning-goals] - Adopt formatting and naming conventions that communicate intent across modules. - Structure documentation and tests so they form an executable spec for your APIs. - Apply `defer`, `errdefer`, and invariant helpers to maintain resource safety and correctness in the long term. Refs: ## Section: Foundations: Consistency as a Feature [section_id: style-foundations] [section_url: https://zigbook.net/chapters/36__style-and-best-practices#style-foundations] Formatting is not a cosmetic step: the standard formatter eliminates subjective whitespace debates and highlights semantic changes in diffs. `zig fmt` received incremental improvements in 0.15.x to ensure generated code matches what the compiler expects, so projects should wire formatting into editors and CI from the outset. Combine auto-formatting with descriptive identifiers, doc comments, and scoped error sets so readers can follow the control flow without rummaging through implementation details. ### Subsection: Documenting APIs with Executable Tests [section_id: documenting-apis] [section_url: https://zigbook.net/chapters/36__style-and-best-practices#documenting-apis] The following example assembles naming, documentation, and testing into a single file. It exposes a small statistics helper, expands the error set when printing, and demonstrates how tests can double as usage examples (see fmt.zig (https://github.com/ziglang/zig/tree/master/lib/std/fmt.zig)). ```zig //! Style baseline example demonstrating naming, documentation, and tests. const std = @import("std"); /// Error set for statistical computation failures. /// Intentionally narrow to allow precise error handling by callers. pub const StatsError = error{EmptyInput}; /// Combined error set for logging operations. /// Merges statistical errors with output formatting failures. pub const LogError = StatsError || error{OutputTooSmall}; /// Calculates the arithmetic mean of the provided samples. /// /// Parameters: /// - `samples`: slice of `f64` values collected from a measurement series. /// /// Returns the mean as `f64` or `StatsError.EmptyInput` when `samples` is empty. pub fn mean(samples: []const f64) StatsError!f64 { // Guard against division by zero; return domain-specific error for empty input if (samples.len == 0) return StatsError.EmptyInput; // Accumulate the sum of all sample values var total: f64 = 0.0; for (samples) |value| { total += value; } // Convert sample count to floating-point for precise division const count = @as(f64, @floatFromInt(samples.len)); return total / count; } /// Computes the mean and prints the result using the supplied writer. /// /// Accepts any writer type that conforms to the standard writer interface, /// enabling flexible output destinations (files, buffers, sockets). pub fn logMean(writer: anytype, samples: []const f64) LogError!void { // Delegate computation to mean(); propagate any statistical errors const value = try mean(samples); // Attempt to format and write result; catch writer-specific failures writer.print("mean = {d:.3}\n", .{value}) catch { // Translate opaque writer errors into our domain-specific error set return error.OutputTooSmall; }; } /// Helper for comparing floating-point values with tolerance. /// Wraps std.math.approxEqAbs to work seamlessly with test error handling. fn assertApproxEqual(expected: f64, actual: f64, tolerance: f64) !void { try std.testing.expect(std.math.approxEqAbs(f64, expected, actual, tolerance)); } test "mean handles positive numbers" { // Verify mean of [2.0, 3.0, 4.0] equals 3.0 within floating-point tolerance try assertApproxEqual(3.0, try mean(&[_]f64{ 2.0, 3.0, 4.0 }), 0.001); } test "mean returns error on empty input" { // Confirm that an empty slice triggers the expected domain error try std.testing.expectError(StatsError.EmptyInput, mean(&[_]f64{})); } test "logMean forwards formatted output" { // Allocate a fixed buffer to capture written output var storage: [128]u8 = undefined; var stream = std.io.fixedBufferStream(&storage); // Write mean result to the in-memory buffer try logMean(stream.writer(), &[_]f64{ 1.0, 2.0, 3.0 }); // Retrieve what was written and verify it contains the expected label const rendered = stream.getWritten(); try std.testing.expect(std.mem.containsAtLeast(u8, rendered, 1, "mean")); } ``` Run: ```shell $ zig test 01_style_baseline.zig ``` Output: ```shell All 3 tests passed. ``` TIP: Treat documentation comments plus unit tests as the minimum viable API reference—both are compiled on every run, so they stay in sync with the code you ship. ## Section: Resource Management & Error Patterns [section_id: resource-patterns] [section_url: https://zigbook.net/chapters/36__style-and-best-practices#resource-patterns] Zig’s standard library favors explicit resource ownership; pairing `defer` with `errdefer` helps ensure that temporary allocations unwind correctly. When parsing user-supplied data, keep the error vocabulary small and deterministic so callers can route failure modes without inspecting strings. See fs.zig (https://github.com/ziglang/zig/tree/master/lib/std/fs.zig). ```zig //! Resource-safe error handling patterns with defer and errdefer. const std = @import("std"); /// Custom error set for data loading operations. /// Keeping error sets small and explicit helps callers route failures precisely. pub const LoaderError = error{InvalidNumber}; /// Loads floating-point samples from a UTF-8 text file. /// Each non-empty line is parsed as an f64. /// Caller owns the returned slice and must free it with the same allocator. pub fn loadSamples(dir: std.fs.Dir, allocator: std.mem.Allocator, path: []const u8) ![]f64 { // Open the file; propagate any I/O errors to caller var file = try dir.openFile(path, .{}); // Guarantee file handle is released when function exits, regardless of path taken defer file.close(); // Start with an empty list; we'll grow it as we parse lines var list = std.ArrayListUnmanaged(f64){}; // If any error occurs after this point, free the list's backing memory errdefer list.deinit(allocator); // Read entire file into memory; cap at 64KB for safety const contents = try file.readToEndAlloc(allocator, 1 << 16); // Free the temporary buffer once we've parsed it defer allocator.free(contents); // Split contents by newline; iterator yields one line at a time var lines = std.mem.splitScalar(u8, contents, '\n'); while (lines.next()) |line| { // Strip leading/trailing whitespace and carriage returns const trimmed = std.mem.trim(u8, line, " \t\r"); // Skip empty lines entirely if (trimmed.len == 0) continue; // Attempt to parse the line as a float; surface a domain-specific error on failure const value = std.fmt.parseFloat(f64, trimmed) catch return LoaderError.InvalidNumber; // Append successfully parsed value to the list try list.append(allocator, value); } // Transfer ownership of the backing array to the caller return list.toOwnedSlice(allocator); } test "loadSamples returns parsed floats" { // Create a temporary directory that will be cleaned up automatically var tmp_fs = std.testing.tmpDir(.{}); defer tmp_fs.cleanup(); // Write sample data to a test file const file_path = try tmp_fs.dir.createFile("samples.txt", .{}); defer file_path.close(); try file_path.writeAll("1.0\n2.5\n3.75\n"); // Load and parse the samples; defer ensures cleanup even if assertions fail const samples = try loadSamples(tmp_fs.dir, std.testing.allocator, "samples.txt"); defer std.testing.allocator.free(samples); // Verify we parsed exactly three values try std.testing.expectEqual(@as(usize, 3), samples.len); // Check each value is within acceptable floating-point tolerance try std.testing.expectApproxEqAbs(1.0, samples[0], 0.001); try std.testing.expectApproxEqAbs(2.5, samples[1], 0.001); try std.testing.expectApproxEqAbs(3.75, samples[2], 0.001); } test "loadSamples surfaces invalid numbers" { // Set up another temporary directory for error-path testing var tmp_fs = std.testing.tmpDir(.{}); defer tmp_fs.cleanup(); // Write non-numeric content to trigger parsing failure const file_path = try tmp_fs.dir.createFile("bad.txt", .{}); defer file_path.close(); try file_path.writeAll("not-a-number\n"); // Confirm that loadSamples returns the expected domain error try std.testing.expectError(LoaderError.InvalidNumber, loadSamples(tmp_fs.dir, std.testing.allocator, "bad.txt")); } ``` Run: ```shell $ zig test 02_error_handling_patterns.zig ``` Output: ```shell All 2 tests passed. ``` NOTE: Returning slices via `toOwnedSlice` keeps the lifetimes obvious and prevents leaking the backing allocation when parsing fails midway—`errdefer` makes the cleanup explicit (see mem.zig (https://github.com/ziglang/zig/tree/master/lib/std/mem.zig)). ## Section: Maintainability Checklist: Guarding Invariants [section_id: maintainability] [section_url: https://zigbook.net/chapters/36__style-and-best-practices#maintainability] Data structures that defend their own invariants are easier to refactor safely. By isolating the checks in a helper and calling it before and after mutations, you create a single source of truth for correctness. `std.debug.assert` makes the contract visible in debug builds without penalizing release performance (see debug.zig (https://github.com/ziglang/zig/tree/master/lib/std/debug.zig)). ```zig //! Maintainability checklist example with an internal invariant helper. //! //! This module demonstrates defensive programming practices by implementing //! a ring buffer data structure that validates its internal state invariants //! before and after mutating operations. const std = @import("std"); /// A fixed-capacity circular buffer that stores i32 values. /// The buffer wraps around when full, and uses modular arithmetic /// to implement FIFO (First-In-First-Out) semantics. pub const RingBuffer = struct { storage: []i32, head: usize = 0, // Index of the first element count: usize = 0, // Number of elements currently stored /// Errors that can occur during ring buffer operations. pub const Error = error{Overflow}; /// Creates a new RingBuffer backed by the provided storage slice. /// The caller retains ownership of the storage memory. pub fn init(storage: []i32) RingBuffer { return .{ .storage = storage }; } /// Validates internal state consistency. /// This is called before and after mutations to catch logic errors early. /// Checks that: /// - Empty storage implies zero head and count /// - Head index is within storage bounds /// - Count doesn't exceed storage capacity fn invariant(self: *const RingBuffer) void { if (self.storage.len == 0) { std.debug.assert(self.head == 0); std.debug.assert(self.count == 0); return; } std.debug.assert(self.head < self.storage.len); std.debug.assert(self.count <= self.storage.len); } /// Adds a value to the end of the buffer. /// Returns Error.Overflow if the buffer is at capacity or has no storage. /// Invariants are checked before and after the operation. pub fn push(self: *RingBuffer, value: i32) Error!void { self.invariant(); if (self.storage.len == 0 or self.count == self.storage.len) return Error.Overflow; // Calculate the insertion position using circular indexing const index = (self.head + self.count) % self.storage.len; self.storage[index] = value; self.count += 1; self.invariant(); } /// Removes and returns the oldest value from the buffer. /// Returns null if the buffer is empty. /// Advances the head pointer circularly and decrements the count. pub fn pop(self: *RingBuffer) ?i32 { self.invariant(); if (self.count == 0) return null; const value = self.storage[self.head]; // Move head forward circularly self.head = (self.head + 1) % self.storage.len; self.count -= 1; self.invariant(); return value; } }; // Verifies that the buffer correctly rejects pushes when at capacity. test "ring buffer enforces capacity" { var storage = [_]i32{ 0, 0, 0 }; var buffer = RingBuffer.init(&storage); try buffer.push(1); try buffer.push(2); try buffer.push(3); // Fourth push should fail because buffer capacity is 3 try std.testing.expectError(RingBuffer.Error.Overflow, buffer.push(4)); } // Verifies that values are retrieved in the same order they were inserted. test "ring buffer preserves FIFO order" { var storage = [_]i32{ 0, 0, 0 }; var buffer = RingBuffer.init(&storage); try buffer.push(10); try buffer.push(20); try buffer.push(30); // Values should come out in insertion order try std.testing.expectEqual(@as(?i32, 10), buffer.pop()); try std.testing.expectEqual(@as(?i32, 20), buffer.pop()); try std.testing.expectEqual(@as(?i32, 30), buffer.pop()); // Buffer is now empty, should return null try std.testing.expectEqual(@as(?i32, null), buffer.pop()); } ``` Run: ```shell $ zig test 03_invariant_guard.zig ``` Output: ```shell All 2 tests passed. ``` TIP: Capture invariants in unit tests as well—assertions guard developers, while tests stop regressions that slip past manual review. ## Section: Notes & Caveats [section_id: notes-caveats] [section_url: https://zigbook.net/chapters/36__style-and-best-practices#notes-caveats] - `zig fmt` only touches syntax it understands; generated code or embedded strings may still need a manual glance. - Expand error sets deliberately—combining the smallest possible unions keeps call sites precise and avoids accidental catch-alls (see error.zig (https://github.com/ziglang/zig/tree/master/lib/std/error.zig)). - Remember to test under both debug and release builds so assertions and `std.debug` checks do not mask production-only issues (see build.zig (https://github.com/ziglang/zig/tree/master/lib/std/build.zig)). ## Section: Exercises [section_id: exercises] [section_url: https://zigbook.net/chapters/36__style-and-best-practices#exercises] - Wrap the statistics helper in a module that exposes both mean and variance; add doctests that demonstrate the API from a consumer’s perspective. - Extend the loader to stream data instead of reading entire files; compare heap usage in release-safe builds to ensure you keep allocations bounded. - Add a stress test to the ring buffer that interleaves pushes and pops across thousands of operations, then run it under `zig test -Drelease-safe` to confirm invariants survive in optimized builds. ## Section: Alternatives & Edge Cases [section_id: caveats-alternatives-edge-cases] [section_url: https://zigbook.net/chapters/36__style-and-best-practices#caveats-alternatives-edge-cases] - Projects with generated code may need formatting exclusions—document those directories so contributors know when `zig fmt` is safe to run. - Favor small helper functions (like `invariant`) over sprinkling assertions everywhere; centralized checks are easier to audit during reviews. - When adding new dependencies, gate them behind feature flags or build options so style rules remain enforceable even in minimal configurations. # Chapter 37 — Illegal Behavior & Safety Modes [chapter_id: 37__illegal-behavior-and-safety-modes] [chapter_slug: illegal-behavior-and-safety-modes] [chapter_number: 37] [chapter_url: https://zigbook.net/chapters/37__illegal-behavior-and-safety-modes] ## Section: Overview [section_id: overview] [section_url: https://zigbook.net/chapters/37__illegal-behavior-and-safety-modes#overview] Finishing our style tune-up made it clear that invariants are worthless unless they fail loudly (36 (36__style-and-best-practices.xml)). This chapter explains how Zig formalizes those failures as illegal behavior and how the toolchain catches most of them before they corrupt state. #illegal behavior (https://ziglang.org/documentation/master/#illegal-behavior) Next we will dive into command-line tooling, so we want our runtime guardrails in place before scripting toggles optimization modes on our behalf. 38 (38__zig-cli-deep-dive.xml) ## Section: Learning Goals [section_id: learning-goals] [section_url: https://zigbook.net/chapters/37__illegal-behavior-and-safety-modes#learning-goals] - Distinguish between safety-checked and unchecked categories of illegal behavior. - Inspect the active optimization mode and reason about which runtime checks Zig will emit. - Build contracts around `@setRuntimeSafety`, `unreachable`, and `std.debug.assert` to keep invariants provable in every build. Refs: ## Section: Illegal Behavior in Zig [section_id: illegal-behavior-basics] [section_url: https://zigbook.net/chapters/37__illegal-behavior-and-safety-modes#illegal-behavior-basics] Illegal behavior is Zig’s umbrella term for operations the language refuses to define, ranging from integer overflow to dereferencing an invalid pointer. We have already relied on bounds checks for slices and optionals; this section consolidates those rules so the upcoming CLI work inherits a predictable failure story. 3 (03__data-fundamentals.xml) ### Subsection: Safety-Checked vs Unchecked Paths [section_id: illegal-behavior-checked-unchecked] [section_url: https://zigbook.net/chapters/37__illegal-behavior-and-safety-modes#illegal-behavior-checked-unchecked] Safety-checked illegal behavior covers the cases the compiler can instrument at runtime (overflow, sentinel mismatches, wrong-union-field access), while unchecked cases remain invisible to safety instrumentation (aliasing through the wrong pointer type, layout violations from foreign code). Debug and ReleaseSafe keep the guards on by default. ReleaseFast and ReleaseSmall assume you traded those traps for performance, so anything that slips past your invariants becomes undefined in practice. ### Subsection: Example: Guarding Unchecked Arithmetic [section_id: illegal-behavior-guarded-example] [section_url: https://zigbook.net/chapters/37__illegal-behavior-and-safety-modes#illegal-behavior-guarded-example] The following helper proves an addition safe with `@addWithOverflow`, then disables runtime safety for the final `+` to avoid redundant checks while saturating pathological inputs to the type’s maximum. #setruntimesafety (https://ziglang.org/documentation/master/#setruntimesafety) ```zig const std = @import("std"); /// Performs addition with overflow detection and saturation. /// If overflow occurs, returns the maximum u8 value instead of wrapping. /// Uses @setRuntimeSafety(false) in the non-overflow path for performance. fn guardedUncheckedAdd(a: u8, b: u8) u8 { // Check if addition would overflow using builtin overflow detection const sum = @addWithOverflow(a, b); const overflow = sum[1] == 1; // Saturate to max value on overflow if (overflow) return std.math.maxInt(u8); // Safe path: disable runtime safety checks for this addition // since we've already verified no overflow will occur return blk: { @setRuntimeSafety(false); break :blk a + b; }; } /// Performs addition without runtime safety checks. /// This allows the operation to wrap on overflow (undefined behavior in safe mode). /// Demonstrates completely disabling safety for a function scope. fn wrappingAddUnsafe(a: u8, b: u8) u8 { // Disable all runtime safety checks for this entire function @setRuntimeSafety(false); return a + b; } // Verifies that guardedUncheckedAdd correctly handles both normal addition // and overflow saturation scenarios. test "guarded unchecked addition saturates on overflow" { // Normal case: 120 + 80 = 200 (no overflow) try std.testing.expectEqual(@as(u8, 200), guardedUncheckedAdd(120, 80)); // Overflow case: 240 + 30 = 270 > 255, should saturate to 255 try std.testing.expectEqual(std.math.maxInt(u8), guardedUncheckedAdd(240, 30)); } // Demonstrates that wrappingAddUnsafe produces the same wrapped result // as @addWithOverflow when overflow occurs. test "wrapping addition mirrors overflow tuple" { // @addWithOverflow returns [wrapped_result, overflow_bit] const sum = @addWithOverflow(@as(u8, 250), @as(u8, 10)); // Verify overflow occurred (250 + 10 = 260 > 255) try std.testing.expect(sum[1] == 1); // Verify wrapped result matches unchecked addition (260 % 256 = 4) try std.testing.expectEqual(sum[0], wrappingAddUnsafe(250, 10)); } ``` Run: ```shell $ zig test 01_guarded_runtime_safety.zig ``` Output: ```shell All 2 tests passed. ``` NOTE: Running the same test with `-OReleaseFast` verifies that the guard continues to saturate rather than panic even when global runtime safety is absent. ## Section: Safety Defaults by Optimization Mode [section_id: safety-modes] [section_url: https://zigbook.net/chapters/37__illegal-behavior-and-safety-modes#safety-modes] The current optimization mode is exposed as `@import("builtin").mode`, making it easy to surface which runtime checks will exist in a given artifact without consulting build scripts. #compile variables (https://ziglang.org/documentation/master/#compile-variables) The table below summarizes the default contract each mode offers before you start opting in or out of checks manually. | Mode | Runtime safety | Typical intent | | --- | --- | --- | | Debug | Enabled | Development builds with maximum diagnostics and stack traces. | | ReleaseSafe | Enabled | Production builds that still prefer predictable traps over silent corruption. | | ReleaseFast | Disabled | High-performance binaries that assume invariants are already proven elsewhere. | | ReleaseSmall | Disabled | Size-constrained deliverables where every injected trap would be a liability. | ### Subsection: Instrumenting Safety at Runtime [section_id: safety-modes-probe] [section_url: https://zigbook.net/chapters/37__illegal-behavior-and-safety-modes#safety-modes-probe] This probe prints the active mode and the implied safety default, then compares a checked addition with an unchecked one so you can see what survives when checks vanish. ```zig const std = @import("std"); const builtin = @import("builtin"); // Extract the compile-time type of the optimization mode enum const ModeType = @TypeOf(builtin.mode); /// Captures both the active optimization mode and its default safety behavior const ModeInfo = struct { mode: ModeType, safety_default: bool, }; /// Determines whether runtime safety checks are enabled by default for a given mode. /// Debug and ReleaseSafe modes retain safety checks; ReleaseFast and ReleaseSmall disable them. fn defaultSafety(mode: ModeType) bool { return switch (mode) { // These modes prioritize correctness with runtime checks .Debug, .ReleaseSafe => true, // These modes prioritize performance/size by removing checks .ReleaseFast, .ReleaseSmall => false, }; } /// Performs checked addition that detects overflow without panicking. /// Returns both the wrapped result and an overflow flag. fn sampleAdd(a: u8, b: u8) struct { result: u8, overflowed: bool } { // @addWithOverflow returns a tuple: [wrapped_result, overflow_bit] const pair = @addWithOverflow(a, b); return .{ .result = pair[0], .overflowed = pair[1] == 1 }; } /// Performs unchecked addition by explicitly disabling runtime safety. /// In Debug/ReleaseSafe, this avoids the panic on overflow. /// In ReleaseFast/ReleaseSmall, the safety was already off, so this is redundant but harmless. fn uncheckedAddStable(a: u8, b: u8) u8 { return blk: { // Temporarily disable runtime safety for this block only @setRuntimeSafety(false); // Raw addition without overflow checks; wraps silently on overflow break :blk a + b; }; } pub fn main() void { // Capture the current build mode and its implied safety setting const info = ModeInfo{ .mode = builtin.mode, .safety_default = defaultSafety(builtin.mode), }; // Report which optimization mode the binary was compiled with std.debug.print("optimize-mode: {s}\n", .{@tagName(info.mode)}); // Show whether runtime safety is on by default in this mode std.debug.print("runtime-safety-default: {}\n", .{info.safety_default}); // Demonstrate checked addition that reports overflow without crashing const checked = sampleAdd(200, 80); std.debug.print("checked-add result={d} overflowed={}\n", .{ checked.result, checked.overflowed }); // Demonstrate unchecked addition that wraps silently (24 = (200+80) % 256) const unchecked = uncheckedAddStable(200, 80); std.debug.print("unchecked-add result={d}\n", .{unchecked}); } ``` Run: ```shell $ zig run 02_mode_probe.zig ``` Output: ```shell optimize-mode: Debug runtime-safety-default: true checked-add result=24 overflowed=true unchecked-add result=24 ``` TIP: Re-run the probe with `-OReleaseFast` to watch the default safety flag flip to `false` while the checked path still reports the overflow, helping you document feature flags or telemetry you might need in release builds. ## Section: Contracts, Panics, and Recovery [section_id: contracts-and-panics] [section_url: https://zigbook.net/chapters/37__illegal-behavior-and-safety-modes#contracts-and-panics] Stack traces are calmly terrifying when you trigger `unreachable` in a safety-enabled build. Treat them as the last line of defense after assertions and error unions have exhausted graceful exits. #reaching unreachable code (https://ziglang.org/documentation/master/#reaching-unreachable-code) Pairing that discipline with the error-handling techniques from earlier chapters keeps failure modes debuggable without sacrificing determinism. 4 (04__errors-resource-cleanup.xml) ### Subsection: Example: Asserting Digit Conversion [section_id: contracts-example] [section_url: https://zigbook.net/chapters/37__illegal-behavior-and-safety-modes#contracts-example] Here we document an ASCII digit contract twice: once with an assertion that unlocks unchecked math and once with an error union for caller-friendly validation. debug.zig (https://github.com/ziglang/zig/tree/master/lib/std/debug.zig) ```zig // This file demonstrates different safety modes in Zig and how to handle // conversions with varying levels of runtime checking. const std = @import("std"); /// Converts an ASCII digit character to its numeric value without runtime safety checks. /// This function uses an assert to document the precondition that the input must be /// a valid ASCII digit ('0'-'9'). The @setRuntimeSafety(false) directive disables /// runtime integer overflow checks for the subtraction and cast operations. /// /// Precondition: byte must be in the range ['0', '9'] /// Returns: The numeric value (0-9) as a u4 pub fn asciiDigitToValueUnchecked(byte: u8) u4 { // Assert documents the contract: caller must provide a valid ASCII digit std.debug.assert(byte >= '0' and byte <= '9'); // Block with runtime safety disabled for performance-critical paths return blk: { // Disable runtime overflow/underflow checks for this conversion @setRuntimeSafety(false); // Safe cast because precondition guarantees result fits in u4 (0-9) break :blk @intCast(byte - '0'); }; } /// Converts an ASCII digit character to its numeric value with error handling. /// This function validates the input at runtime and returns an error if the /// byte is not a valid ASCII digit, making it safe to use with untrusted input. /// /// Returns: The numeric value (0-9) as a u4, or error.InvalidDigit if invalid pub fn asciiDigitToValue(byte: u8) !u4 { // Validate input is within valid ASCII digit range if (byte < '0' or byte > '9') return error.InvalidDigit; // Safe cast: validation ensures result is in range 0-9 return @intCast(byte - '0'); } // Verifies that the unchecked conversion produces correct results for all valid inputs. // Tests all ASCII digits to ensure the assert-backed function maintains correctness // even when runtime safety is disabled internally. test "assert-backed conversion stays safe across modes" { // Iterate over all valid ASCII digit characters at compile time inline for ("0123456789") |ch| { // Verify unchecked function produces same result as direct conversion try std.testing.expectEqual(@as(u4, @intCast(ch - '0')), asciiDigitToValueUnchecked(ch)); } } // Verifies that the error-returning conversion properly rejects invalid input. // Ensures that error handling path works correctly and provides meaningful diagnostics. test "error path preserves diagnosability" { // Verify that non-digit characters return the expected error try std.testing.expectError(error.InvalidDigit, asciiDigitToValue('z')); } ``` Run: ```shell $ zig test 03_unreachable_contract.zig ``` Output: ```shell All 2 tests passed. ``` IMPORTANT: The assertion-backed path compiles to a single subtraction in ReleaseFast, but it still panics in Debug if you pass a non-digit. Keep a defensive error-returning variant around for untrusted data. ## Section: Notes & Caveats [section_id: notes-caveats] [section_url: https://zigbook.net/chapters/37__illegal-behavior-and-safety-modes#notes-caveats] - Even in Debug mode, some pointer-based mistakes stay unchecked. Prefer slice-based APIs when you need bounds enforcement. - Narrow the scope of `@setRuntimeSafety(false)` to the smallest possible block and prove the preconditions before toggling it. - Capture panic stack traces in development and ship symbol files if you expect to triage ReleaseSafe crashes later. ## Section: Exercises [section_id: exercises] [section_url: https://zigbook.net/chapters/37__illegal-behavior-and-safety-modes#exercises] - Extend `guardedUncheckedAdd` to emit diagnostics when a sentinel-terminated slice would overflow the destination buffer, then measure the difference between safety-on and safety-off builds. #sentinel terminated arrays (https://ziglang.org/documentation/master/#sentinel-terminated-arrays) - Write a benchmarking harness that loops through millions of safe additions, toggling `@setRuntimeSafety` per iteration to confirm the cost of the guard in each mode. - Enhance the mode probe to log build metadata in your upcoming CLI project so scripts can warn when ReleaseFast binaries omit traps. 38 (38__zig-cli-deep-dive.xml) ## Section: Alternatives & Edge Cases [section_id: caveats-alternatives-edge-cases] [section_url: https://zigbook.net/chapters/37__illegal-behavior-and-safety-modes#caveats-alternatives-edge-cases] - Failing to switch from `+` to `@addWithOverflow` in ReleaseFast risks silent wraparound that only manifests under rare load patterns. - Runtime safety does not defend against concurrent data races. Pair these tools with the synchronization primitives introduced later in the book. - When calling into C code, remember that Zig’s checks stop at the FFI boundary. Validate foreign inputs before trusting invariants. 33 (33__c-interop-import-export-abi.xml) # Chapter 38 — Zig CLI Deep Dive [chapter_id: 38__zig-cli-deep-dive] [chapter_slug: zig-cli-deep-dive] [chapter_number: 38] [chapter_url: https://zigbook.net/chapters/38__zig-cli-deep-dive] ## Section: Overview [section_id: overview] [section_url: https://zigbook.net/chapters/38__zig-cli-deep-dive#overview] We spent the previous chapter hardening invariants and fail-fast strategies (see 37 (37__illegal-behavior-and-safety-modes.xml)); now we turn that discipline toward the tooling that drives every Zig project. The `zig` command-line interface (CLI) is more than a compiler wrapper: it dispatches to build graph runners, drop-in toolchain shims, formatting pipelines, and metadata exporters that keep your codebase reproducible. See #entry points and command structure (ZIG_DEEP_WIKI.md#entry-points-and-command-structure). The insights you gather here will feed directly into the upcoming performance tuning discussion, where CLI flags like `-OReleaseFast` and `--time-report` become essential measurement levers (see 39 (39__performance-and-inlining.xml)). ## Section: Learning Goals [section_id: learning-goals] [section_url: https://zigbook.net/chapters/38__zig-cli-deep-dive#learning-goals] - Map the major command families exposed by the `zig` CLI and know when to reach for each. - Drive compilation, testing, and sanitizers from the CLI while keeping outputs reproducible across targets. - Combine diagnostic commands—`fmt`, `ast-check`, `env`, `targets`—into daily workflows that surface correctness issues early. Refs: ## Section: Command Map of the Tool [section_id: cli-topology] [section_url: https://zigbook.net/chapters/38__zig-cli-deep-dive#cli-topology] Zig ships a single binary whose first positional argument selects the subsystem to execute. Understanding that dispatch table is the fastest route to mastering the CLI. ```markdown zig --help Usage: zig [command] [options] Commands: build Build project from build.zig fetch Copy a package into global cache and print its hash init Initialize a Zig package in the current directory build-exe Create executable from source or object files build-lib Create library from source or object files build-obj Create object from source or object files test Perform unit testing test-obj Create object for unit testing run Create executable and run immediately ast-check Look for simple compile errors in any set of files fmt Reformat Zig source into canonical form reduce Minimize a bug report translate-c Convert C code to Zig code ar Use Zig as a drop-in archiver cc Use Zig as a drop-in C compiler c++ Use Zig as a drop-in C++ compiler dlltool Use Zig as a drop-in dlltool.exe lib Use Zig as a drop-in lib.exe ranlib Use Zig as a drop-in ranlib objcopy Use Zig as a drop-in objcopy rc Use Zig as a drop-in rc.exe env Print lib path, std path, cache directory, and version help Print this help and exit std View standard library documentation in a browser libc Display native libc paths file or validate one targets List available compilation targets version Print version number and exit zen Print Zen of Zig and exit General Options: -h, --help Print command-specific usage ``` ### Subsection: Build and Execution Commands [section_id: cli-build-execute] [section_url: https://zigbook.net/chapters/38__zig-cli-deep-dive#cli-build-execute] Compilation-centric commands (`build-exe`, `build-lib`, `build-obj`, `run`, `test`, `test-obj`) all flow through the same build output machinery, offering consistent options for targets, optimization, sanitizers, and emission controls. `zig test-obj` (new in 0.15.2) now emits object files for embed-your-own test runners when you need to integrate with foreign harnesses (see #compile tests to object file (https://ziglang.org/download/0.15.1/release-notes.html#compile-tests-to-object-file)). ### Subsection: Toolchain Drop-in Modes [section_id: cli-toolchain-dropin] [section_url: https://zigbook.net/chapters/38__zig-cli-deep-dive#cli-toolchain-dropin] `zig cc`, `zig c++`, `zig ar`, `zig dlltool`, and friends let you replace Clang/LLVM utilities with Zig-managed shims, keeping cross-compilation assets, libc headers, and target triples consistent without juggling SDK installs. These commands honor the same cache directories you see in `zig env`, so the artifacts they produce land beside your native Zig outputs. ### Subsection: Package Bootstrapping Commands [section_id: cli-package-init] [section_url: https://zigbook.net/chapters/38__zig-cli-deep-dive#cli-package-init] `zig init` and `zig fetch` handle project scaffolding and dependency pinning. Version 0.15.2 introduces `zig init --minimal`, which generates just a `build.zig` stub plus a valid `build.zig.zon` fingerprint for teams that already know how they want the build graph structured (see #zig init (https://ziglang.org/download/0.15.1/release-notes.html#zig-init)). Combined with `zig fetch`, you can warm the global cache before CI kicks off, avoiding first-run latency when `zig build` pulls modules from the package manager. ## Section: Driving Compilation from the CLI [section_id: cli-driving-compilation] [section_url: https://zigbook.net/chapters/38__zig-cli-deep-dive#cli-driving-compilation] Once you know which command to call, the art lies in selecting the right flags and reading the metadata they surface. Zig’s CLI mirrors the language’s explicitness: every safety toggle and artifact knob is rendered as a flag, and the `@import("builtin")` namespace reflects back what the build saw. ### Subsection: Inspecting Build Context with [section_id: cli-run-summary] [section_url: https://zigbook.net/chapters/38__zig-cli-deep-dive#cli-run-summary] The `zig run` wrapper accepts all compilation flags plus a `--` separator that forwards the remaining arguments to your program. This makes it ideal for quick experiments that still need deterministic target and optimization settings. ```zig const std = @import("std"); const builtin = @import("builtin"); pub fn main() !void { // Set up a general-purpose allocator for dynamic memory allocation var gpa = std.heap.GeneralPurposeAllocator(.{}){}; defer _ = gpa.deinit(); const allocator = gpa.allocator(); // Retrieve all command-line arguments passed to the program const argv = try std.process.argsAlloc(allocator); defer std.process.argsFree(allocator, argv); // Display the optimization mode used during compilation (Debug, ReleaseSafe, ReleaseFast, ReleaseSmall) std.debug.print("optimize-mode: {s}\n", .{@tagName(builtin.mode)}); // Display the target platform triple (architecture-os-abi) std.debug.print( "target-triple: {s}-{s}-{s}\n", .{ @tagName(builtin.target.cpu.arch), @tagName(builtin.target.os.tag), @tagName(builtin.target.abi), }, ); // Display whether the program was compiled in single-threaded mode std.debug.print("single-threaded: {}\n", .{builtin.single_threaded}); // Check if any user arguments were provided (argv[0] is the program name itself) if (argv.len <= 1) { std.debug.print("user-args: \n", .{}); return; } // Print all user-provided arguments (skipping the program name at argv[0]) std.debug.print("user-args:\n", .{}); for (argv[1..], 0..) |arg, idx| { std.debug.print(" arg[{d}] = {s}\n", .{ idx, arg }); } } ``` Run: ```shell $ zig run 01_cli_modes.zig -OReleaseFast -- --name zig --count 2 ``` Output: ```shell optimize-mode: ReleaseFast target-triple: x86_64-linux-gnu single-threaded: false user-args: arg[0] = --name arg[1] = zig arg[2] = --count arg[3] = 2 ``` TIP: Pair `zig run` with `-fsanitize-c=trap` or `-fsanitize-c=full` to toggle UBSan-style diagnostics without touching source. These flags mirror the new module-level sanitizer controls added in 0.15.2 (see #allow configuring ubsan mode at the module level (https://ziglang.org/download/0.15.1/release-notes.html#allow-configuring-ubsan-mode-at-the-module-level)). ### Subsection: Filtering Test Suites on Demand [section_id: cli-test-filter] [section_url: https://zigbook.net/chapters/38__zig-cli-deep-dive#cli-test-filter] `zig test` accepts `--test-filter` to restrict which test names are compiled and executed, enabling tight edit-run loops even in monolithic suites. Combine it with `--summary all` or `--summary failing` when you need deterministic reporting in CI pipelines. ```zig const std = @import("std"); /// Calculates the sum of all integers in the provided slice. /// Returns 0 for an empty slice. fn sum(values: []const i32) i32 { var total: i32 = 0; // Accumulate all values in the slice for (values) |value| total += value; return total; } /// Calculates the product of all integers in the provided slice. /// Returns 1 for an empty slice (multiplicative identity). fn product(values: []const i32) i32 { var total: i32 = 1; // Multiply each value with the running total for (values) |value| total *= value; return total; } // Verifies that sum correctly adds positive integers test "sum-of-three" { try std.testing.expectEqual(@as(i32, 42), sum(&.{ 20, 10, 12 })); } // Verifies that sum handles mixed positive and negative integers correctly test "sum-mixed-signs" { try std.testing.expectEqual(@as(i32, -1), sum(&.{ 4, -3, -2 })); } // Verifies that product correctly multiplies positive integers test "product-positive" { try std.testing.expectEqual(@as(i32, 120), product(&.{ 2, 3, 4, 5 })); } // Verifies that product correctly handles negative integers, // resulting in a negative product when an odd number of negatives are present test "product-negative" { try std.testing.expectEqual(@as(i32, -18), product(&.{ 3, -3, 2 })); } ``` Run: ```shell $ zig test 02_cli_tests.zig --test-filter sum ``` Output: ```shell All 2 tests passed. ``` NOTE: When your build graph emits `zig test-obj`, reuse the same filters. The command `zig build test-obj --test-filter sum` forwards them to the underlying runner in exactly the same way. ## Section: Long-Running Builds and Reporting [section_id: cli-long-running] [section_url: https://zigbook.net/chapters/38__zig-cli-deep-dive#cli-long-running] Large projects often keep `zig build` running continuously, so understanding its watch mode, web UI, and reporting hooks pays dividends. macOS users finally get reliable file watching in 0.15.2 thanks to a rewritten `--watch` implementation (see #macos file system watching (https://ziglang.org/download/0.15.1/release-notes.html#macos-file-system-watching)). Pair it with incremental compilation (`-fincremental`) to turn rebuilds into sub-second operations when files change. ### Subsection: Web Interface and Time Reports [section_id: cli-webui] [section_url: https://zigbook.net/chapters/38__zig-cli-deep-dive#cli-webui] `zig build --webui` spins up a local dashboard that visualizes the build graph, active steps, and, when combined with `--time-report`, a breakdown of semantic analysis and code generation hotspots (see #web interface and time report (https://ziglang.org/download/0.15.1/release-notes.html#web-interface-and-time-report)). Use it when you suspect slow compile times: the "Declarations" table highlights which files or declarations consume the most analysis time, and those insights flow directly into the optimization work covered in the next chapter (see 39 (39__performance-and-inlining.xml)). ## Section: Diagnostics and Automation Helpers [section_id: cli-diagnostics] [section_url: https://zigbook.net/chapters/38__zig-cli-deep-dive#cli-diagnostics] Beyond compiling programs, the CLI offers tools that keep your repository tidy and introspectable: formatters, AST validators, environment reporters, and target enumerators (see #formatter zig fmt (ZIG_DEEP_WIKI.md#formatter-zig-fmt)). ### Subsection: Batch Syntax Validation with [section_id: cli-ast-check] [section_url: https://zigbook.net/chapters/38__zig-cli-deep-dive#cli-ast-check] `zig ast-check` parses files without emitting binaries, catching syntax and import issues faster than a full compile. This is handy for editor save hooks or pre-commit checks. The helper below returns cache and formatting defaults that build scripts can reuse; running `ast-check` against it ensures the file stays well-formed even if no executable ever imports it. ```zig //! Utility functions for CLI environment configuration and cross-platform defaults. //! This module provides helpers for determining cache directories, color support, //! and default tool configurations based on the target operating system. const std = @import("std"); const builtin = @import("builtin"); /// Returns the appropriate environment variable key for the cache directory /// based on the target operating system. /// /// - Windows uses LOCALAPPDATA for application cache /// - macOS and iOS use HOME (cache typically goes in ~/Library/Caches) /// - Unix-like systems prefer XDG_CACHE_HOME for XDG Base Directory compliance /// - Other systems fall back to HOME directory pub fn defaultCacheEnvKey() []const u8 { return switch (builtin.os.tag) { .windows => "LOCALAPPDATA", .macos => "HOME", .ios => "HOME", .linux, .freebsd, .netbsd, .openbsd, .dragonfly, .haiku => "XDG_CACHE_HOME", else => "HOME", }; } /// Determines whether ANSI color codes should be used in terminal output /// based on standard environment variables. /// /// Follows the informal standard where: /// - NO_COLOR (any value) disables colors /// - CLICOLOR_FORCE (any value) forces colors even if not a TTY /// - Default behavior is to enable colors /// /// Returns true if ANSI colors should be used, false otherwise. pub fn preferAnsiColor(env: std.process.EnvMap) bool { // Check if colors are explicitly disabled if (env.get("NO_COLOR")) |_| return false; // Check if colors are explicitly forced if (env.get("CLICOLOR_FORCE")) |_| return true; // Default to enabling colors return true; } /// Returns the default command-line arguments for invoking the Zig formatter /// in check mode (reports formatting issues without modifying files). pub fn defaultFormatterArgs() []const []const u8 { return &.{ "zig", "fmt", "--check" }; } ``` Run: ```shell $ zig ast-check 03_cli_astcheck.zig ``` Output: ```shell (no output) ``` TIP: Combine `zig ast-check` with `zig fmt --check --ast-check` to refuse commits that either violate style or fail to parse—the formatter already has an AST pass under the hood, so the extra flag keeps both stages in sync. ### Subsection: Introspection Commands Worth Scripting [section_id: cli-env-targets] [section_url: https://zigbook.net/chapters/38__zig-cli-deep-dive#cli-env-targets] `zig env` prints the paths, cache directories, and active target triple that the toolchain resolved, making it a perfect snapshot to capture in bug reports or CI logs. `zig targets` returns exhaustive architecture/OS/ABI matrices, which you can feed into `std.build` matrices to precompute release artifacts. Together they replace brittle environment variables with a single source of truth. ## Section: Notes & Caveats [section_id: notes-caveats] [section_url: https://zigbook.net/chapters/38__zig-cli-deep-dive#notes-caveats] - Prefer `zig build --build-file ` over copying projects into scratch directories; it lets you experiment with CLI options against isolated build graphs while keeping cache entries deterministic. - macOS users still need to grant file system permissions for `--watch`. Without them, the builder falls back to polling and loses the new responsiveness in 0.15.2. - Time reports can surface plenty of data. Capture them alongside sanitized builds so you know whether costly declarations are tied to debug assertions or optimizer work. ## Section: Exercises [section_id: exercises] [section_url: https://zigbook.net/chapters/38__zig-cli-deep-dive#exercises] - Script `zig env` before and after `zig fetch` to verify the cache paths you rely on in CI remain unchanged across Zig releases. - Extend the `zig ast-check` sample to walk a directory tree, then wire it into a `zig build` custom step so `zig build lint` validates syntax without compiling. 22 (22__build-system-deep-dive.xml) - Use `zig build --webui --time-report --watch` on a medium project and record which declarations dominate the time report; refactor one hot declaration and re-run to quantify the improvement. ## Section: Alternatives & Edge Cases [section_id: caveats-alternatives-edge-cases] [section_url: https://zigbook.net/chapters/38__zig-cli-deep-dive#caveats-alternatives-edge-cases] - `zig run` always produces build artifacts in the cache. If you need a hermetic sandbox, favor `zig build-exe -femit-bin` into a throwaway directory and run the binary manually. - The CLI’s drop-in `zig cc` respects Zig’s idea of the sysroot. If you need the platform vendor toolchain verbatim, invoke `clang` directly to avoid surprising header selections. - `zig targets` output can be enormous. Filter it with `jq` or `grep` before piping into build scripts so that your automation remains stable even if future releases add new fields. # Chapter 39 — Performance & Inlining [chapter_id: 39__performance-and-inlining] [chapter_slug: performance-and-inlining] [chapter_number: 39] [chapter_url: https://zigbook.net/chapters/39__performance-and-inlining] ## Section: Overview [section_id: overview] [section_url: https://zigbook.net/chapters/39__performance-and-inlining#overview] Our CLI survey set the stage for disciplined experimentation. 38 (38__zig-cli-deep-dive.xml) Now we focus on how Zig translates those command-line toggles into machine-level behavior. Semantic inlining, call modifiers, and explicit SIMD all give you levers to shape hot paths—provided you measure carefully and respect the compiler’s defaults. #inline fn (https://ziglang.org/documentation/master/#inline-fn) The next chapter formalizes that measurement loop by layering profiling and hardening workflows on top. 40 (40__profiling-optimization-hardening.xml) ## Section: Learning Goals [section_id: learning-goals] [section_url: https://zigbook.net/chapters/39__performance-and-inlining#learning-goals] - Force or forbid inlining when compile-time semantics must win over heuristics. - Sample hot loops with `@call` and `std.time.Timer` to compare build modes. - Use `@Vector` math as a bridge to portable SIMD before reaching for target-specific intrinsics. #call (https://ziglang.org/documentation/master/#call), Timer.zig (https://github.com/ziglang/zig/tree/master/lib/std/time/Timer.zig), #vectors (https://ziglang.org/documentation/master/#vectors) ## Section: Semantic Inlining vs Optimizer Heuristics [section_id: inline-semantics] [section_url: https://zigbook.net/chapters/39__performance-and-inlining#inline-semantics] Zig’s `inline` keyword changes evaluation rules rather than hinting at the optimizer: compile-time known arguments become compile-time constants, allowing you to generate types or precompute values that ordinary calls would defer to runtime. Inline functions restrict the compiler’s freedom, so reach for them only when semantics matter—propagating `comptime` data, improving debugging, or satisfying real benchmarks. ### Subsection: Understanding Optimization Modes [section_id: _understanding_optimization_modes] [section_url: https://zigbook.net/chapters/39__performance-and-inlining#_understanding_optimization_modes] Before exploring inlining behavior, it’s important to understand the optimization modes that affect how the compiler treats your code. The following diagram shows the optimization configuration: ```text graph TB subgraph "Optimization" OPTIMIZE["Optimization Settings"] OPTIMIZE --> OPTMODE["optimize_mode: OptimizeMode
Debug, ReleaseSafe, ReleaseFast, ReleaseSmall"] OPTIMIZE --> LTO["lto: bool
Link Time Optimization"] end ``` Zig provides four distinct optimization modes, each making different tradeoffs between safety, speed, and binary size. Debug mode disables optimizations and keeps full runtime safety checks, making it ideal for development and debugging. The compiler preserves stack frames, emits symbol information, and never inlines functions unless semantically required. ReleaseSafe enables optimizations while retaining all safety checks (bounds checking, integer overflow detection, etc.), balancing performance with error detection. ReleaseFast maximizes speed by disabling runtime safety checks and enabling aggressive optimizations including heuristic inlining. This is the mode used in the benchmarks throughout this chapter. ReleaseSmall prioritizes binary size over speed, often disabling inlining entirely to reduce code duplication. Additionally, Link Time Optimization (LTO) can be enabled independently via `-flto`, allowing the linker to perform whole-program optimization across compilation units. When benchmarking inlining behavior, these modes dramatically affect results: `inline` functions behave identically across modes (semantic guarantee), but heuristic inlining in ReleaseFast may inline functions that Debug or ReleaseSmall would leave as calls. The chapter’s examples use `-OReleaseFast` to showcase optimizer behavior, but you should test across modes to understand the full performance spectrum. ### Subsection: Example: compile-time math with inline functions [section_id: inline-fibonacci] [section_url: https://zigbook.net/chapters/39__performance-and-inlining#inline-fibonacci] `inline` recursion lets us bake small computations into the binary while leaving a fallback runtime path for larger inputs. The `@call` builtin provides a direct handle to evaluate call sites at compile time when arguments are available. ```zig // This file demonstrates Zig's inline semantics and compile-time execution features. // It shows how the `inline` keyword and `@call` builtin can control when and how // functions are evaluated at compile-time versus runtime. const std = @import("std"); /// Computes the nth Fibonacci number using recursion. /// The `inline` keyword forces this function to be inlined at all call sites, /// and the `comptime n` parameter ensures the value can be computed at compile-time. /// This combination allows the result to be available as a compile-time constant. inline fn fib(comptime n: usize) usize { return if (n <= 1) n else fib(n - 1) + fib(n - 2); } /// Computes the factorial of n using recursion. /// Unlike `fib`, this function is not marked `inline`, so the compiler /// decides whether to inline it based on optimization heuristics. /// It can be called at either compile-time or runtime. fn factorial(n: usize) usize { return if (n <= 1) 1 else n * factorial(n - 1); } // Demonstrates that an inline function with comptime parameters // propagates compile-time execution to its call sites. // The entire computation happens at compile-time within the comptime block. test "inline fibonacci propagates comptime" { comptime { const value = fib(10); try std.testing.expectEqual(@as(usize, 55), value); } } // Demonstrates the `@call` builtin with `.compile_time` modifier. // This forces the function call to be evaluated at compile-time, // even though `factorial` is not marked `inline` and takes non-comptime parameters. test "@call compile_time modifier" { const result = @call(.compile_time, factorial, .{5}); try std.testing.expectEqual(@as(usize, 120), result); } // Verifies that a non-inline function can still be called at runtime. // The input is a runtime value, so the computation happens during execution. test "runtime factorial still works" { const input: usize = 6; const value = factorial(input); try std.testing.expectEqual(@as(usize, 720), value); } ``` Run: ```shell $ zig test 01_inline_semantics.zig ``` Output: ```shell All 3 tests passed. ``` TIP: The `.compile_time` modifier fails if the callee touches runtime-only state. Wrap such experiments in `comptime` blocks first, then add runtime tests so release builds remain covered. ## Section: Directing Calls for Measurement [section_id: call-modifiers] [section_url: https://zigbook.net/chapters/39__performance-and-inlining#call-modifiers] Zig 0.15.2’s self-hosted backends reward accurate microbenchmarks. They can deliver dramatic speedups when paired with the new threaded code generation pipeline. v0.15.2 (https://ziglang.org/download/0.15.1/release-notes.html#threaded-codegen) Use `@call` modifiers to compare inline, default, and never-inline dispatches without refactoring your call sites. ### Subsection: Example: comparing call modifiers under ReleaseFast [section_id: call-benchmark] [section_url: https://zigbook.net/chapters/39__performance-and-inlining#call-benchmark] This benchmark pins the optimizer (`-OReleaseFast`) while we toggle call modifiers. Every variant produces the same result, but the timing highlights how `never_inline` can balloon hot loops when function call overhead dominates. ```zig const std = @import("std"); const builtin = @import("builtin"); // Number of iterations to run each benchmark variant const iterations: usize = 5_000_000; /// A simple mixing function that demonstrates the performance impact of inlining. /// Uses bit rotation and arithmetic operations to create a non-trivial workload /// that the optimizer might handle differently based on call modifiers. fn mix(value: u32) u32 { // Rotate left by 7 bits after XORing with a prime-like constant const rotated = std.math.rotl(u32, value ^ 0x9e3779b9, 7); // Apply additional mixing with wrapping arithmetic to prevent compile-time evaluation return rotated *% 0x85eb_ca6b +% 0xc2b2_ae35; } /// Runs the mixing function in a tight loop using the specified call modifier. /// This allows direct comparison of how different inlining strategies affect performance. fn run(comptime modifier: std.builtin.CallModifier) u32 { var acc: u32 = 0; var i: usize = 0; while (i < iterations) : (i += 1) { // The @call builtin lets us explicitly control inlining behavior at the call site acc = @call(modifier, mix, .{acc}); } return acc; } pub fn main() !void { // Benchmark 1: Let the compiler decide whether to inline (default heuristics) var timer = try std.time.Timer.start(); const auto_result = run(.auto); const auto_ns = timer.read(); // Benchmark 2: Force inlining at every call site timer = try std.time.Timer.start(); const inline_result = run(.always_inline); const inline_ns = timer.read(); // Benchmark 3: Prevent inlining, always emit a function call timer = try std.time.Timer.start(); const never_result = run(.never_inline); const never_ns = timer.read(); // Verify all three strategies produce identical results std.debug.assert(auto_result == inline_result); std.debug.assert(auto_result == never_result); // Display the optimization mode and iteration count for reproducibility std.debug.print( "optimize-mode={s} iterations={}\n", .{ @tagName(builtin.mode), iterations, }, ); // Report timing results for each call modifier std.debug.print("auto call : {d} ns\n", .{auto_ns}); std.debug.print("always_inline: {d} ns\n", .{inline_ns}); std.debug.print("never_inline : {d} ns\n", .{never_ns}); } ``` Run: ```shell $ zig run 03_call_benchmark.zig -OReleaseFast ``` Output: ```shell optimize-mode=ReleaseFast iterations=5000000 auto call : 161394 ns always_inline: 151745 ns never_inline : 2116797 ns ``` NOTE: Performing the same run under `-OReleaseSafe` makes the gap larger because additional safety checks amplify the per-call overhead. v0.15.2 (https://ziglang.org/download/0.15.1/release-notes.html#x86-backend) Use `zig run --time-report` from the previous chapter when you want compiler-side attribution for slow code paths. 38 (38__zig-cli-deep-dive.xml#cli-webui) ## Section: Portable Vectorization with @Vector [section_id: vectorization] [section_url: https://zigbook.net/chapters/39__performance-and-inlining#vectorization] When the compiler cannot infer SIMD usage on its own, `@Vector` types offer a portable shim that respects safety checks and fallback scalar execution. Paired with `@reduce`, you can express horizontal reductions without writing target-specific intrinsics. #reduce (https://ziglang.org/documentation/master/#reduce) ### Subsection: Example: SIMD-friendly dot product [section_id: vector-example] [section_url: https://zigbook.net/chapters/39__performance-and-inlining#vector-example] The scalar and vectorized versions produce identical results. Profiling determines whether the extra vector plumbing pays off on your target. ```zig const std = @import("std"); // Number of parallel operations per vector const lanes = 4; // Vector type that processes 4 f32 values simultaneously using SIMD const Vec = @Vector(lanes, f32); /// Loads 4 consecutive f32 values from a slice into a SIMD vector. /// The caller must ensure that start + 3 is within bounds. fn loadVec(slice: []const f32, start: usize) Vec { return .{ slice[start + 0], slice[start + 1], slice[start + 2], slice[start + 3], }; } /// Computes the dot product of two f32 slices using scalar operations. /// This is the baseline implementation that processes one element at a time. fn dotScalar(values_a: []const f32, values_b: []const f32) f32 { std.debug.assert(values_a.len == values_b.len); var sum: f32 = 0.0; // Multiply corresponding elements and accumulate the sum for (values_a, values_b) |a, b| { sum += a * b; } return sum; } /// Computes the dot product using SIMD vectorization for improved performance. /// Processes 4 elements at a time, then reduces the vector accumulator to a scalar. /// Requires that the input length is a multiple of the lane count (4). fn dotVectorized(values_a: []const f32, values_b: []const f32) f32 { std.debug.assert(values_a.len == values_b.len); std.debug.assert(values_a.len % lanes == 0); // Initialize accumulator vector with zeros var accum: Vec = @splat(0.0); var index: usize = 0; // Process 4 elements per iteration using SIMD while (index < values_a.len) : (index += lanes) { const lhs = loadVec(values_a, index); const rhs = loadVec(values_b, index); // Perform element-wise multiplication and add to accumulator accum += lhs * rhs; } // Sum all lanes of the accumulator vector into a single scalar value return @reduce(.Add, accum); } // Verifies that the vectorized implementation produces the same result as the scalar version. test "vectorized dot product matches scalar" { const lhs = [_]f32{ 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0 }; const rhs = [_]f32{ 8.0, 7.0, 6.0, 5.0, 4.0, 3.0, 2.0, 1.0 }; const scalar = dotScalar(&lhs, &rhs); const vector = dotVectorized(&lhs, &rhs); // Allow small floating-point error tolerance try std.testing.expectApproxEqAbs(scalar, vector, 0.0001); } ``` Run: ```shell $ zig test 02_vector_reduction.zig ``` Output: ```shell All 1 tests passed. ``` TIP: Once you start mixing vectors and scalars, use `@splat` to lift constants and avoid the implicit casts forbidden by the vector rules. ## Section: Notes & Caveats [section_id: notes-caveats] [section_url: https://zigbook.net/chapters/39__performance-and-inlining#notes-caveats] - Inline recursion counts against the compile-time branch quota. Raise it with `@setEvalBranchQuota` only when measurements prove the extra compile-time work is worthwhile. #setevalbranchquota (https://ziglang.org/documentation/master/#setevalbranchquota) - Switching between `@call(.always_inline, …​)` and the `inline` keyword matters: the former applies to a single site, whereas `inline` modifies the callee definition and every future call. - Vector lengths other than powers of two may fall back to scalar loops on some targets. Capture the generated assembly with `zig build-exe -femit-asm` before banking on a win. ### Subsection: Code Generation Features Affecting Performance [section_id: _code_generation_features_affecting_performance] [section_url: https://zigbook.net/chapters/39__performance-and-inlining#_code_generation_features_affecting_performance] Beyond optimization modes, several code generation features affect runtime performance and debuggability. Understanding these flags helps you reason about performance tradeoffs: ```text graph TB subgraph "Code Generation Features" Features["Feature Flags"] Features --> UnwindTables["unwind_tables: bool"] Features --> StackProtector["stack_protector: bool"] Features --> StackCheck["stack_check: bool"] Features --> RedZone["red_zone: ?bool"] Features --> OmitFramePointer["omit_frame_pointer: bool"] Features --> Valgrind["valgrind: bool"] Features --> SingleThreaded["single_threaded: bool"] UnwindTables --> EHFrame["Generate .eh_frame
for exception handling"] StackProtector --> CanaryCheck["Stack canary checks
buffer overflow detection"] StackCheck --> ProbeStack["Stack probing
prevents overflow"] RedZone --> RedZoneSpace["Red zone optimization
(x86_64, AArch64)"] OmitFramePointer --> NoFP["Omit frame pointer
for performance"] Valgrind --> ValgrindSupport["Valgrind client requests
for memory debugging"] SingleThreaded --> NoThreading["Assume single-threaded
enable optimizations"] end ``` The omit_frame_pointer flag is particularly relevant for performance work: when enabled (typical in ReleaseFast), the compiler frees the frame pointer register (RBP on x86_64, FP on ARM) for general use, improving register allocation and enabling more aggressive optimizations. However, this makes stack unwinding harder. Debuggers and profilers may produce incomplete or missing stack traces. The red_zone optimization (x86_64 and AArch64 only) allows functions to use 128 bytes below the stack pointer without adjusting RSP, reducing prologue/epilogue overhead in leaf functions. Stack protection adds canary checks to detect buffer overflows but adds runtime cost. This is why ReleaseFast disables it. Stack checking instruments functions to probe the stack and prevent overflow, useful for deep recursion but costly. Unwind tables generate `.eh_frame` sections for exception handling and debugger stack walks. Debug mode always includes them; release modes may omit them for size. When the exercises suggest measuring allocator hot paths with `@call(.never_inline, …​)`, these flags explain why Debug mode shows better stack traces (frame pointers preserved) at the cost of slower execution (extra instructions, no register optimization). Performance-critical code should benchmark with ReleaseFast but validate correctness with Debug to catch issues the optimizer might hide. ## Section: Exercises [section_id: exercises] [section_url: https://zigbook.net/chapters/39__performance-and-inlining#exercises] - Add a `--mode` flag to the benchmark program so you can flip between Debug, ReleaseSafe, and ReleaseFast runs without editing the code. 38 (38__zig-cli-deep-dive.xml#cli-run-summary) - Extend the dot-product example with a remainder loop that handles slices whose lengths are not multiples of four. Measure the crossover point where SIMD still wins. - Experiment with `@call(.never_inline, …​)` on allocator hot paths from Chapter 10 to confirm whether improved stack traces in Debug are worth the runtime cost. 10 (10__allocators-and-memory-management.xml) ## Section: Alternatives & Edge Cases: [section_id: caveats-alternatives-edge-cases] [section_url: https://zigbook.net/chapters/39__performance-and-inlining#caveats-alternatives-edge-cases] - Microbenchmarks that run inside `zig run` share the compilation cache. Warm the cache with a dummy run before comparing timings to avoid skew. #entry points and command structure (ZIG_DEEP_WIKI.md#entry-points-and-command-structure) - The self-hosted x86 backend is fast but not perfect. Fall back to `-fllvm` if you notice miscompilations while exploring aggressive inline patterns. - ReleaseSmall often disables inlining entirely to save size. When you need both tiny binaries and tuned hot paths, isolate the hot functions and call them from a ReleaseFast-built shared library. # Chapter 40 — Profiling, Optimization, Hardening [chapter_id: 40__profiling-optimization-hardening] [chapter_slug: profiling-optimization-hardening] [chapter_number: 40] [chapter_url: https://zigbook.net/chapters/40__profiling-optimization-hardening] ## Section: Overview [section_id: overview] [section_url: https://zigbook.net/chapters/40__profiling-optimization-hardening#overview] Last chapter we explored semantic inlining and SIMD to shape hotspots (see 39 (39__performance-and-inlining.xml)); this time we go hands-on with the measurement loop that tells you whether those tweaks actually paid off. We will combine lightweight timers, build-mode comparisons, and hardened error guards to turn experimental code into a reliable toolchain. Each technique leans on recent CLI improvements, such as `zig build --time-report`, to keep feedback fast (see v0.15.2 (https://ziglang.org/download/0.15.1/release-notes.html#web-interface-and-time-report)). By the end of this chapter you will have a repeatable recipe: collect timing baselines, choose a release strategy (speed versus size), and run safeguards across optimization levels so regressions surface before deployment. ## Section: Learning Goals [section_id: learning-goals] [section_url: https://zigbook.net/chapters/40__profiling-optimization-hardening#learning-goals] - Instrument hot paths with `std.time.Timer` and interpret the relative deltas (see time.zig (https://github.com/ziglang/zig/tree/master/lib/std/time.zig)). - Compare ReleaseFast and ReleaseSmall artifacts, understanding the trade-off between diagnostics and binary size (see #releasefast (https://ziglang.org/documentation/master/#releasefast)). - Harden parsing and throttling code with error guards that hold under every optimization setting (see testing.zig (https://github.com/ziglang/zig/tree/master/lib/std/testing.zig)). ## Section: Profiling Baselines with Monotonic Timers [section_id: profiling-with-timers] [section_url: https://zigbook.net/chapters/40__profiling-optimization-hardening#profiling-with-timers] `std.time.Timer` samples a monotonic clock, making it ideal for quick \"is it faster?\" experiments without touching global state. Paired with deterministic input data, it keeps microbenchmarks honest when you repeat them under different build modes. ### Subsection: Example: Sorting Strategies Under a Single Timer Harness [section_id: timer-sort-bench] [section_url: https://zigbook.net/chapters/40__profiling-optimization-hardening#timer-sort-bench] We reuse the dataset for three algorithms—block sort, heap sort, and insertion sort—to illustrate how timing ratios guide further investigation. The dataset is regenerated for each run so cache effects stay consistent (see sort.zig (https://github.com/ziglang/zig/tree/master/lib/std/sort.zig)). ```zig // This program demonstrates performance measurement and comparison of different // sorting algorithms using Zig's built-in Timer for benchmarking. const std = @import("std"); // Number of elements to sort in each benchmark run const sample_count = 1024; /// Generates a deterministic array of random u32 values for benchmarking. /// Uses a fixed seed to ensure reproducible results across multiple runs. /// @return: Array of 1024 pseudo-random u32 values fn generateData() [sample_count]u32 { var data: [sample_count]u32 = undefined; // Initialize PRNG with fixed seed for deterministic output var prng = std.Random.DefaultPrng.init(0xfeed_beef_dead_cafe); var random = prng.random(); // Fill each array slot with a random 32-bit unsigned integer for (&data) |*slot| { slot.* = random.int(u32); } return data; } /// Measures the execution time of a sorting function on a copy of the input data. /// Creates a scratch buffer to avoid modifying the original data, allowing /// multiple measurements on the same dataset. /// @param sortFn: Compile-time sorting function to benchmark /// @param source: Source data to sort (remains unchanged) /// @return: Elapsed time in nanoseconds fn measureSort( comptime sortFn: anytype, source: []const u32, ) !u64 { // Create scratch buffer to preserve original data var scratch: [sample_count]u32 = undefined; std.mem.copyForwards(u32, scratch[0..], source); // Start high-resolution timer immediately before sort operation var timer = try std.time.Timer.start(); // Execute the sort with ascending comparison function sortFn(u32, scratch[0..], {}, std.sort.asc(u32)); // Capture elapsed nanoseconds return timer.read(); } pub fn main() !void { // Generate shared dataset for all sorting algorithms var dataset = generateData(); // Benchmark each sorting algorithm on identical data const block_ns = try measureSort(std.sort.block, dataset[0..]); const heap_ns = try measureSort(std.sort.heap, dataset[0..]); const insertion_ns = try measureSort(std.sort.insertion, dataset[0..]); // Display raw timing results along with build mode std.debug.print("optimize-mode={s}\n", .{@tagName(@import("builtin").mode)}); std.debug.print("block sort : {d} ns\n", .{block_ns}); std.debug.print("heap sort : {d} ns\n", .{heap_ns}); std.debug.print("insertion sort : {d} ns\n", .{insertion_ns}); // Calculate relative performance metrics using block sort as baseline const baseline = @as(f64, @floatFromInt(block_ns)); const heap_speedup = baseline / @as(f64, @floatFromInt(heap_ns)); const insertion_slowdown = @as(f64, @floatFromInt(insertion_ns)) / baseline; // Display comparative analysis showing speedup/slowdown factors std.debug.print("heap speedup over block: {d:.2}x\n", .{heap_speedup}); std.debug.print("insertion slowdown vs block: {d:.2}x\n", .{insertion_slowdown}); } ``` Run: ```shell $ zig run 01_timer_probe.zig -OReleaseFast ``` Output: ```shell optimize-mode=ReleaseFast block sort : 43753 ns heap sort : 75331 ns insertion sort : 149541 ns heap speedup over block: 0.58x insertion slowdown vs block: 3.42x ``` NOTE: Follow up with `zig build --time-report -Doptimize=ReleaseFast` on the same module when you need attribution for longer stages like hashing or parsing. ## Section: Trading Binary Size for Diagnostics [section_id: size-strategy] [section_url: https://zigbook.net/chapters/40__profiling-optimization-hardening#size-strategy] Switching between ReleaseFast and ReleaseSmall is more than a compiler flag: ReleaseSmall strips safety checks and aggressively prunes code to shrink the final binary. When you profile on laptops but deploy on embedded devices, build both variants and confirm the difference justifies the lost diagnostics. ### Subsection: Example: Tracing Logic That Disappears in ReleaseSmall [section_id: size-comparison] [section_url: https://zigbook.net/chapters/40__profiling-optimization-hardening#size-comparison] Tracing is enabled only when the optimizer leaves safety checks intact. Measuring binary sizes provides a tangible signal that ReleaseSmall is doing its job. ```zig // This program demonstrates how compile-time configuration affects binary size // by conditionally enabling debug tracing based on the build mode. const std = @import("std"); const builtin = @import("builtin"); // Compile-time flag that enables tracing only in Debug mode // This demonstrates how dead code elimination works in release builds const enable_tracing = builtin.mode == .Debug; // Computes a FNV-1a hash for a given word // FNV-1a is a fast, non-cryptographic hash function // @param word: The input byte slice to hash // @return: A 64-bit hash value fn checksumWord(word: []const u8) u64 { // FNV-1a 64-bit offset basis var state: u64 = 0xcbf29ce484222325; // Process each byte of the input for (word) |byte| { // XOR with the current byte state ^= byte; // Multiply by FNV-1a 64-bit prime (with wrapping multiplication) state = state *% 0x100000001b3; } return state; } pub fn main() !void { // Sample word list to demonstrate the checksum functionality const words = [_][]const u8{ "profiling", "optimization", "hardening", "zig" }; // Accumulator for combining all word checksums var digest: u64 = 0; // Process each word and combine their checksums for (words) |word| { const word_sum = checksumWord(word); // Combine checksums using XOR digest ^= word_sum; // Conditional tracing that will be compiled out in release builds // This demonstrates how build mode affects binary size if (enable_tracing) { std.debug.print("trace: {s} -> {x}\n", .{ word, word_sum }); } } // Output the final result along with the current build mode // Shows how the same code behaves differently based on compilation settings std.debug.print( "mode={s} digest={x}\n", .{ @tagName(builtin.mode), digest, }, ); } ``` Run: ```shell $ zig build-exe 02_binary_size.zig -OReleaseFast -femit-bin=perf-releasefast $ zig build-exe 02_binary_size.zig -OReleaseSmall -femit-bin=perf-releasesmall $ ls -lh perf-releasefast perf-releasesmall ``` Output: ```shell -rwxrwxr-x 1 zkevm zkevm 876K Nov 6 13:12 perf-releasefast -rwxrwxr-x 1 zkevm zkevm 11K Nov 6 13:12 perf-releasesmall ``` TIP: Keep both artifacts around—ReleaseFast for symbol-rich profiling sessions, ReleaseSmall for production handoff. Share them via `zig build --artifact` or package manager hashes to keep CI deterministic. ## Section: Hardening Across Optimization Modes [section_id: hardening-regressions] [section_url: https://zigbook.net/chapters/40__profiling-optimization-hardening#hardening-regressions] After tuning performance and size, wrap the pipeline with tests that assert guard rails across build modes. This is vital because ReleaseFast and ReleaseSmall disable runtime safety checks by default (see #setruntimesafety (https://ziglang.org/documentation/master/#setruntimesafety)). Running the same test suite in ReleaseSafe ensures diagnostics still fire when safety remains enabled. ### Subsection: Example: Validating Input Parsing and Throttling in Every Mode [section_id: guarded-pipeline] [section_url: https://zigbook.net/chapters/40__profiling-optimization-hardening#guarded-pipeline] The pipeline parses limits, clamps workloads, and defends against empty input. The final test loops through values inline, mirroring the real application path while staying cheap to execute. ```zig // This example demonstrates input validation and error handling patterns in Zig, // showing how to create guarded data processing pipelines with proper bounds checking. const std = @import("std"); // Custom error set for parsing and validation operations const ParseError = error{ EmptyInput, // Returned when input contains only whitespace or is empty InvalidNumber, // Returned when input cannot be parsed as a valid number OutOfRange, // Returned when parsed value is outside acceptable bounds }; /// Parses and validates a text input as a u32 limit value. /// Ensures the value is between 1 and 10,000 inclusive. /// Whitespace is automatically trimmed from input. fn parseLimit(text: []const u8) ParseError!u32 { // Remove leading and trailing whitespace characters const trimmed = std.mem.trim(u8, text, " \t\r\n"); if (trimmed.len == 0) return error.EmptyInput; // Attempt to parse as base-10 unsigned 32-bit integer const value = std.fmt.parseInt(u32, trimmed, 10) catch return error.InvalidNumber; // Enforce bounds: reject zero and values exceeding maximum threshold if (value == 0 or value > 10_000) return error.OutOfRange; return value; } /// Applies a throttling limit to a work queue, ensuring safe processing bounds. /// Returns the actual number of items that can be processed, which is the minimum /// of the requested limit and the available work length. fn throttle(work: []const u8, limit: u32) ParseError!usize { // Precondition: limit must be positive (enforced at runtime in debug builds) std.debug.assert(limit > 0); // Guard against empty work queues if (work.len == 0) return error.EmptyInput; // Calculate safe processing limit by taking minimum of requested limit and work size // Cast is safe because we're taking the minimum value const safe_limit = @min(limit, @as(u32, @intCast(work.len))); return safe_limit; } // Test: Verify that valid numeric strings are correctly parsed test "valid limit parses" { try std.testing.expectEqual(@as(u32, 750), try parseLimit("750")); } // Test: Ensure whitespace-only input is properly rejected test "empty input rejected" { try std.testing.expectError(error.EmptyInput, parseLimit(" \n")); } // Test: Verify throttling respects the parsed limit and work size test "in-flight throttling respects guard" { const limit = try parseLimit("32"); // Work length (4) is less than limit (32), so expect work length try std.testing.expectEqual(@as(usize, 4), try throttle("hard", limit)); } // Test: Validate multiple inputs meet the maximum threshold requirement // Demonstrates compile-time iteration for testing multiple scenarios test "validate release configurations" { const inputs = [_][]const u8{ "8", "9999", "500" }; // Compile-time loop unrolls test cases for each input value inline for (inputs) |value| { const parsed = try parseLimit(value); // Ensure parsed values never exceed the defined maximum try std.testing.expect(parsed <= 10_000); } } ``` Run: ```shell $ zig test 03_guarded_pipeline.zig -OReleaseFast ``` Output: ```shell All 4 tests passed. ``` NOTE: Repeat the command with `-OReleaseSafe` and plain `zig test` to make sure guard clauses work identically in safety-on builds. The inline loop proves the compiler can still unroll checks without sacrificing correctness. ## Section: Notes & Caveats [section_id: notes-caveats] [section_url: https://zigbook.net/chapters/40__profiling-optimization-hardening#notes-caveats] - Use deterministic data when microbenchmarking so timer noise reflects algorithm changes, not PRNG drift (see Random.zig (https://github.com/ziglang/zig/tree/master/lib/std/Random.zig)). - ReleaseSmall disables error return traces and many assertions; pair it with a ReleaseFast smoke test before shipping to catch missing diagnostics. - `std.debug.assert` remains active in Debug and ReleaseSafe. If ReleaseFast removes it, compensate with integration tests or explicit error handling (see debug.zig (https://github.com/ziglang/zig/tree/master/lib/std/debug.zig)). ## Section: Exercises [section_id: exercises] [section_url: https://zigbook.net/chapters/40__profiling-optimization-hardening#exercises] - Add a `--sort` flag to select the algorithm at runtime, then capture `zig build --time-report` snapshots for each choice. - Extend the size example with a `--metrics` flag that turns tracing back on; document the binary delta using `zig build-exe -fstrip` for extra savings. - Parameterize `parseLimit` to accept hexadecimal input and tighten the tests so they run under `zig test -OReleaseSmall` without triggering UB. 37 (37__illegal-behavior-and-safety-modes.xml) ## Section: Alternatives & Edge Cases [section_id: caveats-alternatives-edge-cases] [section_url: https://zigbook.net/chapters/40__profiling-optimization-hardening#caveats-alternatives-edge-cases] - Microbenchmarks that rely on `std.debug.print` will skew ReleaseSmall timings because the call is removed. Consider logging into ring buffers instead. - Use `zig build run --watch -fincremental` when iterating on instrumentation. Threaded codegen in 0.15.2 keeps rebuilds responsive even after large edits (see v0.15.2 (https://ziglang.org/download/0.15.1/release-notes.html#threaded-codegen)). - If your tests mutate data structures with undefined behavior in ReleaseFast, isolate the risky code behind `@setRuntimeSafety(true)` for the duration of the hardening exercise. # Chapter 41 — Cross-Compilation & WASM [chapter_id: 41__cross-compilation-and-wasm] [chapter_slug: cross-compilation-and-wasm] [chapter_number: 41] [chapter_url: https://zigbook.net/chapters/41__cross-compilation-and-wasm] ## Section: Overview [section_id: overview] [section_url: https://zigbook.net/chapters/41__cross-compilation-and-wasm#overview] Having tightened our feedback loop with profiling and safeguards, 40 (40__profiling-optimization-hardening.xml) we are ready to ship those binaries to other platforms. This chapter walks through target discovery, native cross-compilation, and the essentials for emitting WASI modules, using the same CLI instrumentation we relied on earlier. #entry points and command structure (ZIG_DEEP_WIKI.md#entry-points-and-command-structure) The very next chapter turns these mechanics into a full WASI project, so treat this as your hands-on preflight. 42 (42__project-wasi-build-and-run.xml) ## Section: Learning Goals [section_id: learning-goals] [section_url: https://zigbook.net/chapters/41__cross-compilation-and-wasm#learning-goals] - Interpret target triples and query Zig’s built-in metadata for alternate architectures. Query.zig (https://github.com/ziglang/zig/tree/master/lib/std/Target/Query.zig) - Cross-compile native executables with `zig build-exe` and verify artifacts without leaving Linux. - Produce WASI binaries that share the same source as native code, ready for the project build pipeline. #Command-line-flags (https://ziglang.org/documentation/master/#Command-line-flags) ## Section: Mapping Target Triples [section_id: target-discovery] [section_url: https://zigbook.net/chapters/41__cross-compilation-and-wasm#target-discovery] Zig’s `@import("builtin")` exposes the compiler’s current idea of the world, while `std.Target.Query.parse` lets you inspect hypothetical targets without building them. Target.zig (https://github.com/ziglang/zig/tree/master/lib/std/Target.zig) This is the foundation for tailoring build graphs or ENT files before you touch `zig build`. ### Subsection: Understanding the Target Structure [section_id: _understanding_the_target_structure] [section_url: https://zigbook.net/chapters/41__cross-compilation-and-wasm#_understanding_the_target_structure] Before parsing target triples, it’s valuable to understand how Zig represents compilation targets internally. The following diagram shows the complete `std.Target` structure: ```text graph TB subgraph "std.Target Structure" TARGET["std.Target"] CPU["cpu: Cpu"] OS["os: Os"] ABI["abi: Abi"] OFMT["ofmt: ObjectFormat"] DYNLINKER["dynamic_linker: DynamicLinker"] TARGET --> CPU TARGET --> OS TARGET --> ABI TARGET --> OFMT TARGET --> DYNLINKER end subgraph "Cpu Components" CPU --> ARCH["arch: Cpu.Arch"] CPU --> MODEL["model: *const Cpu.Model"] CPU --> FEATURES["features: Feature.Set"] ARCH --> ARCHEX["x86_64, aarch64, wasm32, etc"] MODEL --> MODELEX["generic, native, specific variants"] FEATURES --> FEATEX["CPU feature flags"] end subgraph "Os Components" OS --> OSTAG["tag: Os.Tag"] OS --> VERSION["version_range: VersionRange"] OSTAG --> OSEX["linux, windows, macos, wasi, etc"] VERSION --> VERUNION["linux: LinuxVersionRange
windows: WindowsVersion.Range
semver: SemanticVersion.Range
none: void"] end subgraph "Abi and Format" ABI --> ABIEX["gnu, musl, msvc, none, etc"] OFMT --> OFMTEX["elf, macho, coff, wasm, c, spirv"] end ``` This structure reveals how target triples map to concrete configuration. When you specify `-target wasm32-wasi`, you’re setting CPU architecture to `wasm32`, OS tag to `wasi`, and implicitly ObjectFormat to `wasm`. The triple `x86_64-windows-gnu` maps to arch `x86_64`, OS `windows`, ABI `gnu`, and format `coff` (Windows PE). Each component affects code generation: the CPU arch determines instruction sets and calling conventions, the OS tag selects system call interfaces and runtime expectations, the ABI specifies calling conventions and name mangling, and the ObjectFormat chooses the linker (ELF for Linux, Mach-O for Darwin, COFF for Windows, WASM for web/WASI). Understanding this mapping helps you decode `std.Target.Query.parse` results, predict cross-compilation behavior, and troubleshoot target-specific issues. The CPU features field captures architecture-specific capabilities (AVX on x86_64, SIMD on ARM) that the optimizer uses for code generation. ### Subsection: Target Resolution Flow [section_id: _target_resolution_flow] [section_url: https://zigbook.net/chapters/41__cross-compilation-and-wasm#_target_resolution_flow] Target queries (user input) get resolved into concrete targets through a systematic process: ```text graph TB subgraph "Resolution Flow" QUERY["std.Target.Query
user input with defaults"] RESOLVE["resolveTargetQuery()"] TARGET["std.Target
fully resolved"] QUERY --> RESOLVE RESOLVE --> TARGET end subgraph "Query Sources" CMDLINE["-target flag
command line"] DEFAULT["native detection
std.zig.system"] MODULE["Module.resolved_target"] CMDLINE --> QUERY DEFAULT --> QUERY end subgraph "Native Detection" DETECT["std.zig.system detection"] CPUDETECT["CPU: cpuid, /proc/cpuinfo"] OSDETECT["OS: uname, NT version"] ABIDETECT["ABI: ldd, platform defaults"] DETECT --> CPUDETECT DETECT --> OSDETECT DETECT --> ABIDETECT end TARGET --> COMP["Compilation.root_mod
.resolved_target.result"] ``` Target queries come from three sources: command-line `-target` flags (explicit user choice), native detection when no target is specified (reads host CPU via cpuid or /proc/cpuinfo, OS via uname or NT APIs, and ABI via ldd or platform defaults), or module configuration in build scripts. The `resolveTargetQuery()` function converts queries (which may contain "native" or "default" placeholders) into fully concrete `std.Target` instances by filling in all missing details. This resolution happens during compilation initialization before any code generation occurs. When you omit `-target`, Zig automatically detects your host system and builds a native target. When you specify a partial triple like `wasm32-wasi`, resolution fills in the ABI (typically `musl` for WASI) and object format (`wasm`). The resolved target then flows into the compilation module where it controls every aspect of code generation, from instruction selection to runtime library choices. ### Subsection: Example: comparing host and cross targets from code [section_id: target-query-example] [section_url: https://zigbook.net/chapters/41__cross-compilation-and-wasm#target-query-example] The sample introspects the host triple and then parses two cross targets, printing the resolved architecture, OS, and ABI. ```zig // Import standard library for target querying and printing const std = @import("std"); // Import builtin module to access compile-time host target information const builtin = @import("builtin"); /// Entry point that demonstrates target discovery and cross-platform metadata inspection. /// This example shows how to introspect both the host compilation target and parse /// hypothetical cross-compilation targets without actually building for them. pub fn main() void { // Print the host target triple (architecture-OS-ABI) by accessing builtin.target // This shows the platform Zig is currently compiling for std.debug.print( "host triple: {s}-{s}-{s}\n", .{ @tagName(builtin.target.cpu.arch), @tagName(builtin.target.os.tag), @tagName(builtin.target.abi), }, ); // Display the pointer width for the host target // @bitSizeOf(usize) returns the size in bits of a pointer on the current platform std.debug.print("pointer width: {d} bits\n", .{@bitSizeOf(usize)}); // Parse a WASI target query from a target triple string // This demonstrates how to inspect cross-compilation targets programmatically const wasm_query = std.Target.Query.parse(.{ .arch_os_abi = "wasm32-wasi" }) catch unreachable; describeQuery("wasm32-wasi", wasm_query); // Parse a Windows target query to show another cross-compilation scenario // The triple format follows: architecture-OS-ABI const windows_query = std.Target.Query.parse(.{ .arch_os_abi = "x86_64-windows-gnu" }) catch unreachable; describeQuery("x86_64-windows-gnu", windows_query); // Print whether the host target is configured for single-threaded execution // This compile-time constant affects runtime library behavior std.debug.print("single-threaded: {}\n", .{builtin.single_threaded}); } /// Prints the resolved architecture, OS, and ABI for a given target query. /// This helper demonstrates how to extract and display target metadata, using /// the host target as a fallback when the query doesn't specify certain fields. fn describeQuery(label: []const u8, query: std.Target.Query) void { std.debug.print( "query {s}: arch={s} os={s} abi={s}\n", .{ label, // Fall back to host architecture if query doesn't specify one @tagName((query.cpu_arch orelse builtin.target.cpu.arch)), // Fall back to host OS if query doesn't specify one @tagName((query.os_tag orelse builtin.target.os.tag)), // Fall back to host ABI if query doesn't specify one @tagName((query.abi orelse builtin.target.abi)), }, ); } ``` Run: ```shell $ zig run 01_target_matrix.zig ``` Output: ```shell host triple: x86_64-linux-gnu pointer width: 64 bits query wasm32-wasi: arch=wasm32 os=wasi abi=gnu query x86_64-windows-gnu: arch=x86_64 os=windows abi=gnu single-threaded: false ``` NOTE: The parser obeys the same syntax as `-Dtarget` or `zig build-exe -target`; recycle the output to seed build configurations before invoking the compiler. ## Section: Cross-Compiling Native Executables [section_id: native-cross] [section_url: https://zigbook.net/chapters/41__cross-compilation-and-wasm#native-cross] With a triple in hand, cross-compiling is a matter of swapping the target flag. Zig 0.15.2 ships with self-contained libc integrations, so producing Windows or macOS binaries on Linux no longer requires additional SDKs. v0.15.2 (https://ziglang.org/download/0.15.1/release-notes.html) Use `file` or similar tooling to confirm artifacts without booting another OS. ### Subsection: Example: to Windows from Linux [section_id: windows-example] [section_url: https://zigbook.net/chapters/41__cross-compilation-and-wasm#windows-example] We keep the source identical, run it natively for sanity, then emit a Windows PE binary and inspect it in place. ```zig // Import the standard library for printing and platform utilities const std = @import("std"); // Import builtin to access compile-time target information const builtin = @import("builtin"); // Entry point that demonstrates cross-compilation by displaying target platform information pub fn main() void { // Print the target platform's CPU architecture, OS, and ABI // Uses builtin.target to access compile-time target information std.debug.print("hello from {s}-{s}-{s}!\n", .{ @tagName(builtin.target.cpu.arch), @tagName(builtin.target.os.tag), @tagName(builtin.target.abi), }); // Retrieve the platform-specific executable file extension (e.g., ".exe" on Windows, "" on Linux) const suffix = std.Target.Os.Tag.exeFileExt(builtin.target.os.tag, builtin.target.cpu.arch); std.debug.print("default executable suffix: {s}\n", .{suffix}); } ``` Run: ```shell $ zig run 02_cross_greeter.zig ``` Output: ```shell hello from x86_64-linux-gnu! default executable suffix: ``` Cross-compile: ```shell $ zig build-exe 02_cross_greeter.zig -target x86_64-windows-gnu -OReleaseFast -femit-bin=greeter-windows.exe $ file greeter-windows.exe ``` Output: ```shell greeter-windows.exe: PE32+ executable (console) x86-64, for MS Windows, 7 sections ``` TIP: Pair `-target` with `-mcpu=baseline` when you need portable binaries for older hardware; the `std.Target.Query` output above shows which CPU model Zig will assume. ## Section: Emitting WASI Modules [section_id: wasi-modules] [section_url: https://zigbook.net/chapters/41__cross-compilation-and-wasm#wasi-modules] WebAssembly System Interface (WASI) builds share most of the native pipeline with a different object format. The same Zig source can print diagnostics on Linux and emit a `.wasm` payload when cross-compiled, thanks to shared libc pieces introduced in this release. ### Subsection: Object Formats and Linker Selection [section_id: _object_formats_and_linker_selection] [section_url: https://zigbook.net/chapters/41__cross-compilation-and-wasm#_object_formats_and_linker_selection] Before generating WASI binaries, it’s important to understand how object formats determine compilation output. The following diagram shows the relationship between ABIs and object formats: ```text graph TB subgraph "Common ABIs" ABI["Abi enum"] ABI --> GNU["gnu
GNU toolchain"] ABI --> MUSL["musl
musl libc"] ABI --> MSVC["msvc
Microsoft Visual C++"] ABI --> NONE["none
freestanding"] ABI --> ANDROID["android, gnueabi, etc
platform variants"] end subgraph "Object Formats" OFMT["ObjectFormat enum"] OFMT --> ELF["elf
Linux, BSD"] OFMT --> MACHO["macho
Darwin systems"] OFMT --> COFF["coff
Windows PE"] OFMT --> WASM["wasm
WebAssembly"] OFMT --> C["c
C source output"] OFMT --> SPIRV["spirv
Shaders"] end ``` The object format determines which linker implementation Zig uses to produce final binaries. ELF (Executable and Linkable Format) is used for Linux and BSD systems, producing `.so` shared libraries and standard executables. Mach-O targets Darwin systems (macOS, iOS), generating `.dylib` libraries and Mach executables. COFF (Common Object File Format) produces Windows PE binaries (`.exe`, `.dll`) when targeting Windows. WASM (WebAssembly) is a unique format that produces `.wasm` modules for web browsers and WASI runtimes. Unlike traditional formats, WASM modules are platform-independent bytecode designed for sandboxed execution. C and SPIRV are specialized: C outputs source code for integration with C build systems, while SPIRV produces GPU shader bytecode. When you build for `-target wasm32-wasi`, Zig selects the WASM object format and invokes the WebAssembly linker (`link/Wasm.zig`), which handles WASM-specific concepts like function imports/exports, memory management, and table initialization. This is fundamentally different from the ELF linker (symbol resolution, relocations) or COFF linker (import tables, resource sections). The same source code compiles to different object formats transparently—your Zig code remains identical whether targeting native Linux (ELF), Windows (COFF), or WASI (WASM). ### Subsection: Example: single source, native run, WASI artifact [section_id: wasi-example] [section_url: https://zigbook.net/chapters/41__cross-compilation-and-wasm#wasi-example] Our pipeline logs the execution stages and branches on `builtin.target.os.tag` so the WASI build announces its own entry point. ```zig // Import standard library for debug printing capabilities const std = @import("std"); // Import builtin module to access compile-time target information const builtin = @import("builtin"); /// Prints a stage name to stderr for tracking execution flow. /// This helper function demonstrates debug output in cross-platform contexts. fn stage(name: []const u8) void { std.debug.print("stage: {s}\n", .{name}); } /// Demonstrates conditional compilation based on target OS. /// This example shows how Zig code can branch at compile-time depending on /// whether it's compiled for WASI (WebAssembly System Interface) or native platforms. /// The execution flow changes based on the target, illustrating cross-compilation capabilities. pub fn main() void { // Simulate initial argument parsing stage stage("parse-args"); // Simulate payload rendering stage stage("render-payload"); // Compile-time branch: different entry points for WASI vs native targets // This demonstrates how Zig handles platform-specific code paths if (builtin.target.os.tag == .wasi) { stage("wasi-entry"); } else { stage("native-entry"); } // Print the actual OS tag name for the compilation target // @tagName converts the enum value to its string representation stage(@tagName(builtin.target.os.tag)); } ``` Run: ```shell $ zig run 03_wasi_pipeline.zig ``` Output: ```shell stage: parse-args stage: render-payload stage: native-entry stage: linux ``` WASI build: ```shell $ zig build-exe 03_wasi_pipeline.zig -target wasm32-wasi -OReleaseSmall -femit-bin=wasi-pipeline.wasm $ ls -lh wasi-pipeline.wasm ``` Output: ```shell -rwxr--r-- 1 zkevm zkevm 4.6K Nov 6 13:40 wasi-pipeline.wasm ``` NOTE: Run the resulting module with your preferred runtime (Wasmtime, Wasmer, browsers) or hand it to the build graph from the next chapter. No source changes required. ## Section: Notes & Caveats [section_id: notes-caveats] [section_url: https://zigbook.net/chapters/41__cross-compilation-and-wasm#notes-caveats] - `zig targets` provides the authoritative matrix of supported triples. Script it to validate your build matrix before dispatching jobs. - Some targets default to `ReleaseSmall`-style safety. Explicitly set `-Doptimize` when you require consistent runtime checks across architectures. #releasefast (https://ziglang.org/documentation/master/#releasefast) - When cross-linking to glibc, populate `ZIG_LIBC` or use `zig fetch` to cache sysroot artifacts so the linker does not reach for host headers unexpectedly. ## Section: Exercises [section_id: exercises] [section_url: https://zigbook.net/chapters/41__cross-compilation-and-wasm#exercises] - Extend the greeter program with `--cpu` and `--os` flags, then emit binaries for `x86_64-macos-gnu` and `aarch64-linux-musl` and capture their sizes with `ls -lh`. - Modify the WASI pipeline to emit JSON via `std.json.stringify`, then run it in a WASI runtime and capture the output for regression tests. json.zig (https://github.com/ziglang/zig/tree/master/lib/std/json.zig) - Write a `build.zig` step that loops over a list of target triples and calls `addExecutable` once per target, using the `std.Target.Query` helper to print human-friendly labels. 22 (22__build-system-deep-dive.xml) ## Section: Alternatives & Edge Cases: [section_id: caveats-alternatives-edge-cases] [section_url: https://zigbook.net/chapters/41__cross-compilation-and-wasm#caveats-alternatives-edge-cases] - LLVM-backed targets may still behave differently from Zig’s self-hosted codegen. Fall back to `-fllvm` when you hit nascent architectures. - WASI forbids many syscalls and dynamic allocation patterns. Keep logging terse or gated to avoid blowing the import budget. - Windows cross-compiles pick the GNU toolchain by default. Add `-msvc` or switch ABI if you intend to link against MSVC-provided libraries. 20 (20__concept-primer-modules-vs-programs-vs-packages-vs-libraries.xml) # Chapter 42 — Project [chapter_id: 42__project-wasi-build-and-run] [chapter_slug: project-wasi-build-and-run] [chapter_number: 42] [chapter_url: https://zigbook.net/chapters/42__project-wasi-build-and-run] ## Section: Overview [section_id: overview] [section_url: https://zigbook.net/chapters/42__project-wasi-build-and-run#overview] With the cross-compilation mechanics from the previous chapter (see 41 (41__cross-compilation-and-wasm.xml)), we can now assemble a complete WASI project that compiles to both native and WebAssembly targets using a single `build.zig`. This chapter constructs a small log analyzer CLI that reads input, processes it, and emits summary statistics—functionality that maps cleanly to WASI’s file and stdio capabilities (see wasi.zig (https://github.com/ziglang/zig/tree/master/lib/std/os/wasi.zig)). You’ll write the application once, then generate and test both a Linux executable and a `.wasm` module using runtimes like Wasmtime or Wasmer (see v0.15.2 (https://ziglang.org/download/0.15.1/release-notes.html)). The build system will define multiple targets, each with its own artifact, and you’ll wire run steps that automatically launch the correct runtime based on target (see 22 (22__build-system-deep-dive.xml)). By the end, you’ll have a working template for shipping portable command-line tools as both native binaries and WASI modules. ## Section: Learning Goals [section_id: learning-goals] [section_url: https://zigbook.net/chapters/42__project-wasi-build-and-run#learning-goals] - Structure a Zig project with shared source code that compiles cleanly to `x86_64-linux` and `wasm32-wasi` (see Target.zig (https://github.com/ziglang/zig/tree/master/lib/std/Target.zig)). - Integrate multiple `addExecutable` targets in `build.zig` with distinct optimization and naming strategies (see Build.zig (https://github.com/ziglang/zig/tree/master/lib/std/Build.zig)). - Configure run steps with runtime detection (native vs Wasmtime/Wasmer) and pass arguments through to the final binary (see 22 (22__build-system-deep-dive.xml)). - Test the same logic path in both native and WASI environments, validating cross-platform behavior (see #Command-line-flags (https://ziglang.org/documentation/master/#Command-line-flags)). ## Section: Project Structure [section_id: project-structure] [section_url: https://zigbook.net/chapters/42__project-wasi-build-and-run#project-structure] We organize the analyzer as a single-package workspace with a `src/` directory containing the entry point and analysis logic. The `build.zig` will create two artifacts: `log-analyzer-native` and `log-analyzer-wasi`. ### Subsection: Directory Layout [section_id: directory-layout] [section_url: https://zigbook.net/chapters/42__project-wasi-build-and-run#directory-layout] ```text 42-log-analyzer/ ├── build.zig ├── build.zig.zon └── src/ ├── main.zig └── analysis.zig ``` NOTE: The `build.zig.zon` is minimal since we have no external dependencies; it serves as metadata for potential future packaging (see 21 (21__zig-init-and-package-metadata.xml)). ### Subsection: Package Metadata [section_id: build-zig-zon] [section_url: https://zigbook.net/chapters/42__project-wasi-build-and-run#build-zig-zon] ```zig .{ // Package identifier used in dependencies and imports // Must be a valid Zig identifier (no hyphens or special characters) .name = .log_analyzer, // Semantic version of this package // Format: major.minor.patch following semver conventions .version = "0.1.0", // Minimum Zig compiler version required to build this package // Ensures compatibility with language features and build system APIs .minimum_zig_version = "0.15.2", // List of paths to include when publishing or distributing the package // Empty string includes all files in the package directory .paths = .{ "", }, // Unique identifier generated by the package manager for integrity verification // Used to detect changes and ensure package authenticity .fingerprint = 0xba0348facfd677ff, } ``` TIP: The `.minimum_zig_version` field prevents accidental builds with older compilers that lack WASI improvements introduced in 0.15.2. ## Section: Build System Setup [section_id: build-system-setup] [section_url: https://zigbook.net/chapters/42__project-wasi-build-and-run#build-system-setup] Our `build.zig` defines two executables sharing the same root source file but targeting different platforms. We also add a custom run step for the WASI binary that detects available runtimes. ### Subsection: Multi-Target Build Script [section_id: build-zig-multi-target] [section_url: https://zigbook.net/chapters/42__project-wasi-build-and-run#build-zig-multi-target] ```zig const std = @import("std"); /// Build script for log-analyzer project demonstrating native and WASI cross-compilation. /// Produces two executables: one for native execution and one for WASI runtimes. pub fn build(b: *std.Build) void { // Standard target and optimization options from command-line flags // These allow users to specify --target and --optimize when building const target = b.standardTargetOptions(.{}); const optimize = b.standardOptimizeOption(.{}); // Native executable: optimized for fast runtime performance on the host system // This target respects user-specified target and optimization settings const exe_native = b.addExecutable(.{ .name = "log-analyzer-native", .root_module = b.createModule(.{ .root_source_file = b.path("src/main.zig"), .target = target, .optimize = optimize, }), }); // Register the native executable for installation to zig-out/bin b.installArtifact(exe_native); // WASI executable: cross-compiled to WebAssembly with WASI support // Uses ReleaseSmall to minimize binary size for portable distribution const wasi_target = b.resolveTargetQuery(.{ .cpu_arch = .wasm32, .os_tag = .wasi, }); const exe_wasi = b.addExecutable(.{ .name = "log-analyzer-wasi", .root_module = b.createModule(.{ .root_source_file = b.path("src/main.zig"), .target = wasi_target, .optimize = .ReleaseSmall, // Prioritize small binary size over speed }), }); // Register the WASI executable for installation to zig-out/bin b.installArtifact(exe_wasi); // Create run step for native target that executes the compiled binary directly const run_native = b.addRunArtifact(exe_native); // Ensure the binary is built and installed before attempting to run it run_native.step.dependOn(b.getInstallStep()); // Forward any command-line arguments passed after -- to the executable if (b.args) |args| { run_native.addArgs(args); } // Register the run step so users can invoke it with `zig build run-native` const run_native_step = b.step("run-native", "Run the native log analyzer"); run_native_step.dependOn(&run_native.step); // Create run step for WASI target with automatic runtime detection // First, attempt to detect an available WASI runtime (wasmtime or wasmer) const run_wasi = b.addSystemCommand(&.{"echo"}); const wasi_runtime = detectWasiRuntime(b) orelse { // If no runtime is found, provide a helpful error message run_wasi.addArg("ERROR: No WASI runtime (wasmtime or wasmer) found in PATH"); const run_wasi_step = b.step("run-wasi", "Run the WASI log analyzer (requires wasmtime or wasmer)"); run_wasi_step.dependOn(&run_wasi.step); return; }; // Construct the command to run the WASI binary with the detected runtime const run_wasi_cmd = b.addSystemCommand(&.{wasi_runtime}); // Both wasmtime and wasmer require the 'run' subcommand if (std.mem.eql(u8, wasi_runtime, "wasmtime") or std.mem.eql(u8, wasi_runtime, "wasmer")) { run_wasi_cmd.addArg("run"); // Grant access to the current directory for file I/O operations run_wasi_cmd.addArg("--dir=."); } // Add the WASI binary as the target to execute run_wasi_cmd.addArtifactArg(exe_wasi); // Forward user arguments after the -- separator to the WASI program if (b.args) |args| { run_wasi_cmd.addArg("--"); run_wasi_cmd.addArgs(args); } // Ensure the WASI binary is built before attempting to run it run_wasi_cmd.step.dependOn(b.getInstallStep()); // Register the WASI run step so users can invoke it with `zig build run-wasi` const run_wasi_step = b.step("run-wasi", "Run the WASI log analyzer (requires wasmtime or wasmer)"); run_wasi_step.dependOn(&run_wasi_cmd.step); } /// Detect available WASI runtime in the system PATH. /// Checks for wasmtime first, then wasmer as a fallback. /// Returns the name of the detected runtime, or null if neither is found. fn detectWasiRuntime(b: *std.Build) ?[]const u8 { // Attempt to locate wasmtime using the 'which' command var exit_code: u8 = undefined; _ = b.runAllowFail(&.{ "which", "wasmtime" }, &exit_code, .Ignore) catch { // If wasmtime is not found, try wasmer as a fallback _ = b.runAllowFail(&.{ "which", "wasmer" }, &exit_code, .Ignore) catch { // Neither runtime was found in PATH return null; }; return "wasmer"; }; // wasmtime was successfully located return "wasmtime"; } ``` Build: ```shell $ zig build ``` Output: ```shell (no output on success; artifacts installed to zig-out/bin/) ``` IMPORTANT: The WASI target sets `-OReleaseSmall` to minimize module size, while the native target uses `-OReleaseFast` for runtime speed—demonstrating per-artifact optimization control. ## Section: Analysis Logic [section_id: analysis-logic] [section_url: https://zigbook.net/chapters/42__project-wasi-build-and-run#analysis-logic] The analyzer reads the entire log content, splits it by newlines, counts occurrences of severity keywords (ERROR, WARN, INFO), and prints a summary. We factor the parsing into `analysis.zig` so it can be unit-tested independently of I/O. ### Subsection: Core Analysis Module [section_id: analysis-module] [section_url: https://zigbook.net/chapters/42__project-wasi-build-and-run#analysis-module] ```zig // This module provides log analysis functionality for counting severity levels in log files. // It demonstrates basic string parsing and struct usage in Zig. const std = @import("std"); // LogStats holds the count of each log severity level found during analysis. // All fields are initialized to zero by default, representing no logs counted yet. pub const LogStats = struct { info_count: u32 = 0, warn_count: u32 = 0, error_count: u32 = 0, }; /// Analyze log content, counting severity keywords. /// Returns statistics in a LogStats struct. pub fn analyzeLog(content: []const u8) LogStats { // Initialize stats with all counts at zero var stats = LogStats{}; // Create an iterator that splits the content by newline characters // This allows us to process the log line by line var it = std.mem.splitScalar(u8, content, '\n'); // Process each line in the log content while (it.next()) |line| { // Count occurrences of severity keywords // indexOf returns an optional - if found, we increment the corresponding counter if (std.mem.indexOf(u8, line, "INFO")) |_| { stats.info_count += 1; } if (std.mem.indexOf(u8, line, "WARN")) |_| { stats.warn_count += 1; } if (std.mem.indexOf(u8, line, "ERROR")) |_| { stats.error_count += 1; } } return stats; } // Test basic log analysis with multiple severity levels test "analyzeLog basic counting" { const input = "INFO startup\nERROR failed\nWARN retry\nINFO success\n"; const stats = analyzeLog(input); // Verify each severity level was counted correctly try std.testing.expectEqual(@as(u32, 2), stats.info_count); try std.testing.expectEqual(@as(u32, 1), stats.warn_count); try std.testing.expectEqual(@as(u32, 1), stats.error_count); } // Test that empty input produces zero counts for all severity levels test "analyzeLog empty input" { const input = ""; const stats = analyzeLog(input); // All counts should remain at their default zero value try std.testing.expectEqual(@as(u32, 0), stats.info_count); try std.testing.expectEqual(@as(u32, 0), stats.warn_count); try std.testing.expectEqual(@as(u32, 0), stats.error_count); } ``` NOTE: By accepting content as a slice, `analyzeLog` remains simple and testable. `main.zig` handles file reading, and the function just processes text (see mem.zig (https://github.com/ziglang/zig/tree/master/lib/std/mem.zig)). ## Section: Main Entry Point [section_id: main-entry-point] [section_url: https://zigbook.net/chapters/42__project-wasi-build-and-run#main-entry-point] The entry point parses command-line arguments, reads the entire file content (or stdin), delegates to `analyzeLog`, and prints the results. Both native and WASI builds share this code path; WASI handles file access through its virtualized filesystem or stdin. ### Subsection: Main Source File [section_id: main-zig] [section_url: https://zigbook.net/chapters/42__project-wasi-build-and-run#main-zig] ```zig const std = @import("std"); const analysis = @import("analysis.zig"); pub fn main() !void { // Initialize general-purpose allocator for dynamic memory allocation var gpa = std.heap.GeneralPurposeAllocator(.{}){}; defer _ = gpa.deinit(); const allocator = gpa.allocator(); // Parse command-line arguments into an allocated slice const args = try std.process.argsAlloc(allocator); defer std.process.argsFree(allocator, args); // Check for optional --input flag to specify a file path var input_path: ?[]const u8 = null; var i: usize = 1; // Skip program name at args[0] while (i < args.len) : (i += 1) { if (std.mem.eql(u8, args[i], "--input")) { i += 1; if (i < args.len) { input_path = args[i]; } else { std.debug.print("ERROR: --input requires a file path\n", .{}); return error.MissingArgument; } } } // Read input content from either file or stdin // Using labeled blocks to unify type across both branches const content = if (input_path) |path| blk: { std.debug.print("analyzing: {s}\n", .{path}); // Read entire file content with 10MB limit break :blk try std.fs.cwd().readFileAlloc(allocator, path, 10 * 1024 * 1024); } else blk: { std.debug.print("analyzing: stdin\n", .{}); // Construct File handle directly from stdin file descriptor const stdin = std.fs.File{ .handle = std.posix.STDIN_FILENO }; // Read all available stdin data with same 10MB limit break :blk try stdin.readToEndAlloc(allocator, 10 * 1024 * 1024); }; defer allocator.free(content); // Delegate log analysis to the analysis module const stats = analysis.analyzeLog(content); // Print summary statistics to stderr (std.debug.print) std.debug.print("results: INFO={d} WARN={d} ERROR={d}\n", .{ stats.info_count, stats.warn_count, stats.error_count, }); } ``` TIP: The `--input` flag allows testing with files; omit it to read from stdin, which WASI runtimes can pipe easily. Note that WASI filesystem access requires explicit capability grants from the runtime (see posix.zig (https://github.com/ziglang/zig/tree/master/lib/std/posix.zig)). ## Section: Building and Running [section_id: building-and-running] [section_url: https://zigbook.net/chapters/42__project-wasi-build-and-run#building-and-running] With the source complete, we can build both targets and run them side-by-side to confirm identical behavior. ### Subsection: Native Execution [section_id: native-build-run] [section_url: https://zigbook.net/chapters/42__project-wasi-build-and-run#native-build-run] ```shell $ zig build $ echo -e "INFO startup\nERROR failed\nWARN retry\nINFO success" > sample.log $ ./zig-out/bin/log-analyzer-native --input sample.log ``` Output: ```shell analyzing: sample.log results: INFO=2 WARN=1 ERROR=1 ``` ### Subsection: WASI Execution with Wasmer (Stdin) [section_id: wasi-build-run] [section_url: https://zigbook.net/chapters/42__project-wasi-build-and-run#wasi-build-run] ```shell $ zig build $ echo -e "INFO startup\nERROR failed\nWARN retry\nINFO success" | wasmer run zig-out/bin/log-analyzer-wasi.wasm ``` Output: ```shell analyzing: stdin results: INFO=2 WARN=1 ERROR=1 ``` IMPORTANT: WASI stdin piping works reliably across runtimes. File access with `--input` requires capability grants (`--dir` or `--mapdir`) which vary by runtime implementation and may have limitations in preview1. ### Subsection: Native Stdin Test for Comparison [section_id: wasi-run-with-wasmer] [section_url: https://zigbook.net/chapters/42__project-wasi-build-and-run#wasi-run-with-wasmer] ```shell $ echo -e "INFO startup\nERROR failed\nWARN retry\nINFO success" | ./zig-out/bin/log-analyzer-native ``` Output: ```shell analyzing: stdin results: INFO=2 WARN=1 ERROR=1 ``` TIP: Both native and WASI produce identical output when reading from stdin, demonstrating true source-level portability for command-line tools. ## Section: Using Run Steps [section_id: zig-build-run-steps] [section_url: https://zigbook.net/chapters/42__project-wasi-build-and-run#zig-build-run-steps] The `build.zig` includes run step definitions for both targets. Invoke them directly: ```shell $ zig build run-native -- --input sample.log ``` Output: ```shell analyzing: sample.log results: INFO=2 WARN=1 ERROR=1 ``` ```shell $ echo -e "INFO test" | zig build run-wasi ``` Output: ```shell analyzing: stdin results: INFO=1 WARN=0 ERROR=0 ``` NOTE: The `run-wasi` step automatically selects an installed WASI runtime (Wasmtime or Wasmer) or errors if neither is available. See the `detectWasiRuntime` helper in `build.zig`. ## Section: Binary Size Comparison [section_id: size-comparison] [section_url: https://zigbook.net/chapters/42__project-wasi-build-and-run#size-comparison] WASI modules built with `-OReleaseSmall` produce compact artifacts: ```shell $ ls -lh zig-out/bin/log-analyzer-* ``` Output: ```shell -rwxrwxr-x 1 user user 7.9M Nov 6 14:29 log-analyzer-native -rwxr--r-- 1 user user 18K Nov 6 14:29 log-analyzer-wasi.wasm ``` TIP: The `.wasm` module is dramatically smaller (18KB vs 7.9MB) because it omits native OS integration and relies on the host runtime for system calls, making it ideal for edge deployment or browser environments. ## Section: Extending the Project [section_id: extending-the-project] [section_url: https://zigbook.net/chapters/42__project-wasi-build-and-run#extending-the-project] This template serves as a foundation for more complex CLI tools targeting WASI: - JSON output: Emit structured results using `std.json.stringify`, enabling downstream processing by other tools (see json.zig (https://github.com/ziglang/zig/tree/master/lib/std/json.zig)). - Streaming from stdin: The current implementation already handles stdin efficiently by reading all content at once, suitable for logs up to 10MB with the current limit (see 28 (28__filesystem-and-io.xml)). - Multi-format support: Accept different log formats (JSON, syslog, custom) and detect them automatically based on content patterns. - HTTP frontend: Package the WASI module for use in a serverless function that accepts logs via POST and returns JSON summaries (see 31 (31__networking-http-and-json.xml)). ## Section: Notes & Caveats [section_id: notes-caveats] [section_url: https://zigbook.net/chapters/42__project-wasi-build-and-run#notes-caveats] - WASI preview1 (current snapshot) lacks networking, threading, and has limited filesystem features. Stdin/stdout work reliably, but file access requires runtime-specific capability grants. - The `zig libc` effort introduced in 0.15.2 shares implementation between musl and wasi-libc, improving consistency and enabling features like `readToEndAlloc` to work identically across platforms. - WASI runtimes vary in their permission model. Wasmer’s `--mapdir` had issues in testing, while stdin piping works universally. Design CLI tools to prefer stdin when targeting WASI. ## Section: Exercises [section_id: exercises] [section_url: https://zigbook.net/chapters/42__project-wasi-build-and-run#exercises] - Add a `--format json` flag that emits `{"info": N, "warn": N, "error": N}` instead of the plaintext summary, then validate the output by piping to `jq`. - Extend `analysis.zig` with a unit test that verifies case-insensitive matching (e.g., "info" and "INFO" both count), demonstrating `std.ascii.eqlIgnoreCase` (see 13 (13__testing-and-leak-detection.xml)). - Create a third build target for `wasm32-freestanding` (no WASI) that exposes the analyzer as an exported function callable from JavaScript via `@export` (see wasm.zig (https://github.com/ziglang/zig/tree/master/lib/std/wasm.zig)). - Benchmark native vs WASI execution time with a large log file (generate 100k lines), comparing startup overhead and throughput (see 40 (40__profiling-optimization-hardening.xml)). ## Section: Caveats, Alternatives, Edge Cases [section_id: caveats-alternatives-edge-cases] [section_url: https://zigbook.net/chapters/42__project-wasi-build-and-run#caveats-alternatives-edge-cases] - If you need threading, WASI preview2 (component model) introduces experimental concurrency primitives. Consult upstream WASI specs for migration paths. - For browser targets, switch to `wasm32-freestanding` and use JavaScript interop (`@export`/`@extern`) instead of WASI syscalls (see 33 (33__c-interop-import-export-abi.xml)). - Some WASI runtimes (e.g., Wasmedge) support non-standard extensions like sockets or GPU access. Stick to preview1 for maximum portability, or document runtime-specific dependencies clearly. # Chapter 43 — Stdlib Index (Map) [chapter_id: 43__stdlib-index] [chapter_slug: stdlib-index] [chapter_number: 43] [chapter_url: https://zigbook.net/chapters/43__stdlib-index] ## Section: Overview [section_id: overview] [section_url: https://zigbook.net/chapters/43__stdlib-index#overview] After constructing portable applications targeting WASI and native platforms, 42 (42__project-wasi-build-and-run.xml) you now stand at the threshold of Zig’s standard library—a curated collection of approximately 70 top-level modules and 30+ directories spanning data structures, I/O, cryptography, compression, and compiler internals. std.zig (https://github.com/ziglang/zig/tree/master/lib/std/std.zig) This chapter serves as your map: a navigational index that orients you within `zig/lib/std/` for version 0.15.2, showing where to find functionality and how the library categories align with the deep-dive chapters ahead. Unlike tutorial chapters, this is a reference guide—no code examples, just organized pointers. Use it to locate modules quickly, understand the stdlib’s taxonomy, and jump to the detailed treatment in Chapters 44–52. 44 (44__collections-and-algorithms.xml) Think of it as the table of contents for Zig’s batteries-included philosophy. ## Section: Learning Goals [section_id: learning-goals] [section_url: https://zigbook.net/chapters/43__stdlib-index#learning-goals] - Navigate the physical layout of `zig/lib/std/` with confidence, distinguishing top-level files from subdirectories. std (https://github.com/ziglang/zig/tree/master/lib/std/) - Identify which stdlib components handle collections, I/O, formatting, compression, time, processes, and debugging. 44 (44__collections-and-algorithms.xml) - Recognize the relationship between a module’s filesystem path and its import name (e.g., `std.ArrayList` maps to `array_list.zig`). array_list.zig (https://github.com/ziglang/zig/tree/master/lib/std/array_list.zig) - Use this index as a springboard to detailed API chapters without getting lost in 100+ source files. 44 (44__collections-and-algorithms.xml) ## Section: Stdlib at a Glance [section_id: stdlib-at-a-glance] [section_url: https://zigbook.net/chapters/43__stdlib-index#stdlib-at-a-glance] The standard library ships as part of the Zig compiler installation and resides at `zig/lib/std/`. Its root directory contains approximately: - 70 files providing individual modules (e.g., `array_list.zig`, `json.zig`, `log.zig`) - 30+ subdirectories grouping related functionality (e.g., `crypto/`, `compress/`, `fs/`, `math/`) - 1 entry point: `std.zig`, which re-exports the public API surface When you write `@import("std")`, the compiler loads `std.zig`, which then executes `pub const ArrayList = @import("array_list.zig")` and similar for each public symbol. This flat-namespace design means `std.ArrayList` and `std.json` sit at the same level, even though some implementations span multiple files in subdirectories. ## Section: Directory Tree (Selective View) [section_id: directory-tree] [section_url: https://zigbook.net/chapters/43__stdlib-index#directory-tree] Below is an ASCII tree showing key directories and representative files. This is not exhaustive (some directories contain 20+ files), but it highlights landmarks for orientation. ```text graph TB subgraph "std namespace" STD["std (std.zig)"] end subgraph "Core Utilities" MEM["mem.zig
Allocator, zeroes, copy"] META["meta.zig
Type introspection"] MATH["math.zig
Constants, operations"] FMT["fmt.zig
format, parseInt"] end subgraph "OS Abstraction" OSMOD["os.zig
environ, argv, getFdPath"] POSIX["posix.zig
open, read, write, mmap"] LINUX["os/linux.zig
syscall0-6, clone"] WINDOWS["os/windows.zig
OpenFile, CreatePipe"] end subgraph "Filesystem" FS["fs.zig
cwd, max_path_bytes"] DIR["fs/Dir.zig
openFile, makeDir"] FILE["fs/File.zig
read, write, stat"] end subgraph "Memory Management" HEAP["heap.zig
page_size_min"] ARENA["heap/arena_allocator.zig"] DEBUG_ALLOC["heap/debug_allocator.zig
DebugAllocator"] end subgraph "Debugging" DEBUG["debug.zig
SelfInfo, captureCurrentStackTrace"] DWARF["debug/Dwarf.zig"] PDB["debug/Pdb.zig"] CPU_CTX["debug/cpu_context.zig
Native"] end subgraph "C Interop" C["c.zig
timespec, fd_t, mode_t"] C_DARWIN["c/darwin.zig"] C_FREEBSD["c/freebsd.zig"] end subgraph "Testing & Process" TESTING["testing.zig
allocator, expect"] PROCESS["process.zig
getCwd, exit"] PROGRESS["Progress.zig
Terminal updates"] end subgraph "Network & Other" NET["net.zig
Address, parseIp"] UNICODE["unicode.zig
utf8ToUtf16Le"] COFF["coff.zig
PE format"] end STD --> MEM STD --> META STD --> MATH STD --> FMT STD --> OSMOD STD --> POSIX STD --> FS STD --> HEAP STD --> DEBUG STD --> C STD --> TESTING STD --> PROCESS STD --> NET STD --> UNICODE FS --> DIR FS --> FILE HEAP --> ARENA HEAP --> DEBUG_ALLOC DEBUG --> DWARF DEBUG --> PDB DEBUG --> CPU_CTX OSMOD --> LINUX OSMOD --> WINDOWS C --> C_DARWIN C --> C_FREEBSD POSIX --> OSMOD FS --> POSIX NET --> POSIX PROCESS --> POSIX ``` ```text std/ ──► Build/ ──► Cache.zig, Fuzz.zig, Module.zig, Step.zig, Watch.zig, WebServer.zig, abi.zig │ ├──► Cache/ ──► DepTokenizer.zig, Directory.zig, Path.zig │ ├──► Step/ ──► CheckFile.zig, CheckObject.zig, Compile.zig, ConfigHeader.zig, Fail.zig, │ Fmt.zig, InstallArtifact.zig, InstallDir.zig, InstallFile.zig, ObjCopy.zig, │ Options.zig, RemoveDir.zig, Run.zig, TranslateC.zig, UpdateSourceFiles.zig, WriteFile.zig │ └──► Watch/ ──► FsEvents.zig └──► Io/ ──► DeprecatedReader.zig, DeprecatedWriter.zig, Reader.zig, Writer.zig, counting_reader.zig │ ├──► fixed_buffer_stream.zig, test.zig, tty.zig │ └──► Reader/ ──► Limited.zig, test.zig └──► Random/ ──► Ascon.zig, ChaCha.zig, Isaac64.zig, Pcg.zig, RomuTrio.zig, Sfc64.zig, SplitMix64.zig │ ├──► Xoroshiro128.zig, Xoshiro256.zig, benchmark.zig, test.zig, ziggurat.zig └──► Target/ ──► Query.zig, aarch64.zig, amdgcn.zig, arc.zig, arm.zig, avr.zig, bpf.zig, csky.zig │ ├──► generic.zig, hexagon.zig, lanai.zig, loongarch.zig, m68k.zig, mips.zig, msp430.zig │ ├──► nvptx.zig, powerpc.zig, propeller.zig, riscv.zig, s390x.zig, sparc.zig, spirv.zig │ ├──► ve.zig, wasm.zig, x86.zig, xcore.zig, xtensa.zig └──► Thread/ ──► Condition.zig, Futex.zig, Pool.zig, ResetEvent.zig, RwLock.zig, Semaphore.zig, WaitGroup.zig │ ├──► Mutex/ ──► Recursive.zig │ └──► Mutex.zig └──► builtin/ ──► assembly.zig └──► c/ ──► darwin.zig, dragonfly.zig, freebsd.zig, haiku.zig, netbsd.zig, openbsd.zig, serenity.zig, solaris.zig └──► compress/ ──► flate.zig, lzma.zig, lzma2.zig, xz.zig, zstd.zig │ ├──► flate/ ──► BlockWriter.zig, Compress.zig, Decompress.zig, HuffmanEncoder.zig, Lookup.zig, Token.zig, testdata/ │ ├──► lzma/ ──► decode/, decode.zig, test.zig, testdata/, vec2d.zig │ ├──► lzma2/ ──► decode.zig │ ├──► xz/ ──► block.zig, test.zig, testdata/ │ └──► zstd/ ──► Decompress.zig └──► crypto/ ──► Certificate.zig, Sha1.zig, aegis.zig, aes.zig, aes_gcm.zig, aes_ocb.zig, argon2.zig, ascon.zig, bcrypt.zig │ ├──► benchmark.zig, blake2.zig, blake3.zig, chacha20.zig, cmac.zig, codecs.zig, ecdsa.zig, errors.zig, ff.zig │ ├──► ghash_polyval.zig, hash_composition.zig, hkdf.zig, hmac.zig, isap.zig, keccak_p.zig, md5.zig, ml_kem.zig │ ├──► modes.zig, pbkdf2.zig, phc_encoding.zig, poly1305.zig, salsa20.zig, scrypt.zig, sha2.zig, sha3.zig │ ├──► siphash.zig, test.zig, timing_safe.zig, tlcsprng.zig, tls.zig │ ├──► 25519/ ──► curve25519.zig, ed25519.zig, edwards25519.zig, field.zig, ristretto255.zig, scalar.zig, x25519.zig │ ├──► aes/ ──► aesni.zig, armcrypto.zig, soft.zig │ ├──► codecs/ ──► asn1/, asn1.zig, base64_hex_ct.zig │ ├──► pcurves/ ──► common.zig, p256/, p256.zig, p384/, p384.zig, secp256k1/, secp256k1.zig, tests/ │ └──► tls/ ──► Client.zig └──► debug/ ──► Coverage.zig, Dwarf.zig, FixedBufferReader.zig, Info.zig, MemoryAccessor.zig, Pdb.zig, SelfInfo.zig │ ├──► no_panic.zig, simple_panic.zig │ └──► Dwarf/ ──► abi.zig, call_frame.zig, expression.zig └──► dwarf/ ──► AT.zig, ATE.zig, EH.zig, FORM.zig, LANG.zig, OP.zig, TAG.zig └──► fmt/ ──► float.zig, parse_float.zig │ ├──► parse_float/ ──► FloatInfo.zig, FloatStream.zig, common.zig, convert_eisel_lemire.zig │ ├──► convert_fast.zig, convert_hex.zig, convert_slow.zig, decimal.zig, parse.zig └──► fs/ ──► AtomicFile.zig, Dir.zig, File.zig, get_app_data_dir.zig, path.zig, test.zig, wasi.zig └──► hash/ ──► Adler32.zig, auto_hash.zig, benchmark.zig, cityhash.zig, fnv.zig, murmur.zig, verify.zig, wyhash.zig │ └──► xxhash.zig │ └──► crc/ ──► crc.zig, impl.zig, test.zig └──► heap/ ──► FixedBufferAllocator.zig, PageAllocator.zig, SmpAllocator.zig, ThreadSafeAllocator.zig, WasmAllocator.zig │ ├──► arena_allocator.zig, debug_allocator.zig, memory_pool.zig, sbrk_allocator.zig └──► http/ ──► ChunkParser.zig, Client.zig, HeadParser.zig, HeaderIterator.zig, Server.zig, test.zig └──► json/ ──► JSONTestSuite_test.zig, Scanner.zig, Stringify.zig, dynamic.zig, dynamic_test.zig │ ├──► hashmap.zig, hashmap_test.zig, scanner_test.zig, static.zig, static_test.zig, test.zig └──► math/ ──► acos.zig, acosh.zig, asin.zig, asinh.zig, atan.zig, atan2.zig, atanh.zig, cbrt.zig, copysign.zig │ ├──► cosh.zig, expm1.zig, expo2.zig, float.zig, frexp.zig, gamma.zig, gcd.zig, hypot.zig │ ├──► ilogb.zig, isfinite.zig, isinf.zig, isnan.zig, isnormal.zig, iszero.zig, lcm.zig, ldexp.zig, log.zig │ ├──► log10.zig, log1p.zig, log2.zig, log_int.zig, modf.zig, nextafter.zig, pow.zig, powi.zig │ ├──► scalbn.zig, signbit.zig, sinh.zig, sqrt.zig, tanh.zig │ ├──► big/ ──► int.zig, int_test.zig │ └──► complex/ ──► abs.zig, acos.zig, acosh.zig, arg.zig, asin.zig, asinh.zig, atan.zig, atanh.zig └──► conj.zig, cos.zig, cosh.zig, exp.zig, ldexp.zig, log.zig, pow.zig, proj.zig, sin.zig └──► sinh.zig, sqrt.zig, tan.zig, tanh.zig └──► mem/ ──► Allocator.zig └──► meta/ ──► trailer_flags.zig └──► net/ ──► test.zig └──► os/ ──► emscripten.zig, freebsd.zig, linux.zig, plan9.zig, uefi.zig, wasi.zig, windows.zig │ ├──► linux/ ──► IoUring.zig, aarch64.zig, arm.zig, bpf.zig, bpf/, hexagon.zig, io_uring_sqe.zig, ioctl.zig │ ├──► loongarch64.zig, m68k.zig, mips.zig, mips64.zig, powerpc.zig, powerpc64.zig │ ├──► riscv32.zig, riscv64.zig, s390x.zig, seccomp.zig, sparc64.zig, syscalls.zig │ ├──► test.zig, thumb.zig, tls.zig, vdso.zig, x86.zig, x86_64.zig │ └──► plan9/ ──► x86_64.zig │ └──► uefi/ ──► device_path.zig, hii.zig, pool_allocator.zig, protocol.zig, protocol/, status.zig │ └──► windows/ ──► advapi32.zig, crypt32.zig, kernel32.zig, lang.zig, nls.zig, ntdll.zig └──► ntstatus.zig, sublang.zig, test.zig, tls.zig, win32error.zig, ws2_32.zig └──► posix/ ──► test.zig └──► process/ ──► Child.zig └──► sort/ ──► block.zig, pdq.zig └──► testing/ ──► FailingAllocator.zig └──► time/ ──► epoch.zig └──► unicode/ ──► (empty, API in `unicode.zig`) └──► valgrind/ ──► cachegrind.zig, callgrind.zig, memcheck.zig └──► tar/ ──► Writer.zig, test.zig └──► testdata/ └──► tz/ ──► (empty) └──► zon/ ──► Serializer.zig, parse.zig, stringify.zig └──► zig/ ──► Ast.zig, AstGen.zig, AstRlAnnotate.zig, BuiltinFn.zig, Client.zig, ErrorBundle.zig │ ├──► LibCDirs.zig, LibCInstallation.zig, Parse.zig, Server.zig, WindowsSdk.zig │ ├──► Zir.zig, Zoir.zig, ZonGen.zig, c_builtins.zig, c_translation.zig │ ├──► number_literal.zig, parser_test.zig, perf_test.zig, primitives.zig │ ├──► string_literal.zig, system.zig, target.zig, tokenizer.zig │ ├──► Ast/ ──► Render.zig │ ├──► llvm/ ──► BitcodeReader.zig, Builder.zig, bitcode_writer.zig, ir.zig │ └──► system/ ──► NativePaths.zig, arm.zig, darwin.zig, linux.zig, windows.zig, x86.zig └──► darwin/ ``` TIP: Directories like `crypto/`, `compress/`, and `math/` contain dozens of specialized files. The top-level `.zig` file (e.g., `crypto.zig`) typically re-exports the subdirectory’s public API, so you can write `std.crypto.aes` rather than navigating the internal structure. crypto.zig (https://github.com/ziglang/zig/tree/master/lib/std/crypto.zig) ## Section: Module Categories [section_id: module-categories] [section_url: https://zigbook.net/chapters/43__stdlib-index#module-categories] The following sections group stdlib functionality by purpose, mapping each category to its detailed chapter. Use this as a quick-reference guide when you know what you need but not where it lives. ### Subsection: Collections and Algorithms [section_id: collections] [section_url: https://zigbook.net/chapters/43__stdlib-index#collections] Covered in:Chapter 44 (44__collections-and-algorithms.xml) | Module/Path | Purpose | | --- | --- | | `std.ArrayList` | Dynamic array (vector); see `array_list.zig` | | `std.MultiArrayList` | Structure-of-arrays layout; see `multi_array_list.zig` | | `std.HashMap` / `AutoHashMap` / `ArrayHashMap` | Hash tables with various storage strategies; see `hash_map.zig`, `array_hash_map.zig` | | `std.StaticStringMap` | Compile-time perfect hash map for string keys; see `static_string_map.zig` | | `std.DoublyLinkedList` | Intrusive doubly-linked list; see `DoublyLinkedList.zig` | | `std.SinglyLinkedList` | Intrusive singly-linked list; see `SinglyLinkedList.zig` | | `std.SegmentedList` | Segmented list maintaining stable pointers; see `segmented_list.zig` | | `std.Treap` | Treap (tree + heap); see `treap.zig` | | `std.PriorityQueue` | Min/max heap; see `priority_queue.zig` | | `std.PriorityDequeue` | Double-ended priority queue; see `priority_dequeue.zig` | | `std.sort` | Sorting algorithms (pdqsort, block sort); see `sort/` | ### Subsection: Text, Formatting, and Unicode [section_id: text-formatting] [section_url: https://zigbook.net/chapters/43__stdlib-index#text-formatting] Covered in:Chapter 45 (45__text-formatting-and-unicode.xml) | Module/Path | Purpose | | --- | --- | | `std.fmt` | Formatting API: `format`, `parseInt`, `parseFloat`; see `fmt/` | | `std.ascii` | ASCII character utilities; see `ascii.zig` | | `std.unicode` | Unicode operations (UTF-8, UTF-16); see `unicode/` | | `std.base64` | Base64 encoding/decoding; see `base64.zig` | | `std.leb128` | LEB128 encoding; see `leb128.zig` | ### Subsection: I/O and Stream Adapters [section_id: io-streams] [section_url: https://zigbook.net/chapters/43__stdlib-index#io-streams] Covered in:Chapter 46 (46__io-and-stream-adapters.xml) | Module/Path | Purpose | | --- | --- | | `std.io` (now `std.Io`) | Reader/Writer interfaces; see `Io.zig`, `Io/` | | `std.fs` | Filesystem operations: `Dir`, `File`, `path`; see `fs/` | | `std.io.fixedBufferStream` | Fixed-buffer stream adapter; see `Io/fixed_buffer_stream.zig` | | `std.io.countingReader` | Counting reader adapter; see `Io/counting_reader.zig` | ### Subsection: Time, Logging, and Progress [section_id: time-logging-progress] [section_url: https://zigbook.net/chapters/43__stdlib-index#time-logging-progress] Covered in:Chapter 47 (47__time-logging-and-progress.xml) | Module/Path | Purpose | | --- | --- | | `std.time` | Timekeeping, sleep, clocks; see `time/` | | `std.log` | Logging framework with levels; see `log.zig` | | `std.Progress` | Progress reporting; see `Progress.zig` | | `std.tz` | Time zone data; see `tz/` | ### Subsection: Process and Environment [section_id: process-environment] [section_url: https://zigbook.net/chapters/43__stdlib-index#process-environment] Covered in:Chapter 48 (48__process-and-environment.xml) | Module/Path | Purpose | | --- | --- | | `std.process` | Args, environment, spawning child processes; see `process/` | | `std.posix` | POSIX wrappers; see `posix/` | | `std.os` | Platform-specific OS interfaces; see `os/` | ### Subsection: Compression and Archives [section_id: compression-archives] [section_url: https://zigbook.net/chapters/43__stdlib-index#compression-archives] Covered in:Chapter 49 (49__compression-and-archives.xml) | Module/Path | Purpose | | --- | --- | | `std.compress` | Compression: flate, lzma, lzma2, zstd; see `compress/` | | `std.tar` | TAR archive reading/writing; see `tar/` | | `std.zip` | ZIP archive support; see `zip.zig` | ### Subsection: Random, Math, and Hashing [section_id: random-math-hashing] [section_url: https://zigbook.net/chapters/43__stdlib-index#random-math-hashing] Covered in:Chapter 50 (50__random-and-math.xml) | Module/Path | Purpose | | --- | --- | | `std.Random` | PRNGs: ChaCha, PCG, Ascon; see `Random/` | | `std.math` | Math functions (trig, exp, log, etc.); see `math/` | | `std.hash` | Non-crypto hashing: CRC, Adler32, auto_hash; see `hash/` | | `std.crypto` | Cryptographic primitives: AES, Ed25519, SHA-2, AEGIS; see `crypto/` | ### Subsection: Mem and Meta Utilities [section_id: mem-meta] [section_url: https://zigbook.net/chapters/43__stdlib-index#mem-meta] Covered in:Chapter 51 (51__mem-and-meta-utilities.xml) | Module/Path | Purpose | | --- | --- | | `std.mem` | Memory utilities: split, tokenize, copy, search; see `mem.zig`, `mem/` | | `std.meta` | Type introspection helpers; see `meta/`, `meta.zig` | ### Subsection: Debug and Valgrind [section_id: debug-valgrind] [section_url: https://zigbook.net/chapters/43__stdlib-index#debug-valgrind] Covered in:Chapter 52 (52__debug-and-valgrind.xml) | Module/Path | Purpose | | --- | --- | | `std.debug` | Panic, stack traces, DWARF info; see `debug/` | | `std.valgrind` | Valgrind integration (memcheck, cachegrind); see `valgrind/` | ## Section: Specialized Modules (Not Covered in Detail) [section_id: specialized-modules] [section_url: https://zigbook.net/chapters/43__stdlib-index#specialized-modules] Some stdlib components are highly specialized or primarily internal to the compiler. They appear in the directory tree but aren’t the focus of the upcoming chapters: | Module/Path | Purpose | | --- | --- | | `std.Build` | Build system internals; see `Build/`, covered in Chapter 22 (22__build-system-deep-dive.xml) | | `std.zig` | Compiler AST, codegen; see `zig/`; advanced topic | | `std.zon` | ZON parser/serializer; see `zon/` | | `std.Target` | Target architecture metadata; see `Target/` | | `std.SemanticVersion` | Semantic versioning; see `SemanticVersion.zig` | | `std.Uri` | URI parsing; see `Uri.zig` | | `std.wasm` | WebAssembly constants; see `wasm.zig` | | `std.elf`, `std.macho`, `std.coff`, `std.pdb` | Binary format parsers; see respective `.zig` files | | `std.dwarf` | DWARF constants; see `dwarf/` | | `std.Thread` | Threading primitives; touched in Chapter 29 (29__threads-and-atomics.xml) | | `std.atomic`, `std.once` | Atomic operations, one-time initialization; see Chapter 29 (29__threads-and-atomics.xml) | | `std.http`, `std.net`, `std.json` | Networking and HTTP; covered in Chapter 31 (31__networking-http-and-json.xml) | ## Section: Import Conventions [section_id: import-conventions] [section_url: https://zigbook.net/chapters/43__stdlib-index#import-conventions] Zig’s stdlib uses a flat namespace at the top level: `std.ArrayList`, `std.HashMap`, `std.json`, etc. Internally, some modules split across subdirectories (`crypto/`, `compress/`), but the public API is re-exported through a single `.zig` file in the root. Examples: - `std.crypto.aes` → defined in `crypto/aes/`, re-exported by `crypto.zig` - `std.compress.flate` → defined in `compress/flate/`, re-exported by `compress.zig` - `std.fs.Dir` → defined in `fs/Dir.zig`, re-exported by `fs.zig` TIP: When in doubt, check `std.zig` for the canonical export name, then trace it to the implementing file. ## Section: Navigating the Source [section_id: navigating-source] [section_url: https://zigbook.net/chapters/43__stdlib-index#navigating-source] To explore the stdlib yourself: 1. Locate your Zig installation: Run `zig env` and note `lib_dir`. #Command-line-flags (https://ziglang.org/documentation/master/#Command-line-flags) 2. Browse : Use `ls`, `tree`, or your editor’s file explorer. 3. Read top-level files first: `std.zig` shows what’s public; individual `.zig` files contain implementation. 4. Check subdirectories for depth: Directories like `crypto/`, `compress/`, `math/` house specialized code. crypto (https://github.com/ziglang/zig/tree/master/lib/std/crypto/) TIP: The stdlib is written in Zig, so reading its source is an excellent way to learn idiomatic patterns—especially error handling, allocator threading, and comptime tricks. ## Section: What’s Next [section_id: whats-next] [section_url: https://zigbook.net/chapters/43__stdlib-index#whats-next] This index orients you, but the real learning happens in the detailed chapters: - Chapter 44: Collections and algorithms—`ArrayList`, `HashMap`, `PriorityQueue`, sorting. 44 (44__collections-and-algorithms.xml) - Chapter 45: Text, formatting, Unicode—`std.fmt`, `std.ascii`, Base64. 45 (45__text-formatting-and-unicode.xml) - Chapter 46: I/O and streams—`std.Io`, `std.fs`, adapters. 46 (46__io-and-stream-adapters.xml) - Chapter 47: Time, logging, progress—`std.time`, `std.log`, `std.Progress`. 47 (47__time-logging-and-progress.xml) - Chapter 48: Process and environment—`std.process`, `std.posix`. 48 (48__process-and-environment.xml) - Chapter 49: Compression and archives—`std.compress`, `std.tar`, `std.zip`. 49 (49__compression-and-archives.xml) - Chapter 50: Random, math, hashing—`std.Random`, `std.math`, `std.crypto`. 50 (50__random-and-math.xml) - Chapter 51: Mem and meta utilities—`std.mem`, `std.meta`. 51 (51__mem-and-meta-utilities.xml) - Chapter 52: Debug and Valgrind—`std.debug`, `std.valgrind`. 52 (52__debug-and-valgrind.xml) Each chapter provides code examples, API walkthroughs, and practical guidance. Use this index to jump directly to the module you need. ## Section: Notes & Caveats [section_id: notes-caveats] [section_url: https://zigbook.net/chapters/43__stdlib-index#notes-caveats] - This index reflects Zig 0.15.2. Future versions may reorganize modules or rename exports. v0.15.2 (https://ziglang.org/download/0.15.1/release-notes.html) - Not all stdlib components are stable—some are marked experimental or subject to breaking changes between releases. - The compiler’s own internals (`std.zig.*`, `std.Build.*`) are advanced topics. Start with user-facing APIs like collections and I/O before diving into AST manipulation. zig (https://github.com/ziglang/zig/tree/master/lib/std/zig/) ## Section: Exercises [section_id: exercises] [section_url: https://zigbook.net/chapters/43__stdlib-index#exercises] - Run `zig env` and navigate to `lib_dir/std/`, then `ls` the root to see all top-level modules. Pick three you’ve never heard of and read their first 20 lines to understand their purpose. - Open `std.zig` in your editor and trace how `std.ArrayList` is defined. (Hint: `pub const ArrayList = @import("array_list.zig").ArrayList;`) - Grep the stdlib for `pub fn` to count how many public functions exist across all modules. Use this to gauge the stdlib’s scope. - Compare the directory structure of `crypto/` vs. `compress/`. Notice how both contain subdirectories for specific algorithms, illustrating Zig’s modular organization philosophy. ## Section: Caveats, alternatives, edge cases [section_id: caveats-alternatives-edge-cases] [section_url: https://zigbook.net/chapters/43__stdlib-index#caveats-alternatives-edge-cases] - Some modules have both a `.zig` file and a directory (e.g., `fmt.zig` and `fmt/`). The file typically re-exports the directory’s contents or provides a high-level API. fmt.zig (https://github.com/ziglang/zig/tree/master/lib/std/fmt.zig) - The stdlib does not include a package manager or registry integration (that’s external tooling). It focuses on language-level utilities and OS abstractions. - Third-party libraries are not part of `std`. You’ll use `build.zig.zon` and `std.Build.dependency()` to fetch them. # Chapter 44 — Collections and Algorithms [chapter_id: 44__collections-and-algorithms] [chapter_slug: collections-and-algorithms] [chapter_number: 44] [chapter_url: https://zigbook.net/chapters/44__collections-and-algorithms] ## Section: Overview [section_id: overview] [section_url: https://zigbook.net/chapters/44__collections-and-algorithms#overview] With the stdlib index providing your map, you now dive into Zig’s collection types—the workhorses of data manipulation. This chapter explores dynamic arrays (`ArrayList`), hash tables (`HashMap` and variants), priority structures (`PriorityQueue`), linked lists, specialized containers like `MultiArrayList` and `SegmentedList`, and sorting algorithms (see array_list.zig (https://github.com/ziglang/zig/tree/master/lib/std/array_list.zig) and hash_map.zig (https://github.com/ziglang/zig/tree/master/lib/std/hash_map.zig)). Each collection embraces Zig’s explicit allocator model, giving you control over memory lifetime and enabling leak detection during testing. Unlike languages with implicit garbage collection, Zig collections require you to `deinit()` or transfer ownership explicitly. This discipline, combined with the standard library’s rich suite of adapters (unmanaged variants, sentinel-aware slices, custom contexts), makes collections both powerful and predictable. By chapter’s end, you’ll confidently choose the right structure for your use case and understand the performance trade-offs inherent in each design (see sort.zig (https://github.com/ziglang/zig/tree/master/lib/std/sort.zig)). ## Section: Learning Goals [section_id: learning-goals] [section_url: https://zigbook.net/chapters/44__collections-and-algorithms#learning-goals] - Use `ArrayList(T)` for dynamic arrays: append, insert, remove, iterate, and understand reallocation strategies. - Employ `HashMap` and `AutoHashMap` for key-value lookups with custom hash and equality functions. - Leverage `PriorityQueue` for min/max heap operations and understand comparison contexts (see priority_queue.zig (https://github.com/ziglang/zig/tree/master/lib/std/priority_queue.zig)). - Apply `std.sort` for in-place sorting with stable and unstable algorithms (pdqsort, block sort, insertion sort). - Recognize specialized structures: `MultiArrayList` for structure-of-arrays layout, `SegmentedList` for stable pointers, linked lists for intrusive designs (see multi_array_list.zig (https://github.com/ziglang/zig/tree/master/lib/std/multi_array_list.zig) and segmented_list.zig (https://github.com/ziglang/zig/tree/master/lib/std/segmented_list.zig)). - Appreciate allocator impact: how collection growth triggers reallocation and how arenas simplify bulk-free patterns (see 10 (10__allocators-and-memory-management.xml)). ## Section: ArrayList: Dynamic Arrays [section_id: arraylist] [section_url: https://zigbook.net/chapters/44__collections-and-algorithms#arraylist] `ArrayList(T)` is Zig’s foundational growable array, analogous to C++'s `std::vector` or Rust’s `Vec`. It manages a contiguous slice of `T` values, expanding capacity as needed. You interact with `.items` (the current slice) and call methods like `append`, `pop`, `insert`, and `remove`. ### Subsection: Basic Operations [section_id: arraylist-basics] [section_url: https://zigbook.net/chapters/44__collections-and-algorithms#arraylist-basics] Create an `ArrayList` by specifying the element type and passing an allocator. Call `deinit()` when done to free the backing memory. ```zig const std = @import("std"); pub fn main() !void { var gpa = std.heap.GeneralPurposeAllocator(.{}){}; defer _ = gpa.deinit(); const allocator = gpa.allocator(); var list: std.ArrayList(i32) = .empty; defer list.deinit(allocator); try list.append(allocator, 10); try list.append(allocator, 20); try list.append(allocator, 30); for (list.items, 0..) |item, i| { std.debug.print("Item {d}: {d}\n", .{ i, item }); } const popped = list.pop(); std.debug.print("Popped: {d}\n", .{popped.?}); std.debug.print("Remaining length: {d}\n", .{list.items.len}); } ``` Build: ```shell $ zig build-exe arraylist_basic.zig ``` Run: ```shell $ ./arraylist_basic ``` Output: ```shell Item 0: 10 Item 1: 20 Item 2: 30 Popped: 30 Remaining length: 2 ``` TIP: `ArrayList` doubles capacity when full (exponential growth), amortizing reallocation cost. You can pre-allocate with `try list.ensureTotalCapacity(allocator, n)` if you know the final size. ### Subsection: Ownership and Unmanaged Variants [section_id: arraylist-ownership] [section_url: https://zigbook.net/chapters/44__collections-and-algorithms#arraylist-ownership] By default, `ArrayList(T)` stores its allocator internally (managed variant). For more explicit control, use the unmanaged form by accessing `.items` and `.capacity` directly, or use the deprecated `Unmanaged` APIs. The modern pattern is to use the simpler managed form unless you need to decouple allocation from the list itself. ```zig const std = @import("std"); pub fn main() !void { var gpa = std.heap.GeneralPurposeAllocator(.{}){}; defer _ = gpa.deinit(); const allocator = gpa.allocator(); // ArrayList with explicit allocator var list: std.ArrayList(u32) = .empty; defer list.deinit(allocator); try list.append(allocator, 1); try list.append(allocator, 2); try list.append(allocator, 3); std.debug.print("Managed list length: {d}\n", .{list.items.len}); // Transfer ownership to a slice const owned_slice = try list.toOwnedSlice(allocator); defer allocator.free(owned_slice); std.debug.print("After transfer, original list length: {d}\n", .{list.items.len}); std.debug.print("Owned slice length: {d}\n", .{owned_slice.len}); } ``` Build and Run: ```shell $ zig build-exe arraylist_ownership.zig && ./arraylist_ownership ``` Output: ```shell Managed list length: 3 After transfer, original list length: 0 Owned slice length: 3 ``` NOTE: `toOwnedSlice()` empties the list and returns the backing memory as a slice—you become responsible for freeing it with `allocator.free(slice)`. ### Subsection: Insertion and Removal [section_id: arraylist-insertion-removal] [section_url: https://zigbook.net/chapters/44__collections-and-algorithms#arraylist-insertion-removal] Beyond `append` and `pop`, `ArrayList` supports mid-array operations. `orderedRemove` maintains element order (shifts subsequent elements), while `swapRemove` is O(1) but doesn’t preserve order (swaps with the last element). ```zig const std = @import("std"); pub fn main() !void { var gpa = std.heap.GeneralPurposeAllocator(.{}){}; defer _ = gpa.deinit(); const allocator = gpa.allocator(); var list: std.ArrayList(i32) = .empty; defer list.deinit(allocator); try list.appendSlice(allocator, &.{ 1, 2, 3, 4 }); // Insert 99 at index 1 try list.insert(allocator, 1, 99); std.debug.print("After insert at 1: {any}\n", .{list.items}); // Remove at index 2 (shifts elements) _ = list.orderedRemove(2); std.debug.print("After orderedRemove at 2: {any}\n", .{list.items}); // Remove at index 1 (swaps with last, no shift) _ = list.swapRemove(1); std.debug.print("After swapRemove at 1: {any}\n", .{list.items}); } ``` Build and Run: ```shell $ zig build-exe arraylist_insert_remove.zig && ./arraylist_insert_remove ``` Output: ```shell After insert at 1: [1, 99, 2, 3, 4] After orderedRemove at 2: [1, 99, 3, 4] After swapRemove at 1: [1, 4, 3] ``` IMPORTANT: `orderedRemove` is O(n) in the worst case (removing the first element requires shifting all others); use `swapRemove` when order doesn’t matter for O(1) performance. ## Section: HashMap: Key-Value Lookups [section_id: hashmap] [section_url: https://zigbook.net/chapters/44__collections-and-algorithms#hashmap] Zig’s hash map family provides O(1) average-case lookups via open addressing and linear probing. `HashMap(K, V, Context, max_load_percentage)` requires a context with `hash` and `eql` functions. For convenience, `AutoHashMap` auto-generates these for hashable types, and `StringHashMap` specializes for `[]const u8` keys. ### Subsection: StringHashMap Basics [section_id: hashmap-basic] [section_url: https://zigbook.net/chapters/44__collections-and-algorithms#hashmap-basic] For string keys (`[]const u8`), use `StringHashMap(V)` which provides optimized string hashing. Note that `AutoHashMap` does not support slice types like `[]const u8` to avoid ambiguity—use `StringHashMap` instead. ```zig const std = @import("std"); pub fn main() !void { var gpa = std.heap.GeneralPurposeAllocator(.{}){}; defer _ = gpa.deinit(); const allocator = gpa.allocator(); var map = std.StringHashMap(i32).init(allocator); defer map.deinit(); try map.put("foo", 42); try map.put("bar", 100); if (map.get("foo")) |value| { std.debug.print("Value for 'foo': {d}\n", .{value}); } std.debug.print("Contains 'bar': {}\n", .{map.contains("bar")}); std.debug.print("Contains 'baz': {}\n", .{map.contains("baz")}); _ = map.remove("foo"); std.debug.print("After removing 'foo', contains: {}\n", .{map.contains("foo")}); } ``` Build and Run: ```shell $ zig build-exe hashmap_basic.zig && ./hashmap_basic ``` Output: ```shell Value for 'foo': 42 Contains 'bar': true Contains 'baz': false After removing 'foo', contains: false ``` TIP: Use `put` to insert or update, `get` to retrieve (returns `?V`), and `remove` to delete. Check existence with `contains` without retrieving the value. ### Subsection: StringHashMap for String Keys [section_id: hashmap-string] [section_url: https://zigbook.net/chapters/44__collections-and-algorithms#hashmap-string] When keys are `[]const u8`, use `StringHashMap(V)` for optimized string hashing. Remember: the map doesn’t copy key memory—you must ensure strings outlive the map or use an arena allocator. ```zig const std = @import("std"); pub fn main() !void { var gpa = std.heap.GeneralPurposeAllocator(.{}){}; defer _ = gpa.deinit(); const allocator = gpa.allocator(); var population = std.StringHashMap(u32).init(allocator); defer population.deinit(); try population.put("Seattle", 750_000); try population.put("Austin", 950_000); try population.put("Boston", 690_000); var iter = population.iterator(); while (iter.next()) |entry| { std.debug.print("City: {s}, Population: {d}\n", .{ entry.key_ptr.*, entry.value_ptr.* }); } } ``` Build and Run: ```shell $ zig build-exe hashmap_string.zig && ./hashmap_string ``` Output: ```shell City: Seattle, Population: 750000 City: Austin, Population: 950000 City: Boston, Population: 690000 ``` IMPORTANT: String keys are not duplicated by the map—if you pass stack-allocated or temporary strings, they must remain valid. Use an arena allocator or `dupe` to manage key lifetimes. ### Subsection: Custom Hash and Equality [section_id: hashmap-custom] [section_url: https://zigbook.net/chapters/44__collections-and-algorithms#hashmap-custom] For types not supported by `autoHash`, define a context with custom `hash` and `eql` functions. ```zig const std = @import("std"); const Point = struct { x: i32, y: i32, }; const PointContext = struct { pub fn hash(self: @This(), p: Point) u64 { _ = self; var hasher = std.hash.Wyhash.init(0); std.hash.autoHash(&hasher, p.x); std.hash.autoHash(&hasher, p.y); return hasher.final(); } pub fn eql(self: @This(), a: Point, b: Point) bool { _ = self; return a.x == b.x and a.y == b.y; } }; pub fn main() !void { var gpa = std.heap.GeneralPurposeAllocator(.{}){}; defer _ = gpa.deinit(); const allocator = gpa.allocator(); var map = std.HashMap(Point, []const u8, PointContext, 80).init(allocator); defer map.deinit(); try map.put(.{ .x = 10, .y = 20 }, "Alice"); try map.put(.{ .x = 30, .y = 40 }, "Bob"); var iter = map.iterator(); while (iter.next()) |entry| { std.debug.print("Point({d}, {d}): {s}\n", .{ entry.key_ptr.x, entry.key_ptr.y, entry.value_ptr.* }); } std.debug.print("Contains (10, 20): {}\n", .{map.contains(.{ .x = 10, .y = 20 })}); } ``` Build and Run: ```shell $ zig build-exe hashmap_custom.zig && ./hashmap_custom ``` Output: ```shell Point(10, 20): Alice Point(30, 40): Bob Contains (10, 20): true ``` NOTE: The context parameter in `HashMap(K, V, Context, max_load_percentage)` allows stateful hashing (e.g., salted hashes). For stateless contexts, pass `void`. ## Section: PriorityQueue: Heap-Based Priority Structures [section_id: priorityqueue] [section_url: https://zigbook.net/chapters/44__collections-and-algorithms#priorityqueue] `PriorityQueue(T, Context, compareFn)` implements a binary min-heap or max-heap depending on your comparison function. It supports `add`, `peek`, `remove` (pop the top element), and `removeIndex`. ### Subsection: Min-Heap Example [section_id: priorityqueue-basic] [section_url: https://zigbook.net/chapters/44__collections-and-algorithms#priorityqueue-basic] A min-heap pops the smallest element first. The comparison function returns `.lt` when the first argument should come before the second. ```zig const std = @import("std"); fn lessThan(context: void, a: i32, b: i32) std.math.Order { _ = context; return std.math.order(a, b); } pub fn main() !void { var gpa = std.heap.GeneralPurposeAllocator(.{}){}; defer _ = gpa.deinit(); const allocator = gpa.allocator(); var queue = std.PriorityQueue(i32, void, lessThan).init(allocator, {}); defer queue.deinit(); try queue.add(10); try queue.add(5); try queue.add(20); try queue.add(1); while (queue.removeOrNull()) |item| { std.debug.print("Popped: {d}\n", .{item}); } } ``` Build and Run: ```shell $ zig build-exe priorityqueue_min.zig && ./priorityqueue_min ``` Output: ```shell Popped: 1 Popped: 5 Popped: 10 Popped: 20 ``` TIP: For a max-heap, reverse the comparison logic: return `.gt` when `a < b`. ### Subsection: Priority Queue for Task Scheduling [section_id: priorityqueue-tasks] [section_url: https://zigbook.net/chapters/44__collections-and-algorithms#priorityqueue-tasks] Priority queues excel at scheduling: add tasks with priorities, then always process the highest-priority task first. ```zig const std = @import("std"); const Task = struct { name: []const u8, priority: u32, }; fn compareTasks(context: void, a: Task, b: Task) std.math.Order { _ = context; // Higher priority comes first (max-heap behavior) return std.math.order(b.priority, a.priority); } pub fn main() !void { var gpa = std.heap.GeneralPurposeAllocator(.{}){}; defer _ = gpa.deinit(); const allocator = gpa.allocator(); var queue = std.PriorityQueue(Task, void, compareTasks).init(allocator, {}); defer queue.deinit(); try queue.add(.{ .name = "Documentation", .priority = 1 }); try queue.add(.{ .name = "Feature request", .priority = 5 }); try queue.add(.{ .name = "Critical bug", .priority = 10 }); while (queue.removeOrNull()) |task| { std.debug.print("Processing: {s} (priority {d})\n", .{ task.name, task.priority }); } } ``` Build and Run: ```shell $ zig build-exe priorityqueue_tasks.zig && ./priorityqueue_tasks ``` Output: ```shell Processing: Critical bug (priority 10) Processing: Feature request (priority 5) Processing: Documentation (priority 1) ``` NOTE: `PriorityQueue` uses a heap internally, so `add` is O(log n), `peek` is O(1), and `remove` is O(log n). ## Section: Sorting [section_id: sorting] [section_url: https://zigbook.net/chapters/44__collections-and-algorithms#sorting] Zig’s `std.sort` module provides multiple algorithms: `insertion` (stable, O(n²)), `heap` (unstable, O(n log n)), `pdq` (pattern-defeating quicksort, O(n log n) worst-case), and `block` (stable, O(n log n) with extra memory). The default recommendation is `pdq` for most use cases. ### Subsection: Basic Sorting [section_id: sort-basic] [section_url: https://zigbook.net/chapters/44__collections-and-algorithms#sort-basic] Call `std.sort.pdq` with a slice, a context, and a `lessThan` function. ```zig const std = @import("std"); fn lessThan(context: void, a: i32, b: i32) bool { _ = context; return a < b; } fn greaterThan(context: void, a: i32, b: i32) bool { _ = context; return a > b; } pub fn main() !void { var numbers = [_]i32{ 5, 2, 8, 1, 10 }; std.sort.pdq(i32, &numbers, {}, lessThan); std.debug.print("Sorted ascending: {any}\n", .{numbers}); std.sort.pdq(i32, &numbers, {}, greaterThan); std.debug.print("Sorted descending: {any}\n", .{numbers}); } ``` Build and Run: ```shell $ zig build-exe sort_basic.zig && ./sort_basic ``` Output: ```shell Sorted ascending: [1, 2, 5, 8, 10] Sorted descending: [10, 8, 5, 2, 1] ``` TIP: `pdq` is unstable but fast. Use `block` if you need stability (equal elements retain their original order). ### Subsection: Sorting Structs [section_id: sort-structs] [section_url: https://zigbook.net/chapters/44__collections-and-algorithms#sort-structs] Sort by struct fields by providing a custom comparison function. ```zig const std = @import("std"); const Person = struct { name: []const u8, age: u32, }; fn byAge(context: void, a: Person, b: Person) bool { _ = context; return a.age < b.age; } pub fn main() !void { var people = [_]Person{ .{ .name = "Alice", .age = 30 }, .{ .name = "Bob", .age = 25 }, .{ .name = "Charlie", .age = 35 }, }; std.sort.pdq(Person, &people, {}, byAge); std.debug.print("Sorted by age:\n", .{}); for (people) |person| { std.debug.print("{s}, age {d}\n", .{ person.name, person.age }); } } ``` Build and Run: ```shell $ zig build-exe sort_structs.zig && ./sort_structs ``` Output: ```shell Sorted by age: Alice, age 30 Bob, age 25 Charlie, age 35 ``` NOTE: The context parameter in sorting functions can hold state (e.g., sort direction flags or comparison modifiers). Use `anytype` for flexibility. ## Section: MultiArrayList: Structure-of-Arrays Layout [section_id: multiarraylist] [section_url: https://zigbook.net/chapters/44__collections-and-algorithms#multiarraylist] `MultiArrayList(T)` stores structs in a structure-of-arrays (SoA) format: each field is stored in its own contiguous array, improving cache locality when accessing individual fields across many elements. ```zig const std = @import("std"); const Entity = struct { id: u32, x: f32, y: f32, }; pub fn main() !void { var gpa = std.heap.GeneralPurposeAllocator(.{}){}; defer _ = gpa.deinit(); const allocator = gpa.allocator(); var entities = std.MultiArrayList(Entity){}; defer entities.deinit(allocator); try entities.append(allocator, .{ .id = 1, .x = 10.5, .y = 20.3 }); try entities.append(allocator, .{ .id = 2, .x = 30.1, .y = 40.7 }); for (0..entities.len) |i| { const entity = entities.get(i); std.debug.print("Entity {d}: id={d}, x={d}, y={d}\n", .{ i, entity.id, entity.x, entity.y }); } // Access a single field slice for efficient iteration const x_coords = entities.items(.x); var sum: f32 = 0; for (x_coords) |x| { sum += x; } std.debug.print("Sum of x coordinates: {d}\n", .{sum}); } ``` Build and Run: ```shell $ zig build-exe multiarraylist.zig && ./multiarraylist ``` Output: ```shell Entity 0: id=1, x=10.5, y=20.3 Entity 1: id=2, x=30.1, y=40.7 Sum of x coordinates: 40.6 ``` TIP: Use `MultiArrayList` when you frequently iterate over a single field (e.g., positions in a game engine) but rarely need the entire struct. This layout maximizes CPU cache efficiency. ## Section: SegmentedList: Stable Pointers [section_id: segmentedlist] [section_url: https://zigbook.net/chapters/44__collections-and-algorithms#segmentedlist] `SegmentedList(T, prealloc_item_count)` grows by allocating fixed-size segments rather than reallocating a single contiguous array. This ensures pointers to elements remain valid across insertions. ```zig const std = @import("std"); pub fn main() !void { var gpa = std.heap.GeneralPurposeAllocator(.{}){}; defer _ = gpa.deinit(); const allocator = gpa.allocator(); var list = std.SegmentedList(i32, 4){}; defer list.deinit(allocator); try list.append(allocator, 10); try list.append(allocator, 20); // Take a pointer to the first element const first_ptr = list.at(0); std.debug.print("First item: {d}\n", .{first_ptr.*}); // Append more items - pointer remains valid! try list.append(allocator, 30); std.debug.print("First item (after append): {d}\n", .{first_ptr.*}); std.debug.print("List length: {d}\n", .{list.len}); } ``` Build and Run: ```shell $ zig build-exe segmentedlist.zig && ./segmentedlist ``` Output: ```shell First item: 10 First item (after append): 10 List length: 3 ``` IMPORTANT: Unlike `ArrayList`, pointers to `SegmentedList` elements remain valid even as you add more items. Use this when you need stable addressing (e.g., storing pointers in other data structures). ## Section: Linked Lists [section_id: linkedlists] [section_url: https://zigbook.net/chapters/44__collections-and-algorithms#linkedlists] Zig provides `DoublyLinkedList(T)` and `SinglyLinkedList(T)` as intrusive linked lists: nodes embed the link pointers directly (see DoublyLinkedList.zig (https://github.com/ziglang/zig/tree/master/lib/std/DoublyLinkedList.zig) and SinglyLinkedList.zig (https://github.com/ziglang/zig/tree/master/lib/std/SinglyLinkedList.zig)). This avoids allocator overhead per node and integrates naturally with existing structs. ```zig const std = @import("std"); const Node = struct { data: i32, link: std.DoublyLinkedList.Node = .{}, }; pub fn main() !void { var gpa = std.heap.GeneralPurposeAllocator(.{}){}; defer _ = gpa.deinit(); const allocator = gpa.allocator(); var list = std.DoublyLinkedList{}; var node1 = try allocator.create(Node); node1.* = .{ .data = 10 }; list.append(&node1.link); var node2 = try allocator.create(Node); node2.* = .{ .data = 20 }; list.append(&node2.link); var node3 = try allocator.create(Node); node3.* = .{ .data = 30 }; list.append(&node3.link); var it = list.first; while (it) |node| : (it = node.next) { const data_node: *Node = @fieldParentPtr("link", node); std.debug.print("Node: {d}\n", .{data_node.data}); } // Clean up allocator.destroy(node1); allocator.destroy(node2); allocator.destroy(node3); } ``` Build and Run: ```shell $ zig build-exe linkedlist.zig && ./linkedlist ``` Output: ```shell Node: 10 Node: 20 Node: 30 ``` NOTE: Intrusive lists don’t own node memory—you allocate and manage nodes yourself. This is powerful but requires discipline to avoid use-after-free bugs. ## Section: Specialized Maps [section_id: specialized-maps] [section_url: https://zigbook.net/chapters/44__collections-and-algorithms#specialized-maps] ### Subsection: ArrayHashMap [section_id: _arrayhashmap] [section_url: https://zigbook.net/chapters/44__collections-and-algorithms#_arrayhashmap] `ArrayHashMap` stores keys and values in separate arrays, preserving insertion order and enabling iteration by index (see array_hash_map.zig (https://github.com/ziglang/zig/tree/master/lib/std/array_hash_map.zig)). ### Subsection: StaticStringMap [section_id: _staticstringmap] [section_url: https://zigbook.net/chapters/44__collections-and-algorithms#_staticstringmap] `StaticStringMap(V)` is a compile-time perfect hash map for string keys—fast lookups with zero runtime allocation or hashing overhead (see static_string_map.zig (https://github.com/ziglang/zig/tree/master/lib/std/static_string_map.zig)). ```zig const std = @import("std"); const status_codes = std.StaticStringMap(u32).initComptime(.{ .{ "ok", 200 }, .{ "created", 201 }, .{ "not_found", 404 }, .{ "server_error", 500 }, }); pub fn main() !void { std.debug.print("Status code for 'ok': {d}\n", .{status_codes.get("ok").?}); std.debug.print("Status code for 'not_found': {d}\n", .{status_codes.get("not_found").?}); std.debug.print("Status code for 'server_error': {d}\n", .{status_codes.get("server_error").?}); } ``` Build and Run: ```shell $ zig build-exe static_string_map.zig && ./static_string_map ``` Output: ```shell Status code for 'ok': 200 Status code for 'not_found': 404 Status code for 'server_error': 500 ``` TIP: Use `StaticStringMap` for compile-time constant mappings (e.g., keyword tables, command parsers). It compiles to optimal switch statements or lookup tables. ## Section: Allocator Impact on Collections [section_id: allocator-impact] [section_url: https://zigbook.net/chapters/44__collections-and-algorithms#allocator-impact] Every collection requires an allocator, either passed at initialization (`ArrayList(T).init(allocator)`) or per operation (unmanaged variants). Growth strategies trigger reallocations, and failure returns `error.OutOfMemory` (see 10 (10__allocators-and-memory-management.xml)). ### Subsection: Arena Pattern for Bulk-Free [section_id: arena-pattern] [section_url: https://zigbook.net/chapters/44__collections-and-algorithms#arena-pattern] When building temporary collections that live for a single scope, use an arena allocator to free everything at once. ```zig const std = @import("std"); pub fn main() !void { var gpa = std.heap.GeneralPurposeAllocator(.{}){}; defer _ = gpa.deinit(); const allocator = gpa.allocator(); var arena = std.heap.ArenaAllocator.init(allocator); defer arena.deinit(); const arena_allocator = arena.allocator(); var list: std.ArrayList(i32) = .empty; for (0..1000) |i| { try list.append(arena_allocator, @intCast(i)); } std.debug.print("List has {d} items\n", .{list.items.len}); var map = std.AutoHashMap(u32, []const u8).init(arena_allocator); for (0..500) |i| { try map.put(@intCast(i), "value"); } std.debug.print("Map has {d} entries\n", .{map.count()}); // No need to call list.deinit() or map.deinit() // arena.deinit() frees everything at once std.debug.print("All freed at once via arena.deinit()\n", .{}); } ``` Build and Run: ```shell $ zig build-exe collections_arena.zig && ./collections_arena ``` Output: ```shell List has 1000 items Map has 500 entries All freed at once via arena.deinit() ``` NOTE: The arena doesn’t call individual collection `deinit()` methods. It frees all memory at once. Use this pattern when you know collections won’t outlive the arena’s scope (see 10 (10__allocators-and-memory-management.xml)). ## Section: Performance Considerations [section_id: performance-considerations] [section_url: https://zigbook.net/chapters/44__collections-and-algorithms#performance-considerations] - ArrayList growth: Doubling capacity amortizes reallocation cost, but large allocations may fail. Pre-allocate if size is known. - HashMap load factor: Default `max_load_percentage` is 80%. Higher values save memory but increase collision chains. - Sort stability: `pdq` is fastest but unstable. Use `block` or `insertion` when order of equal elements matters. - MultiArrayList cache: SoA layout shines when iterating single fields but adds indirection overhead for full-struct access. - SegmentedList segments: Smaller `prealloc_item_count` means more segments (more allocations); larger values waste memory if lists stay small. ## Section: Exercises [section_id: exercises] [section_url: https://zigbook.net/chapters/44__collections-and-algorithms#exercises] - Implement a `FrequencyMap` using `StringHashMap(u32)` that counts word occurrences in a text file, then print the top-10 most frequent words using a `PriorityQueue`. - Compare `ArrayList` vs `SegmentedList` performance: create 10,000 items, take pointers to the first 100, then append 10,000 more. Verify pointers remain valid with `SegmentedList` but may invalidate with `ArrayList`. - Write an `LRU` cache using `HashMap` for lookups and `DoublyLinkedList` for eviction order. When capacity is reached, remove the least-recently-used item. - Sort an `ArrayList` of structs by multiple keys (e.g., sort by `age`, then by `name` for ties) using a custom comparator and `std.sort.pdq`. ## Section: Caveats, Alternatives, Edge Cases [section_id: caveats-alternatives-edge-cases] [section_url: https://zigbook.net/chapters/44__collections-and-algorithms#caveats-alternatives-edge-cases] - Unmanaged variants: Most collections have unmanaged counterparts (e.g., `ArrayListUnmanaged(T)`) for manual allocator threading, useful in generic code or when embedding collections in structs. - HashMap key lifetimes: Maps don’t duplicate keys. Ensure key memory outlives the map, or use an arena allocator to manage key storage collectively. - Iterator invalidation: Like C++, modifying a collection (append, remove) may invalidate iterators or pointers to elements. Always check documentation for each operation. - Stable vs unstable sort: If your data has equal elements that must maintain relative order (e.g., sorting a table by column but preserving row order for ties), use `std.sort.block` or `insertion`, not `pdq`. - Treap: Zig also provides `std.Treap`, a tree-heap hybrid for ordered maps with probabilistic balancing, useful when you need both sorted iteration and O(log n) operations (see treap.zig (https://github.com/ziglang/zig/tree/master/lib/std/treap.zig)). # Chapter 45 — Text, Formatting, and Unicode [chapter_id: 45__text-formatting-and-unicode] [chapter_slug: text-formatting-and-unicode] [chapter_number: 45] [chapter_url: https://zigbook.net/chapters/45__text-formatting-and-unicode] ## Section: Overview [section_id: overview] [section_url: https://zigbook.net/chapters/45__text-formatting-and-unicode#overview] After mastering collections for structured data, 44 (44__collections-and-algorithms.xml) you now turn to text—the fundamental medium of human-computer interaction. This chapter explores `std.fmt` for formatting and parsing, `std.ascii` for ASCII character operations, `std.unicode` for UTF-8/UTF-16 handling, and encoding utilities like `base64`. fmt.zig (https://github.com/ziglang/zig/tree/master/lib/std/fmt.zig)ascii.zig (https://github.com/ziglang/zig/tree/master/lib/std/ascii.zig) Unlike high-level languages that hide encoding complexities, Zig exposes the mechanics: you choose between `[]const u8` (byte slices) and proper Unicode code point iteration, control number formatting precision, and handle encoding errors explicitly. Text processing in Zig demands awareness of byte vs. character boundaries, allocator usage for dynamic formatting, and the performance implications of different string operations. By chapter’s end, you’ll format numbers with custom precision, parse integers and floats safely, manipulate ASCII efficiently, navigate UTF-8 sequences, and encode binary data for transport—all with Zig’s characteristic explicitness and zero hidden costs. unicode.zig (https://github.com/ziglang/zig/tree/master/lib/std/unicode.zig) ## Section: Learning Goals [section_id: learning-goals] [section_url: https://zigbook.net/chapters/45__text-formatting-and-unicode#learning-goals] - Format values with `Writer.print()` using format specifiers for integers, floats, and custom types. Writer.zig (https://github.com/ziglang/zig/tree/master/lib/std/Io/Writer.zig) - Parse strings into integers (`parseInt`) and floats (`parseFloat`) with proper error handling. - Use `std.ascii` for character classification (`isDigit`, `isAlpha`, `toUpper`, `toLower`). - Navigate UTF-8 sequences with `std.unicode` and understand code point vs. byte distinctions. - Encode and decode Base64 data for binary-to-text transformations. base64.zig (https://github.com/ziglang/zig/tree/master/lib/std/base64.zig) - Implement custom formatters for user-defined types using the `{f}` specifier in Zig 0.15.2. ## Section: Formatting with std.fmt [section_id: formatting-basics] [section_url: https://zigbook.net/chapters/45__text-formatting-and-unicode#formatting-basics] Zig’s formatting revolves around `Writer.print(fmt, args)`, which writes formatted output to any `Writer` implementation. Format strings use `{}` placeholders with optional specifiers: `{d}` for decimal, `{x}` for hex, `{s}` for strings, `{any}` for debug representation, and `{f}` for custom formatters. ### Subsection: Basic Formatting [section_id: print-basic] [section_url: https://zigbook.net/chapters/45__text-formatting-and-unicode#print-basic] The simplest pattern: capture a buffer with `std.io.fixedBufferStream`, then `print` into it. ```zig const std = @import("std"); pub fn main() !void { var buffer: [100]u8 = undefined; var fbs = std.io.fixedBufferStream(&buffer); const writer = fbs.writer(); try writer.print("Answer={d}, pi={d:.2}", .{ 42, 3.14159 }); std.debug.print("Formatted: {s}\n", .{fbs.getWritten()}); } ``` Build and Run: ```shell $ zig build-exe format_basic.zig && ./format_basic ``` Output: ```shell Formatted: Answer=42, pi=3.14 ``` TIP: `std.io.fixedBufferStream` provides a `Writer` backed by a fixed buffer. No allocation needed. For dynamic output, use `std.ArrayList(u8).writer()`. fixed_buffer_stream.zig (https://github.com/ziglang/zig/tree/master/lib/std/Io/fixed_buffer_stream.zig) ### Subsection: Format Specifiers [section_id: format-specifiers] [section_url: https://zigbook.net/chapters/45__text-formatting-and-unicode#format-specifiers] Zig’s format specifiers control number bases, precision, alignment, and padding. ```zig const std = @import("std"); pub fn main() !void { const value: i32 = 255; const pi = 3.14159; const large = 123.0; std.debug.print("Decimal: {d}\n", .{value}); std.debug.print("Hexadecimal (lowercase): {x}\n", .{value}); std.debug.print("Hexadecimal (uppercase): {X}\n", .{value}); std.debug.print("Binary: {b}\n", .{value}); std.debug.print("Octal: {o}\n", .{value}); std.debug.print("Float with 2 decimals: {d:.2}\n", .{pi}); std.debug.print("Scientific notation: {e}\n", .{large}); std.debug.print("Padded: {d:0>5}\n", .{42}); std.debug.print("Right-aligned: {d:>5}\n", .{42}); } ``` Build and Run: ```shell $ zig build-exe format_specifiers.zig && ./format_specifiers ``` Output: ```shell Decimal: 255 Hexadecimal (lowercase): ff Hexadecimal (uppercase): FF Binary: 11111111 Octal: 377 Float with 2 decimals: 3.14 Scientific notation: 1.23e2 Padded: 00042 Right-aligned: 42 ``` NOTE: Use `{d}` for decimal, `{x}` for hex, `{b}` for binary, `{o}` for octal. Precision (`.N`) and width work with floats and integers. Padding with `0` creates zero-filled fields. ## Section: Parsing Strings [section_id: parsing] [section_url: https://zigbook.net/chapters/45__text-formatting-and-unicode#parsing] Zig provides `parseInt` and `parseFloat` for converting text to numbers, returning errors for invalid input rather than crashing or silently failing. ### Subsection: Parsing Integers [section_id: parse-int] [section_url: https://zigbook.net/chapters/45__text-formatting-and-unicode#parse-int] `parseInt(T, buf, base)` converts a string to an integer of type `T` in the specified base (2-36, or 0 for auto-detection). ```zig const std = @import("std"); pub fn main() !void { const decimal = try std.fmt.parseInt(i32, "42", 10); std.debug.print("Parsed decimal: {d}\n", .{decimal}); const hex = try std.fmt.parseInt(i32, "FF", 16); std.debug.print("Parsed hex: {d}\n", .{hex}); const binary = try std.fmt.parseInt(i32, "111", 2); std.debug.print("Parsed binary: {d}\n", .{binary}); // Auto-detect base with prefix const auto = try std.fmt.parseInt(i32, "0x1234", 0); std.debug.print("Auto-detected (0x): {d}\n", .{auto}); // Error handling const result = std.fmt.parseInt(i32, "not_a_number", 10); if (result) |_| { std.debug.print("Unexpected success\n", .{}); } else |err| { std.debug.print("Parse error: {}\n", .{err}); } } ``` Build and Run: ```shell $ zig build-exe parse_int.zig && ./parse_int ``` Output: ```shell Parsed decimal: 42 Parsed hex: 255 Parsed binary: 7 Auto-detected (0x): 4660 Parse error: InvalidCharacter ``` IMPORTANT: `parseInt` returns `error{Overflow, InvalidCharacter}`. Always handle these explicitly or propagate with `try`. Base 0 auto-detects `0x` (hex), `0o` (octal), `0b` (binary) prefixes. ### Subsection: Parsing Floats [section_id: parse-float] [section_url: https://zigbook.net/chapters/45__text-formatting-and-unicode#parse-float] `parseFloat(T, buf)` converts a string to a floating-point number, handling scientific notation and special values (`nan`, `inf`). ```zig const std = @import("std"); pub fn main() !void { const pi = try std.fmt.parseFloat(f64, "3.14159"); std.debug.print("Parsed: {d}\n", .{pi}); const scientific = try std.fmt.parseFloat(f64, "1.23e5"); std.debug.print("Scientific: {d}\n", .{scientific}); const infinity = try std.fmt.parseFloat(f64, "inf"); std.debug.print("Special (inf): {d}\n", .{infinity}); } ``` Build and Run: ```shell $ zig build-exe parse_float.zig && ./parse_float ``` Output: ```shell Parsed: 3.14159 Scientific: 123000 Special (inf): inf ``` TIP: `parseFloat` supports decimal notation (`3.14`), scientific notation (`1.23e5`), hexadecimal floats (`0x1.8p3`), and special values (`nan`, `inf`, `-inf`). parse_float.zig (https://github.com/ziglang/zig/tree/master/lib/std/fmt/parse_float.zig) ## Section: ASCII Character Operations [section_id: ascii-operations] [section_url: https://zigbook.net/chapters/45__text-formatting-and-unicode#ascii-operations] `std.ascii` provides fast character classification and case conversion for 7-bit ASCII. Functions gracefully handle values outside the ASCII range by returning `false` or leaving them unchanged. ### Subsection: Character Classification [section_id: ascii-classification] [section_url: https://zigbook.net/chapters/45__text-formatting-and-unicode#ascii-classification] Test whether characters are digits, letters, whitespace, etc. ```zig const std = @import("std"); pub fn main() void { const chars = [_]u8{ 'A', '5', ' ' }; for (chars) |c| { std.debug.print("'{c}': alpha={}, digit={}, ", .{ c, std.ascii.isAlphabetic(c), std.ascii.isDigit(c) }); if (c == 'A') { std.debug.print("upper={}\n", .{std.ascii.isUpper(c)}); } else if (c == '5') { std.debug.print("upper={}\n", .{std.ascii.isUpper(c)}); } else { std.debug.print("whitespace={}\n", .{std.ascii.isWhitespace(c)}); } } } ``` Build and Run: ```shell $ zig build-exe ascii_classify.zig && ./ascii_classify ``` Output: ```shell 'A': alpha=true, digit=false, upper=true '5': alpha=false, digit=true, upper=false ' ': alpha=false, digit=false, whitespace=true ``` NOTE: ASCII functions operate on bytes (`u8`). Non-ASCII bytes (>127) return `false` for classification checks. ### Subsection: Case Conversion [section_id: ascii-case] [section_url: https://zigbook.net/chapters/45__text-formatting-and-unicode#ascii-case] Convert between uppercase and lowercase for ASCII characters. ```zig const std = @import("std"); pub fn main() void { const text = "Hello, World!"; var upper_buf: [50]u8 = undefined; var lower_buf: [50]u8 = undefined; _ = std.ascii.upperString(&upper_buf, text); _ = std.ascii.lowerString(&lower_buf, text); std.debug.print("Original: {s}\n", .{text}); std.debug.print("Uppercase: {s}\n", .{upper_buf[0..text.len]}); std.debug.print("Lowercase: {s}\n", .{lower_buf[0..text.len]}); } ``` Build and Run: ```shell $ zig build-exe ascii_case.zig && ./ascii_case ``` Output: ```shell Original: Hello, World! Uppercase: HELLO, WORLD! Lowercase: hello, world! ``` IMPORTANT: `std.ascii` functions operate byte-by-byte and only affect ASCII characters. For full Unicode case mapping, use dedicated Unicode libraries or manually handle UTF-8 sequences. ## Section: Unicode and UTF-8 [section_id: unicode] [section_url: https://zigbook.net/chapters/45__text-formatting-and-unicode#unicode] Zig strings are `[]const u8` byte slices, typically UTF-8 encoded. `std.unicode` provides utilities for validating UTF-8, decoding code points, and converting between UTF-8 and UTF-16. ### Subsection: UTF-8 Validation [section_id: utf8-validation] [section_url: https://zigbook.net/chapters/45__text-formatting-and-unicode#utf8-validation] Check whether a byte sequence is valid UTF-8. ```zig const std = @import("std"); pub fn main() void { const valid = "Hello, 世界"; const invalid = "\xff\xfe"; if (std.unicode.utf8ValidateSlice(valid)) { std.debug.print("Valid UTF-8: {s}\n", .{valid}); } if (!std.unicode.utf8ValidateSlice(invalid)) { std.debug.print("Invalid UTF-8 detected\n", .{}); } } ``` Build and Run: ```shell $ zig build-exe utf8_validate.zig && ./utf8_validate ``` Output: ```shell Valid UTF-8: Hello, 世界 Invalid UTF-8 detected ``` TIP: Use `std.unicode.utf8ValidateSlice` to verify entire strings. Invalid UTF-8 can cause undefined behavior in code that assumes well-formed sequences. ### Subsection: Iterating Code Points [section_id: utf8-iteration] [section_url: https://zigbook.net/chapters/45__text-formatting-and-unicode#utf8-iteration] Decode UTF-8 byte sequences into Unicode code points using `std.unicode.Utf8View`. ```zig const std = @import("std"); pub fn main() !void { const text = "Hello, 世界"; var view = try std.unicode.Utf8View.init(text); var iter = view.iterator(); var byte_count: usize = 0; var codepoint_count: usize = 0; while (iter.nextCodepoint()) |codepoint| { const len: usize = std.unicode.utf8CodepointSequenceLength(codepoint) catch unreachable; const c = iter.bytes[iter.i - len .. iter.i]; std.debug.print("Code point: U+{X:0>4} ({s})\n", .{ codepoint, c }); byte_count += c.len; codepoint_count += 1; } std.debug.print("Byte count: {d}, Code point count: {d}\n", .{ text.len, codepoint_count }); } ``` Build and Run: ```shell $ zig build-exe utf8_iterate.zig && ./utf8_iterate ``` Output: ```shell Code point: U+0048 (H) Code point: U+0065 (e) Code point: U+006C (l) Code point: U+006C (l) Code point: U+006F (o) Code point: U+002C (,) Code point: U+0020 ( ) Code point: U+4E16 (世) Code point: U+754C (界) Byte count: 13, Code point count: 9 ``` NOTE: UTF-8 is variable-width: ASCII characters are 1 byte, but many Unicode characters require 2-4 bytes. Always iterate code points when character semantics matter, not bytes. ## Section: Base64 Encoding [section_id: base64] [section_url: https://zigbook.net/chapters/45__text-formatting-and-unicode#base64] Base64 encodes binary data as printable ASCII, useful for embedding binary in text formats (JSON, XML, URLs). Zig provides standard, URL-safe, and custom Base64 variants. ### Subsection: Encoding and Decoding [section_id: base64-basic] [section_url: https://zigbook.net/chapters/45__text-formatting-and-unicode#base64-basic] Encode binary data to Base64 and decode it back. ```zig const std = @import("std"); pub fn main() !void { var gpa = std.heap.GeneralPurposeAllocator(.{}){}; defer _ = gpa.deinit(); const allocator = gpa.allocator(); const original = "Hello, World!"; // Encode const encoded_len = std.base64.standard.Encoder.calcSize(original.len); const encoded = try allocator.alloc(u8, encoded_len); defer allocator.free(encoded); _ = std.base64.standard.Encoder.encode(encoded, original); std.debug.print("Original: {s}\n", .{original}); std.debug.print("Encoded: {s}\n", .{encoded}); // Decode var decoded_buf: [100]u8 = undefined; const decoded_len = try std.base64.standard.Decoder.calcSizeForSlice(encoded); try std.base64.standard.Decoder.decode(&decoded_buf, encoded); std.debug.print("Decoded: {s}\n", .{decoded_buf[0..decoded_len]}); } ``` Build and Run: ```shell $ zig build-exe base64_basic.zig && ./base64_basic ``` Output: ```shell Original: Hello, World! Encoded: SGVsbG8sIFdvcmxkIQ== Decoded: Hello, World! ``` TIP: `std.base64.standard.Encoder` and `.Decoder` provide encode/decode methods. The `==` padding is optional and can be controlled with encoder options. ## Section: Custom Formatters [section_id: custom-formatters] [section_url: https://zigbook.net/chapters/45__text-formatting-and-unicode#custom-formatters] Implement the `format` function for your types to control how they’re printed with `Writer.print()`. ```zig const std = @import("std"); const Point = struct { x: i32, y: i32, pub fn format(self: @This(), writer: *std.Io.Writer) std.Io.Writer.Error!void { try writer.print("({d}, {d})", .{ self.x, self.y }); } }; pub fn main() !void { const p = Point{ .x = 10, .y = 20 }; std.debug.print("Point: {f}\n", .{p}); } ``` Build and Run: ```shell $ zig build-exe custom_formatter.zig && ./custom_formatter ``` Output: ```shell Point: (10, 20) ``` NOTE: In Zig 0.15.2, the `format` method signature is simplified to: `pub fn format(self: @This(), writer: *std.Io.Writer) std.Io.Writer.Error!void`. Use the `{f}` format specifier to invoke custom formatters (e.g., `"{f}"`, not `"{}"`). ## Section: Formatting to Buffers [section_id: bufprint] [section_url: https://zigbook.net/chapters/45__text-formatting-and-unicode#bufprint] For stack-allocated formatting without allocation, use `std.fmt.bufPrint`. ```zig const std = @import("std"); pub fn main() !void { var buffer: [100]u8 = undefined; const result = try std.fmt.bufPrint(&buffer, "x={d}, y={d:.2}", .{ 42, 3.14159 }); std.debug.print("Formatted: {s}\n", .{result}); } ``` Build and Run: ```shell $ zig build-exe bufprint.zig && ./bufprint ``` Output: ```shell Formatted: x=42, y=3.14 ``` IMPORTANT: `bufPrint` returns `error.NoSpaceLeft` if the buffer is too small. Always size buffers appropriately or handle the error. ## Section: Dynamic Formatting with Allocation [section_id: allocprint] [section_url: https://zigbook.net/chapters/45__text-formatting-and-unicode#allocprint] For dynamically sized output, use `std.fmt.allocPrint` which allocates and returns a formatted string. ```zig const std = @import("std"); pub fn main() !void { var gpa = std.heap.GeneralPurposeAllocator(.{}){}; defer _ = gpa.deinit(); const allocator = gpa.allocator(); const result = try std.fmt.allocPrint(allocator, "The answer is {d}", .{42}); defer allocator.free(result); std.debug.print("Dynamic: {s}\n", .{result}); } ``` Build and Run: ```shell $ zig build-exe allocprint.zig && ./allocprint ``` Output: ```shell Dynamic: The answer is 42 ``` TIP: `allocPrint` returns a slice you must free with `allocator.free(result)`. Use this when output size is unpredictable. ## Section: Exercises [section_id: exercises] [section_url: https://zigbook.net/chapters/45__text-formatting-and-unicode#exercises] - Write a CSV parser using `std.mem.split` and `parseInt` to read rows of numbers from a comma-separated file. mem.zig (https://github.com/ziglang/zig/tree/master/lib/std/mem.zig) - Implement a hex dump utility that formats binary data as hexadecimal with ASCII representation (similar to `hexdump -C`). - Create a string validation function that checks if a string contains only ASCII printable characters, rejecting control codes and non-ASCII bytes. - Build a simple URL encoder/decoder using Base64 for the encoding portion and custom logic for percent-encoding special characters. ## Section: Caveats, alternatives, edge cases [section_id: caveats-alternatives-edge-cases] [section_url: https://zigbook.net/chapters/45__text-formatting-and-unicode#caveats-alternatives-edge-cases] - UTF-8 vs. bytes: Zig strings are `[]const u8`. Always clarify whether you’re working with bytes (indexing) or code points (semantic characters). Mismatched assumptions cause bugs with multi-byte characters. - Locale-sensitive operations: `std.ascii` and `std.unicode` don’t handle locale-specific case mapping or collation. For Turkish `i` vs. `I` or locale-aware sorting, you need external libraries. - Float formatting precision: `parseFloat` round-trips through text may lose precision for very large or very small numbers. For exact decimal representation, use fixed-point arithmetic or dedicated decimal libraries. - Base64 variants: Standard Base64 uses `+/`, URL-safe uses `-_`. Choose the correct encoder/decoder for your use case (`std.base64.standard` vs. `std.base64.url_safe_no_pad`). - Format string safety: Format strings are `comptime`-checked, but runtime-constructed format strings won’t benefit from compile-time validation. Avoid building format strings dynamically when possible. - Writer interface: All formatting functions accept `anytype` Writers, allowing output to files, sockets, ArrayLists, or custom destinations. Ensure your Writer implements `write(self, bytes: []const u8) !usize`. # Chapter 46 — I/O and Stream Adapters [chapter_id: 46__io-and-stream-adapters] [chapter_slug: io-and-stream-adapters] [chapter_number: 46] [chapter_url: https://zigbook.net/chapters/46__io-and-stream-adapters] ## Section: Overview [section_id: overview] [section_url: https://zigbook.net/chapters/46__io-and-stream-adapters#overview] The previous chapter focused on formatting and text, while other chapters introduced basic printing with simple buffered output. This chapter dives into Zig 0.15.2’s streaming primitives: the modern `std.Io.Reader` / `std.Io.Writer` interfaces and their supporting adapters (limited views, discarding, duplication, simple counting). These abstractions intentionally expose buffer internals so performance-critical paths (formatting, delimiter scanning, hashing) remain deterministic and allocation-free. Unlike opaque I/O layers found in other languages, Zig’s adapters are ultra-thin—often plain structs whose methods manipulate explicit slices and indices. Writer.zig (https://github.com/ziglang/zig/tree/master/lib/std/Io/Writer.zig)Reader.zig (https://github.com/ziglang/zig/tree/master/lib/std/Io/Reader.zig) You will learn how to create fixed in-memory writers, migrate legacy `std.io.fixedBufferStream` usage, cap reads with `limited`, duplicate an input stream (tee), discard output efficiently, and assemble pipelines (e.g., delimiter processing) without hidden allocations. Each example is small, self-contained, and demonstrates a single concept you can reuse when connecting to files, sockets, or future async abstractions. ## Section: Learning Goals [section_id: learning-goals] [section_url: https://zigbook.net/chapters/46__io-and-stream-adapters#learning-goals] - Construct fixed-buffer writers/readers with `Writer.fixed` / `Reader.fixed` and inspect buffered data. - Migrate from legacy `std.io.fixedBufferStream` to the newer APIs safely.44 (44__collections-and-algorithms.xml) - Enforce byte limits using `Reader.limited` to guard parsers against runaway inputs.Limited.zig (https://github.com/ziglang/zig/tree/master/lib/std/Io/Reader/Limited.zig) - Implement duplication (tee) and discard patterns without extra allocations.10 (10__allocators-and-memory-management.xml) - Stream delimiter-separated data using `takeDelimiter` / related helpers for line processing. - Reason about when buffered vs. direct streaming is chosen and its performance implications.39 (39__performance-and-inlining.xml) ## Section: Fundamentals: Fixed Writers & Readers [section_id: fundamentals] [section_url: https://zigbook.net/chapters/46__io-and-stream-adapters#fundamentals] The cornerstone abstractions are value types representing the state of a stream endpoint. A fixed writer buffers bytes until either full or flushed. A fixed reader exposes slices of its buffered region and offers peek/take semantics, facilitating incremental parsing without copying.3 (03__data-fundamentals.xml) ### Subsection: Basic Fixed Writer () [section_id: fixed-writer-basic] [section_url: https://zigbook.net/chapters/46__io-and-stream-adapters#fixed-writer-basic] Create an in-memory writer, emit formatted content, then inspect and forward the buffered slice. This mirrors earlier formatting patterns but without allocating an `ArrayList` or dealing with dynamic capacity.45 (45__text-formatting-and-unicode.xml) ```zig const std = @import("std"); // Demonstrates basic buffered writing using the new std.Io.Writer API // and then flushing to stdout via the older std.io File writer. pub fn main() !void { var buf: [128]u8 = undefined; // New streaming Writer backed by a fixed buffer. Writes accumulate until flushed/consumed. var w: std.Io.Writer = .fixed(&buf); try w.print("Header: {s}\n", .{"I/O adapters"}); try w.print("Value A: {d}\n", .{42}); try w.print("Value B: {x}\n", .{0xdeadbeef}); // Grab buffered bytes and print through std.debug (stdout) const buffered = w.buffered(); std.debug.print("{s}", .{buffered}); } ``` Run: ```shell $ zig run reader_writer_basics.zig ``` Output: ```shell Header: I/O adapters Value A: 42 Value B: deadbeef ``` TIP: The buffer is user-owned; you decide its lifetime and size budget. No implicit heap allocation occurs—critical for tight loops or embedded targets. ### Subsection: Migrating from [section_id: legacy-migration] [section_url: https://zigbook.net/chapters/46__io-and-stream-adapters#legacy-migration] Legacy `fixedBufferStream` (lowercase `io`) returns wrapper types with `reader()` / `writer()` methods. Zig 0.15.2 retains them for compatibility but prefers `std.Io.Writer.fixed` / `Reader.fixed` for uniform adapter composition.1 (01__boot-basics.xml)fixed_buffer_stream.zig (https://github.com/ziglang/zig/tree/master/lib/std/Io/fixed_buffer_stream.zig) ```zig const std = @import("std"); // Demonstrates legacy fixedBufferStream (deprecated in favor of std.Io.Writer.fixed) // to highlight migration paths. pub fn main() !void { var backing: [64]u8 = undefined; var fbs = std.io.fixedBufferStream(&backing); const w = fbs.writer(); try w.print("Legacy buffered writer example: {s} {d}\n", .{ "answer", 42 }); try w.print("Capacity used: {d}/{d}\n", .{ fbs.getWritten().len, backing.len }); // Echo buffer contents to stdout. std.debug.print("{s}", .{fbs.getWritten()}); } ``` Run: ```shell $ zig run fixed_buffer_stream.zig ``` Output: ```shell Legacy buffered writer example: answer 42 Capacity used: 42/64 ``` NOTE: Prefer the new capital `Io` APIs for future interoperability; `fixedBufferStream` may eventually phase out as more adapters target the modern interfaces. ### Subsection: Limiting Input () [section_id: limited-reader] [section_url: https://zigbook.net/chapters/46__io-and-stream-adapters#limited-reader] Wrap a reader with a hard cap to defend against oversized inputs (e.g., header sections, magic prefixes). Once the limit exhausts, subsequent reads indicate end of stream early, protecting downstream logic.4 (04__errors-resource-cleanup.xml) ```zig const std = @import("std"); // Reads at most N bytes from an input using std.Io.Reader.Limited pub fn main() !void { const input = "Hello, world!\nRest is skipped"; var r: std.Io.Reader = .fixed(input); var tmp: [8]u8 = undefined; // buffer backing the limited reader var limited = r.limited(.limited(5), &tmp); // allow only first 5 bytes var out_buf: [64]u8 = undefined; var out: std.Io.Writer = .fixed(&out_buf); // Pump until limit triggers EndOfStream for the limited reader _ = limited.interface.streamRemaining(&out) catch |err| { switch (err) { error.WriteFailed, error.ReadFailed => unreachable, } }; std.debug.print("{s}\n", .{out.buffered()}); } ``` Run: ```shell $ zig run limited_reader.zig ``` Output: ```shell Hello ``` TIP: Use `limited(.limited(N), tmp_buffer)` for protocol guards; parsing functions can assume bounded consumption and bail out cleanly on premature end.33 (33__c-interop-import-export-abi.xml) ## Section: Adapters & Patterns [section_id: adapters] [section_url: https://zigbook.net/chapters/46__io-and-stream-adapters#adapters] Higher-level behaviors (counting, tee, discard, delimiter streaming) emerge from simple loops over `buffered()` and small helper functions rather than heavy inheritance or trait chains.39 (39__performance-and-inlining.xml) ### Subsection: Counting Bytes (Buffered Length) [section_id: counting] [section_url: https://zigbook.net/chapters/46__io-and-stream-adapters#counting] For many scenarios, you only need the number of bytes produced so far—reading the writer’s current buffered slice length suffices, avoiding a dedicated counting adapter.10 (10__allocators-and-memory-management.xml) ```zig const std = @import("std"); // Simple counting example using Writer.fixed and buffered length. pub fn main() !void { var buf: [128]u8 = undefined; var w: std.Io.Writer = .fixed(&buf); try w.print("Counting: {s} {d}\n", .{"bytes", 123}); try w.print("And more\n", .{}); const written = w.buffered().len; std.debug.print("Total bytes logically written: {d}\n", .{written}); } ``` Run: ```shell $ zig run counting_writer.zig ``` Output: ```shell Total bytes logically written: 29 ``` NOTE: For streaming sinks where buffer length resets after flush, integrate a custom `update` function (see hashing writer design) to accumulate totals across flush boundaries. ### Subsection: Discarding Output () [section_id: discarding] [section_url: https://zigbook.net/chapters/46__io-and-stream-adapters#discarding] Benchmarks and dry-runs often need to measure formatting or transformation cost without retaining the result. Consuming the buffer zeros its length; subsequent writes continue normally.45 (45__text-formatting-and-unicode.xml) ```zig const std = @import("std"); // Demonstrate std.Io.Writer.Discarding to ignore outputs (useful in benchmarks) pub fn main() !void { var buf: [32]u8 = undefined; var w: std.Io.Writer = .fixed(&buf); try w.print("Ephemeral output: {d}\n", .{999}); // Discard content by consuming buffered bytes _ = std.Io.Writer.consumeAll(&w); // Show buffer now empty std.debug.print("Buffer after consumeAll length: {d}\n", .{w.buffered().len}); } ``` Run: ```shell $ zig run discarding_writer.zig ``` Output: ```shell Buffer after consumeAll length: 0 ``` TIP: `consumeAll` is a structural no-allocation operation; it simply adjusts `end` and (if needed) shifts remaining bytes. Cheap enough for tight inner loops. ### Subsection: Tee / Duplication [section_id: tee] [section_url: https://zigbook.net/chapters/46__io-and-stream-adapters#tee] Duplicating a stream ("teeing") can be built manually: peek, write to both targets, toss. This avoids intermediary heap buffers and works for finite or pipelined inputs.28 (28__filesystem-and-io.xml) ```zig const std = @import("std"); fn tee(r: *std.Io.Reader, a: *std.Io.Writer, b: *std.Io.Writer) !void { while (true) { const chunk = r.peekGreedy(1) catch |err| switch (err) { error.EndOfStream => break, error.ReadFailed => return err, }; try a.writeAll(chunk); try b.writeAll(chunk); r.toss(chunk.len); } } pub fn main() !void { const input = "tee me please"; var r: std.Io.Reader = .fixed(input); var abuf: [64]u8 = undefined; var bbuf: [64]u8 = undefined; var a: std.Io.Writer = .fixed(&abuf); var b: std.Io.Writer = .fixed(&bbuf); try tee(&r, &a, &b); std.debug.print("A: {s}\nB: {s}\n", .{ a.buffered(), b.buffered() }); } ``` Run: ```shell $ zig run tee_stream.zig ``` Output: ```shell A: tee me please B: tee me please ``` IMPORTANT: Always `peekGreedy(1)` (or appropriate size) before writing; failing to ensure buffered content can cause needless underlying reads or premature termination.44 (44__collections-and-algorithms.xml) ### Subsection: Delimiter Streaming Pipeline [section_id: delimiter-stream] [section_url: https://zigbook.net/chapters/46__io-and-stream-adapters#delimiter-stream] Line- or record-based protocols benefit from `takeDelimiter`, which returns slices excluding the delimiter. Loop until `null` to process all logical lines without copying or allocation.31 (31__networking-http-and-json.xml) ```zig const std = @import("std"); // Demonstrates composing Reader -> Writer pipeline with delimiter streaming. pub fn main() !void { const data = "alpha\nbeta\ngamma\n"; var r: std.Io.Reader = .fixed(data); var out_buf: [128]u8 = undefined; var out: std.Io.Writer = .fixed(&out_buf); while (true) { // Stream one line (excluding the delimiter) then print processed form const line_opt = r.takeDelimiter('\n') catch |err| switch (err) { error.StreamTooLong => unreachable, error.ReadFailed => return err, }; if (line_opt) |line| { try out.print("Line({d}): {s}\n", .{ line.len, line }); } else break; } std.debug.print("{s}", .{out.buffered()}); } ``` Run: ```shell $ zig run stream_pipeline.zig ``` Output: ```shell Line(5): alpha Line(4): beta Line(5): gamma ``` NOTE: `takeDelimiter` yields `null` after the final segment—even if the underlying data ends with a delimiter—allowing simple termination checks without extra state.4 (04__errors-resource-cleanup.xml) ## Section: Notes & Caveats [section_id: notes-caveats] [section_url: https://zigbook.net/chapters/46__io-and-stream-adapters#notes-caveats] - Fixed buffers are finite: exceeding capacity triggers writes that may fail—choose sizes based on worst-case formatted output.45 (45__text-formatting-and-unicode.xml) - `limited` enforces a hard ceiling; any remainder of the original stream remains unread (preventing over-read vulnerabilities). - Delimiter streaming requires nonzero buffer capacity; extremely tiny buffers can degrade performance due to frequent underlying reads.39 (39__performance-and-inlining.xml) - Mixing legacy `std.io.fixedBufferStream` and new `std.Io.*` is safe, but prefer consistency for future maintenance. - Counting via `buffered().len` excludes flushed data—use a persistent accumulator if you flush mid-pipeline.10 (10__allocators-and-memory-management.xml) ## Section: Exercises [section_id: exercises] [section_url: https://zigbook.net/chapters/46__io-and-stream-adapters#exercises] - Implement a simple line counter that aborts if any single line exceeds 256 bytes using `limited` wrappers.4 (04__errors-resource-cleanup.xml) - Build a tee that also computes a SHA-256 hash of all streamed bytes using `Hasher.update` from the hashing writer adapter.sha2.zig (https://github.com/ziglang/zig/tree/master/lib/std/crypto/sha2.zig) - Write a delimiter + limit based reader that extracts only the first M CSV fields from large records without reading the entire line.44 (44__collections-and-algorithms.xml) - Extend the counting example to track both logical (post-format) and raw content length when using `{any}` formatting.45 (45__text-formatting-and-unicode.xml) ## Section: Caveats, Alternatives, Edge Cases [section_id: caveats-alternatives-edge-cases] [section_url: https://zigbook.net/chapters/46__io-and-stream-adapters#caveats-alternatives-edge-cases] - Zero-capacity writers are legal but will immediately force drains—avoid for performance unless intentionally testing error paths. - A tee loop that copies very large buffered chunks may monopolize cache; consider chunking for huge streams to improve locality.39 (39__performance-and-inlining.xml) - `takeDelimiter` treats end-of-stream similarly to a delimiter; if you must distinguish trailing empty segments, track whether the last byte processed was the delimiter.31 (31__networking-http-and-json.xml) - Direct mixing with filesystem APIs (Chapter 28) introduces platform-specific buffering; re-validate limits when wrapping OS file descriptors.28 (28__filesystem-and-io.xml) - If future async I/O introduces suspend points, adapters that rely on tight peek/toss loops must ensure invariants across yields—document assumptions early.17 (17__generic-apis-and-type-erasure.xml) # Chapter 47 — Time, Logging, and Progress [chapter_id: 47__time-logging-and-progress] [chapter_slug: time-logging-and-progress] [chapter_number: 47] [chapter_url: https://zigbook.net/chapters/47__time-logging-and-progress] ## Section: Introduction [section_id: _introduction] [section_url: https://zigbook.net/chapters/47__time-logging-and-progress#_introduction] This chapter rounds out everyday operational tools in Zig: precise time measurement (`std.time`), structured logging (`std.log`), and terminal-friendly progress reporting (`std.Progress`). Here we make pipelines observable, measurable, and user-friendly. time.zig (https://github.com/ziglang/zig/tree/master/lib/std/time.zig)log.zig (https://github.com/ziglang/zig/tree/master/lib/std/log.zig)Progress.zig (https://github.com/ziglang/zig/tree/master/lib/std/Progress.zig) We’ll focus on deterministic snippets that work across platforms under Zig 0.15.2, highlighting gotchas, performance notes, and best practices. ## Section: Timekeeping with std.time [section_id: _timekeeping_with_std_time] [section_url: https://zigbook.net/chapters/47__time-logging-and-progress#_timekeeping_with_std_time] Zig’s `std.time` provides: - Calendar timestamps: `timestamp()`, `milliTimestamp()`, `microTimestamp()`, `nanoTimestamp()`. - Duration/units: constants like `ns_per_ms`, `ns_per_s`, `s_per_min` for conversions. - High-precision timers: `Instant` (fast, not strictly monotonic) and `Timer` (monotonic behavior by saturation). In general, prefer `Timer` for measuring elapsed durations. Reach for `Instant` only when you need faster sampling and can tolerate occasional non-monotonicity from quirky OS/firmware environments. ### Subsection: Measuring elapsed time (Timer) [section_id: _measuring_elapsed_time_timer] [section_url: https://zigbook.net/chapters/47__time-logging-and-progress#_measuring_elapsed_time_timer] `Timer` yields monotonic readings (saturating on regressions) and is ideal for benchmarking and timeouts. 39 (39__performance-and-inlining.xml) ```zig const std = @import("std"); pub fn main() !void { var t = try std.time.Timer.start(); std.Thread.sleep(50 * std.time.ns_per_ms); const ns = t.read(); // Ensure we slept at least 50ms if (ns < 50 * std.time.ns_per_ms) return error.TimerResolutionTooLow; // Print a stable message std.debug.print("Timer OK\n", .{}); } ``` Run: ```shell $ zig run time_timer_sleep.zig ``` Expected output: ```text Timer OK ``` NOTE: Sleeping uses `std.Thread.sleep(ns)`. On most OSes the granularity is ~1ms or worse; timers are as precise as the underlying clocks permit. Thread.zig (https://github.com/ziglang/zig/tree/master/lib/std/Thread.zig) ### Subsection: Instant sampling and ordering [section_id: _instant_sampling_and_ordering] [section_url: https://zigbook.net/chapters/47__time-logging-and-progress#_instant_sampling_and_ordering] `Instant.now()` gives a fast, high-precision timestamp for the current process. It tries to advance during suspend and can be compared or differenced. It is not guaranteed strictly monotonic everywhere. Use `Timer` when you need that property enforced. ```zig const std = @import("std"); pub fn main() !void { const a = try std.time.Instant.now(); std.Thread.sleep(1 * std.time.ns_per_ms); const b = try std.time.Instant.now(); if (b.order(a) == .lt) return error.InstantNotMonotonic; std.debug.print("Instant OK\n", .{}); } ``` Run: ```shell $ zig run time_instant_order.zig ``` Expected output: ```text Instant OK ``` ### Subsection: Time unit conversions [section_id: _time_unit_conversions] [section_url: https://zigbook.net/chapters/47__time-logging-and-progress#_time_unit_conversions] Prefer the provided unit constants over hand-rolled math. They improve clarity and prevent mistakes in mixed units. ```zig const std = @import("std"); pub fn main() !void { const two_min_s = 2 * std.time.s_per_min; const hour_ns = std.time.ns_per_hour; std.debug.print("2 min = {d} s\n1 h = {d} ns\n", .{ two_min_s, hour_ns }); } ``` Run: ```shell $ zig run time_units.zig ``` Expected output: ```text 2 min = 120 s 1 h = 3600000000000 ns ``` TIP: For calendar computations (year, month, day), see `std.time.epoch` helpers; for file timestamp metadata, see `std.fs.File` APIs. 28 (28__filesystem-and-io.xml), epoch.zig (https://github.com/ziglang/zig/tree/master/lib/std/time/epoch.zig), File.zig (https://github.com/ziglang/zig/tree/master/lib/std/fs/File.zig) ## Section: Logging with std.log [section_id: _logging_with_std_log] [section_url: https://zigbook.net/chapters/47__time-logging-and-progress#_logging_with_std_log] `std.log` is a small, composable logging façade. You can: - Control log level via `std_options` (defaults are build-mode dependent). - Use scopes (namespaces) to categorize messages. - Provide a custom `logFn` to change formatting or redirects. Below, we set `.log_level = .info` so debug logs are suppressed, and demonstrate both default and scoped logging. ```zig const std = @import("std"); // Configure logging for this program pub const std_options: std.Options = .{ .log_level = .info, // hide debug .logFn = std.log.defaultLog, }; pub fn main() void { std.log.debug("debug hidden", .{}); std.log.info("starting", .{}); std.log.warn("high temperature", .{}); const app = std.log.scoped(.app); app.info("running", .{}); } ``` Run: ```shell $ zig run logging_basic.zig 2>&1 | cat ``` Expected output: ```text info: starting warning: high temperature info(app): running ``` NOTE: - The default logger writes to stderr, so we use `2>&1` above to show it inline in this book. - In Debug builds the default level is `.debug`. Override via `std_options` to make examples stable across optimize modes. ## Section: Progress reporting with std.Progress [section_id: _progress_reporting_with_std_progress] [section_url: https://zigbook.net/chapters/47__time-logging-and-progress#_progress_reporting_with_std_progress] `std.Progress` draws a small tree of tasks to the terminal, updating periodically from another thread. It is non-allocating and aims to be portable across terminals and Windows consoles. Use it to indicate long-running work such as builds, downloads, or analysis passes. The following demo disables printing for deterministic output while exercising the API (root node, children, `completeOne`, `end`). In real tools, omit `disable_printing` to render a dynamic progress view. ```zig const std = @import("std"); pub fn main() void { // Progress can draw to stderr; disable printing in this demo for deterministic output. const root = std.Progress.start(.{ .root_name = "build", .estimated_total_items = 3, .disable_printing = true }); var compile = root.start("compile", 2); compile.completeOne(); compile.completeOne(); compile.end(); var link = root.start("link", 1); link.completeOne(); link.end(); root.end(); } ``` Run: ```shell $ zig run progress_basic.zig ``` Expected output: ```text no output ``` TIP: - Use `Options.estimated_total_items` to show counts (“[3/10] compile”); - Update names with `setName`; - Signal success/failure via `std.Progress.setStatus`. ## Section: Notes and caveats [section_id: _notes_and_caveats] [section_url: https://zigbook.net/chapters/47__time-logging-and-progress#_notes_and_caveats] - Timer vs. Instant: prefer `Timer` for elapsed time and monotonic behavior; use `Instant` for fast samples when occasional non-monotonicity is acceptable. - Sleep resolution is OS-dependent. Don’t assume sub-millisecond precision. - Logging filters apply per scope. Use `scoped(.your_component)` to gate noisy subsystems cleanly. - `std.Progress` output adapts to terminal capabilities. On CI/non-TTY or disabled printing, nothing is written. - Timezone support: stdlib does not yet have a stable `std.tz` module in 0.15.2. Use platform APIs or a library if you need timezone math. [TBD] ## Section: Exercises [section_id: _exercises] [section_url: https://zigbook.net/chapters/47__time-logging-and-progress#_exercises] - Write a micro-benchmark using `Timer` to compare two formatting routines. Print the faster one and by how many microseconds. - Wrap `std.log` with a custom `logFn` that prefixes timestamps from `nanoTimestamp()`. Ensure it remains non-allocating. - Create a small build simulator with `std.Progress` showing three phases. Make the second phase dynamically increase `estimated_total_items`. ## Section: Open Questions [section_id: _open_questions] [section_url: https://zigbook.net/chapters/47__time-logging-and-progress#_open_questions] - Timezone helpers in std: status and roadmap for a future `std.tz` or equivalent? [TBD] # Chapter 48 — Process and Environment [chapter_id: 48__process-and-environment] [chapter_slug: process-and-environment] [chapter_number: 48] [chapter_url: https://zigbook.net/chapters/48__process-and-environment] ## Section: Overview [section_id: overview] [section_url: https://zigbook.net/chapters/48__process-and-environment#overview] After building observability with timers, logging, and progress bars in the previous chapter (47__time-logging-and-progress.xml), we now step into the mechanics of how Zig programs interact with their immediate operating-system context. That means enumerating command-line arguments, examining and shaping environment variables, managing working directories, and spawning child processes—all via `std.process` on Zig 0.15.2. process.zig (https://github.com/ziglang/zig/tree/master/lib/std/process.zig) Mastering these APIs lets tools feel at home on every machine: flags parse predictably, configuration flows in cleanly, and subprocesses cooperate instead of hanging or leaking handles. In Part VI we will broaden that scope across build targets, so the patterns here form the portable baseline to build upon. 41 (41__cross-compilation-and-wasm.xml) ## Section: Learning Goals [section_id: learning-goals] [section_url: https://zigbook.net/chapters/48__process-and-environment#learning-goals] - Navigate `std.process` iterators to inspect program arguments without leaking allocations. - Capture, clone, and modify environment maps safely using Zig’s sentinel-aware strings. 3 (03__data-fundamentals.xml) - Query and update the current working directory with deterministic error handling. - Launch child processes, harvest their output, and interpret exit conditions in a portable fashion. Child.zig (https://github.com/ziglang/zig/tree/master/lib/std/process/Child.zig) - Build small utilities that respect user overrides while maintaining predictable defaults. 5 (05__project-tempconv-cli.xml) ## Section: Process Basics: Arguments, Environment, and CWD [section_id: process-basics] [section_url: https://zigbook.net/chapters/48__process-and-environment#process-basics] Zig keeps process state explicit: argument iteration, environment snapshots, and working-directory lookups all surface as functions returning slices or dedicated structs rather than hidden globals. That mirrors the data-first mindset from Part I while adding just enough OS abstraction to stay portable. 1 (01__boot-basics.xml) ### Subsection: Command-line arguments without surprises [section_id: process-basics-args] [section_url: https://zigbook.net/chapters/48__process-and-environment#process-basics-args] `std.process.argsAlloc` copies the null-terminated argument list into allocator-owned memory so you can safely compute lengths, take basenames, or duplicate strings. 5 (05__project-tempconv-cli.xml) For lightweight scans, `argsWithAllocator` exposes an iterator that reuses buffers. Just remember to call `deinit` once you finish. ```zig const std = @import("std"); pub fn main() !void { var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); defer arena.deinit(); const allocator = arena.allocator(); const argv = try std.process.argsAlloc(allocator); defer std.process.argsFree(allocator, argv); const argc = argv.len; const program_name = if (argc > 0) std.fs.path.basename(std.mem.sliceTo(argv[0], 0)) else ""; std.debug.print("argv[0].basename = {s}\n", .{program_name}); std.debug.print("argc = {d}\n", .{argc}); if (argc > 1) { std.debug.print("user args present\n", .{}); } else { std.debug.print("user args absent\n", .{}); } } ``` Run: ```shell $ zig run args_overview.zig ``` Output: ```shell argv[0].basename = args_overview argc = 1 user args absent ``` TIP: When you hand off `[:0]u8` entries to other APIs, use `std.mem.sliceTo(arg, 0)` to strip the sentinel without copying. This preserves both allocator ownership and Unicode correctness. ### Subsection: Environment maps as explicit snapshots [section_id: process-basics-envmap] [section_url: https://zigbook.net/chapters/48__process-and-environment#process-basics-envmap] Environment variables become predictable once you work on a local `EnvMap` copy. The map deduplicates keys, provides case-insensitive lookups on Windows, and makes ownership rules clear. 28 (28__filesystem-and-io.xml) ```zig const std = @import("std"); pub fn main() !void { var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); defer arena.deinit(); const allocator = arena.allocator(); var env = std.process.EnvMap.init(allocator); defer env.deinit(); try env.put("APP_MODE", "demo"); try env.put("HOST", "localhost"); try env.put("THREADS", "4"); std.debug.print("pairs = {d}\n", .{env.count()}); try env.put("APP_MODE", "override"); std.debug.print("APP_MODE = {s}\n", .{env.get("APP_MODE").?}); env.remove("THREADS"); const threads = env.get("THREADS"); std.debug.print("THREADS present? {s}\n", .{if (threads == null) "no" else "yes"}); } ``` Run: ```shell $ zig run env_map_playground.zig ``` Output: ```shell pairs = 3 APP_MODE = override THREADS present? no ``` NOTE: Use `putMove` when you already own heap-allocated strings and want the map to adopt them. It avoids extra copies and mirrors the `ArrayList.put` semantics covered in the collections chapter (44__collections-and-algorithms.xml). ### Subsection: Current working directory helpers [section_id: process-basics-cwd] [section_url: https://zigbook.net/chapters/48__process-and-environment#process-basics-cwd] `std.process.getCwdAlloc` delivers the working directory in a heap slice, while `getCwd` writes into a caller-supplied buffer. Choose the latter inside hot loops to avoid churn. Combine that with `std.fs.cwd()` from the filesystem chapter (28__filesystem-and-io.xml) for path joins or scoped directory changes. ## Section: Managing Child Processes [section_id: child-processes] [section_url: https://zigbook.net/chapters/48__process-and-environment#child-processes] Process orchestration centers on `std.process.Child`, which wraps OS-specific hazards (handle inheritance, Unicode command lines, signal races) in a consistent interface. 22 (22__build-system-deep-dive.xml) You decide how each stream behaves (inherit, ignore, pipe, or close), then wait for a `Term` that spells out whether the child exited, signaled, or stopped. ### Subsection: Capturing stdout deterministically [section_id: child-processes-capture] [section_url: https://zigbook.net/chapters/48__process-and-environment#child-processes-capture] Spawning `zig version` makes a portable demo: we pipe stdout/stderr, collect data into `ArrayList` buffers, and accept only exit code zero. 39 (39__performance-and-inlining.xml) ```zig const std = @import("std"); pub fn main() !void { var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); defer arena.deinit(); const allocator = arena.allocator(); var child = std.process.Child.init(&.{ "zig", "version" }, allocator); child.stdin_behavior = .Ignore; child.stdout_behavior = .Pipe; child.stderr_behavior = .Pipe; try child.spawn(); defer if (child.term == null) { _ = child.kill() catch {}; }; var stdout_buffer = try std.ArrayList(u8).initCapacity(allocator, 0); defer stdout_buffer.deinit(allocator); var stderr_buffer = try std.ArrayList(u8).initCapacity(allocator, 0); defer stderr_buffer.deinit(allocator); try std.process.Child.collectOutput(child, allocator, &stdout_buffer, &stderr_buffer, 16 * 1024); const term = try child.wait(); const stdout_trimmed = std.mem.trimRight(u8, stdout_buffer.items, "\r\n"); switch (term) { .Exited => |code| { if (code != 0) return error.UnexpectedExit; }, else => return error.UnexpectedExit, } std.debug.print("zig version -> {s}\n", .{stdout_trimmed}); std.debug.print("stderr bytes -> {d}\n", .{stderr_buffer.items.len}); } ``` Run: ```shell $ zig run child_process_capture.zig ``` Output: ```shell zig version -> 0.15.2 stderr bytes -> 0 ``` TIP: Always set `stdin_behavior = .Ignore` for fire-and-forget commands. Otherwise, the child inherits the parent’s stdin and may block on accidental reads (common with shells or REPLs). ### Subsection: Exit semantics and diagnostics [section_id: child-processes-exits] [section_url: https://zigbook.net/chapters/48__process-and-environment#child-processes-exits] `Child.wait()` returns a `Term` union. Inspect `Term.Exited` for numeric codes and report `Term.Signal` or `Term.Stopped` verbosely so users know when a signal intervened. Tie those diagnostics into the structured logging discipline from Chapter 47 (47__time-logging-and-progress.xml) for uniform CLI error reporting. ## Section: Notes & Caveats [section_id: notes-caveats] [section_url: https://zigbook.net/chapters/48__process-and-environment#notes-caveats] - `argsWithAllocator` borrows buffers. Stash any data you need beyond the iteration before calling `deinit`. - Environment keys are case-insensitive on Windows. Avoid storing duplicates that differ only by case. 36 (36__style-and-best-practices.xml) - `Child.spawn` can still fail after `fork`/`CreateProcess`. Always call `waitForSpawn` implicitly via `wait()` before touching pipes. 13 (13__testing-and-leak-detection.xml) ## Section: Exercises [section_id: exercises] [section_url: https://zigbook.net/chapters/48__process-and-environment#exercises] - Write a wrapper that prints a table of `(index, argument, length)` using only the iterator interface. No heap copies permitted. - Extend the `EnvMap` example to merge overlay variables from a `.env` file while rejecting duplicates of security-critical keys (e.g., `PATH`). 28 (28__filesystem-and-io.xml) - Build a miniature task runner that spawns three commands in sequence, piping stdout into a progress logger from Chapter 47 (47__time-logging-and-progress.xml). ## Section: Caveats, alternatives, edge cases [section_id: caveats-alternatives-edge-cases] [section_url: https://zigbook.net/chapters/48__process-and-environment#caveats-alternatives-edge-cases] - WASI without libc disables dynamic argument/environment access. Gate code with `builtin.os.tag` checks when targeting the browser or serverless runtimes. - On Windows, batch files require `cmd.exe` quoting rules. Rely on `argvToScriptCommandLineWindows` rather than crafting strings manually. 41 (41__cross-compilation-and-wasm.xml) - High-output children can exhaust pipes. Use `collectOutput` with a sensible `max_output_bytes`, or stream to disk to avoid `StdoutStreamTooLong`. # Chapter 49 — Compression and Archives [chapter_id: 49__compression-and-archives] [chapter_slug: compression-and-archives] [chapter_number: 49] [chapter_url: https://zigbook.net/chapters/49__compression-and-archives] ## Section: Overview [section_id: overview] [section_url: https://zigbook.net/chapters/49__compression-and-archives#overview] Zig trims its compression APIs down to the pragmatic core: high-quality decompressors that plug into the new `std.Io.Reader`/`Writer` interfaces and feed formats like TAR and ZIP without hidden side effects. #reworked stdcompressflate (https://ziglang.org/download/0.15.1/release-notes.html#reworked-stdcompressflate)flate.zig (https://github.com/ziglang/zig/tree/master/lib/std/compress/flate.zig) Bringing these pieces together lets you revive logs, package assets, or slurp registries straight into memory while keeping the same explicit resource management discipline. Because Zig treats archives as simple byte streams, the challenge shifts from magic helper functions to composing the right iterators, buffers, and metadata checks. Mastering the decompression building blocks here prepares you for the package pipelines and deployment tooling. tar.zig (https://github.com/ziglang/zig/tree/master/lib/std/tar.zig)zip.zig (https://github.com/ziglang/zig/tree/master/lib/std/zip.zig) ## Section: Learning Goals [section_id: learning-goals] [section_url: https://zigbook.net/chapters/49__compression-and-archives#learning-goals] - Drive `std.compress.flate.Decompress`, `std.compress.lzma2.decompress`, and friends directly against `std.Io.Reader`/`Writer` endpoints.Decompress.zig (https://github.com/ziglang/zig/tree/master/lib/std/compress/flate/Decompress.zig)lzma2.zig (https://github.com/ziglang/zig/tree/master/lib/std/compress/lzma2.zig)Writer.zig (https://github.com/ziglang/zig/tree/master/lib/std/Io/Writer.zig) - Choose history buffers, streaming limits, and allocators that keep decompression memory-safe under both debug and release builds.10 (10__allocators-and-memory-management.xml) - Generate small TAR archives on the fly and iterate them without touching disk state.28 (28__filesystem-and-io.xml) - Inspect and extract ZIP central directory entries while enforcing filesystem hygiene and compression-method constraints.36 (36__style-and-best-practices.xml) ## Section: Streaming Decompression Interfaces [section_id: streaming-decompression] [section_url: https://zigbook.net/chapters/49__compression-and-archives#streaming-decompression] Zig’s decompressors speak the same streaming dialect: you hand them any reader, optionally supply a scratch buffer, and they emit their payload into a writer you already own. That design leaves full control over allocation, error propagation, and flushing behavior.22 (22__build-system-deep-dive.xml) ### Subsection: Flate Containers in Practice [section_id: streaming-decompression-flate] [section_url: https://zigbook.net/chapters/49__compression-and-archives#streaming-decompression-flate] Deflate-style payloads (raw, zlib, gzip) rely on a history window up to 32 KiB. Zig 0.15.2 lets you skip allocating that window when you pipe data straight into another writer—pass `&.{}`, and the decoder will call `streamRemaining` with minimal buffering. ```zig const std = @import("std"); pub fn main() !void { var stdout_buffer: [4096]u8 = undefined; var stdout_writer = std.fs.File.stdout().writer(&stdout_buffer); const stdout = &stdout_writer.interface; const compressed = [_]u8{ 0x78, 0x9c, 0x0b, 0x2e, 0x29, 0x4a, 0x4d, 0xcc, 0xcd, 0xcc, 0x4b, 0x57, 0x48, 0x49, 0x4d, 0xce, 0xcf, 0x2d, 0x28, 0x4a, 0x2d, 0x2e, 0xce, 0xcc, 0xcf, 0x53, 0xc8, 0x4e, 0x4d, 0x2d, 0x28, 0x56, 0x28, 0xc9, 0xcf, 0xcf, 0x29, 0x56, 0x00, 0x0a, 0xa6, 0x64, 0x26, 0x97, 0x24, 0x26, 0xe5, 0xa4, 0xea, 0x71, 0x01, 0x00, 0xdf, 0xba, 0x12, 0xa6, }; var source: std.Io.Reader = .fixed(&compressed); var inflater = std.compress.flate.Decompress.init(&source, .zlib, &.{}); var plain_buf: [128]u8 = undefined; var sink = std.Io.Writer.fixed(&plain_buf); const decoded_len = try inflater.reader.streamRemaining(&sink); const decoded = plain_buf[0..decoded_len]; try stdout.print("decoded ({d} bytes): {s}\n", .{ decoded.len, decoded }); try stdout.flush(); } ``` Run: ```shell $ zig run inflate_greeting.zig ``` Output: ```shell decoded (49 bytes): Streaming decompression keeps tools predictable. ``` TIP: `std.Io.Writer.fixed` provides a stack-allocated sink with deterministic capacity; always flush manual stdout buffers afterwards to avoid losing output when the process exits.1 (01__boot-basics.xml) ### Subsection: LZMA2 Without External Tooling [section_id: streaming-decompression-lzma2] [section_url: https://zigbook.net/chapters/49__compression-and-archives#streaming-decompression-lzma2] Some registries still ship LZMA2 frames for deterministic byte-for-byte payloads. Zig wraps the decoder behind a single helper that grows an `std.Io.Writer.Allocating` for you—perfect for short configuration bundles or firmware blocks.12 (12__config-as-data.xml) ```zig const std = @import("std"); pub fn main() !void { var stdout_buffer: [4096]u8 = undefined; var stdout_writer = std.fs.File.stdout().writer(&stdout_buffer); const stdout = &stdout_writer.interface; var gpa = std.heap.GeneralPurposeAllocator(.{}){}; defer std.debug.assert(gpa.deinit() == .ok); const allocator = gpa.allocator(); const compressed = [_]u8{ 0x01, 0x00, 0x05, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x0a, 0x02, 0x00, 0x06, 0x57, 0x6f, 0x72, 0x6c, 0x64, 0x21, 0x0a, 0x00, }; var stream = std.io.fixedBufferStream(&compressed); var collector = std.Io.Writer.Allocating.init(allocator); defer collector.deinit(); try std.compress.lzma2.decompress(allocator, stream.reader(), &collector.writer); const decoded = collector.writer.buffer[0..collector.writer.end]; try stdout.print("lzma2 decoded ({d} bytes):\n{s}\n", .{ decoded.len, decoded }); try stdout.flush(); } ``` Run: ```shell $ zig run lzma2_memory_decode.zig ``` Output: ```shell lzma2 decoded (13 bytes): Hello World! ``` NOTE: `std.heap.GeneralPurposeAllocator` now reports leaks via an enum—assert on `.ok` during teardown so corrupted archives fail loudly under debug builds.heap.zig (https://github.com/ziglang/zig/tree/master/lib/std/heap.zig)13 (13__testing-and-leak-detection.xml) ### Subsection: Window Sizing Across zstd, xz, and Friends [section_id: streaming-decompression-other-codecs] [section_url: https://zigbook.net/chapters/49__compression-and-archives#streaming-decompression-other-codecs] `std.compress.zstd.Decompress` defaults to an 8 MiB window, while `std.compress.xz.Decompress` performs checksum validation as part of stream finalization.zstd.zig (https://github.com/ziglang/zig/tree/master/lib/std/compress/zstd.zig)xz.zig (https://github.com/ziglang/zig/tree/master/lib/std/compress/xz.zig) When wiring unfamiliar data sources, start with empty scratch buffers to minimize peak memory, then profile with `ReleaseFast` builds before opting into persistent ring buffers.39 (39__performance-and-inlining.xml) ## Section: Archive Workflows [section_id: archive-workflows] [section_url: https://zigbook.net/chapters/49__compression-and-archives#archive-workflows] With decompression primitives in hand, archives become composition exercises: format-specific iterators hand you metadata, and you decide whether to buffer, discard, or stream to disk.28 (28__filesystem-and-io.xml) ### Subsection: TAR Roundtrip Entirely in Memory [section_id: archive-workflows-tar] [section_url: https://zigbook.net/chapters/49__compression-and-archives#archive-workflows-tar] `std.tar.Writer` emits deterministic 512-byte blocks, so you can assemble small bundles in RAM, inspect them, and only then decide whether to persist them.24 (24__zig-package-manager-deep.xml) ```zig const std = @import("std"); pub fn main() !void { var stdout_buffer: [4096]u8 = undefined; var stdout_writer = std.fs.File.stdout().writer(&stdout_buffer); const stdout = &stdout_writer.interface; var archive_storage: [4096]u8 = undefined; var archive_writer = std.Io.Writer.fixed(&archive_storage); var tar_writer = std.tar.Writer{ .underlying_writer = &archive_writer }; try tar_writer.writeDir("reports", .{ .mode = 0o755 }); try tar_writer.writeFileBytes( "reports/summary.txt", "cpu=28%\nmem=512MiB\n", .{ .mode = 0o644 }, ); const archive = archive_writer.buffer[0..archive_writer.end]; try stdout.print("tar archive is {d} bytes and holds:\n", .{archive.len}); var source: std.Io.Reader = .fixed(archive); var name_buf: [std.fs.max_path_bytes]u8 = undefined; var link_buf: [std.fs.max_path_bytes]u8 = undefined; var iter = std.tar.Iterator.init(&source, .{ .file_name_buffer = &name_buf, .link_name_buffer = &link_buf, }); while (try iter.next()) |entry| { try stdout.print("- {s} ({s}, {d} bytes)\n", .{ entry.name, @tagName(entry.kind), entry.size }); if (entry.kind == .file) { var file_buf: [128]u8 = undefined; var file_writer = std.Io.Writer.fixed(&file_buf); try iter.streamRemaining(entry, &file_writer); const written = file_writer.end; const payload = file_buf[0..written]; try stdout.print(" contents: {s}\n", .{payload}); } } try stdout.flush(); } ``` Run: ```shell $ zig run tar_roundtrip.zig ``` Output: ```shell tar archive is 1536 bytes and holds: - reports (directory, 0 bytes) - reports/summary.txt (file, 19 bytes) contents: cpu=28% mem=512MiB ``` TIP: After calling `Iterator.next` on a regular file, you must drain the payload with `streamRemaining`; otherwise, the next header will be misaligned and the iterator throws `error.UnexpectedEndOfStream`. ### Subsection: Peeking Into ZIP Central Directories Safely [section_id: archive-workflows-zip] [section_url: https://zigbook.net/chapters/49__compression-and-archives#archive-workflows-zip] ZIP support exposes the central directory via `std.zip.Iterator`, leaving extraction policy to you. Routing entries through `std.testing.tmpDir` keeps artifacts isolated while you validate compression methods and inspect contents.testing.zig (https://github.com/ziglang/zig/tree/master/lib/std/testing.zig) ```zig const std = @import("std"); pub fn main() !void { var stdout_buffer: [4096]u8 = undefined; var stdout_writer = std.fs.File.stdout().writer(&stdout_buffer); const stdout = &stdout_writer.interface; const archive_bytes = @embedFile("demo.zip"); var gpa = std.heap.GeneralPurposeAllocator(.{}){}; defer std.debug.assert(gpa.deinit() == .ok); const allocator = gpa.allocator(); var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); var zip_file = try tmp.dir.createFile("demo.zip", .{ .read = true, .truncate = true }); defer { zip_file.close(); tmp.dir.deleteFile("demo.zip") catch {}; } try zip_file.writeAll(archive_bytes); try zip_file.seekTo(0); var read_buffer: [4096]u8 = undefined; var archive_reader = zip_file.reader(&read_buffer); var iter = try std.zip.Iterator.init(&archive_reader); var name_buf: [std.fs.max_path_bytes]u8 = undefined; try stdout.print("zip archive contains:\n", .{}); while (try iter.next()) |entry| { try entry.extract(&archive_reader, .{}, &name_buf, tmp.dir); const name = name_buf[0..entry.filename_len]; try stdout.print( "- {s} ({s}, {d} bytes)\n", .{ name, @tagName(entry.compression_method), entry.uncompressed_size }, ); if (name.len != 0 and name[name.len - 1] == '/') continue; var file = try tmp.dir.openFile(name, .{}); defer file.close(); const info = try file.stat(); const size: usize = @intCast(info.size); const contents = try allocator.alloc(u8, size); defer allocator.free(contents); const read_len = try file.readAll(contents); const slice = contents[0..read_len]; if (std.mem.endsWith(u8, name, ".txt")) { try stdout.print(" text: {s}\n", .{slice}); } else { try stdout.print(" bytes:", .{}); for (slice, 0..) |byte, idx| { const prefix = if (idx % 16 == 0) "\n " else " "; try stdout.print("{s}{X:0>2}", .{ prefix, byte }); } try stdout.print("\n", .{}); } } try stdout.flush(); } ``` Run: ```shell $ zig run zip_iterator_preview.zig ``` Output: ```shell zip archive contains: - demo/readme.txt (store, 34 bytes) text: Decompression from Zig streaming. - demo/raw.bin (store, 4 bytes) bytes: 00 01 02 03 ``` NOTE: `std.zip.Entry.extract` only supports `store` and `deflate`; reject other methods up front or shell out to a third-party library when interoperability demands it. ### Subsection: Pattern Catalog for Mixed Sources [section_id: archive-workflows-patterns] [section_url: https://zigbook.net/chapters/49__compression-and-archives#archive-workflows-patterns] Blend these techniques to hydrate manifests from package registries, decompress release artifacts before signature checks, or stage binary blobs for GPU uploads—all without leaving Zig’s standard toolbox.35 (35__project-gpu-compute-in-zig.xml) ## Section: Notes & Caveats [section_id: notes-caveats] [section_url: https://zigbook.net/chapters/49__compression-and-archives#notes-caveats] - Passing a zero-length buffer to `std.compress.flate.Decompress.init` disables history reuse, but large archives benefit from reusing a `[flate.max_window_len]u8` scratch array. - TAR iterators keep state about unread file bytes; always stream or discard them before advancing to the next header. - ZIP extraction normalizes backslashes only when `allow_backslashes = true`; enforce forward slashes to avoid directory traversal bugs on Windows.33 (33__c-interop-import-export-abi.xml) ## Section: Exercises [section_id: exercises] [section_url: https://zigbook.net/chapters/49__compression-and-archives#exercises] - Rework the flate example to stream directly into `std.fs.File.stdout().writer` without a fixed buffer and profile the difference across build modes.39 (39__performance-and-inlining.xml) - Extend the TAR roundtrip demo to attach a generated checksum footer file summarizing every entry length.43 (43__stdlib-index.xml) - Add a `verify_checksums` pass to the ZIP iterator by computing CRC32 over extracted data and comparing it to the central directory record.crc.zig (https://github.com/ziglang/zig/tree/master/lib/std/hash/crc.zig) ## Section: Caveats, Alternatives, Edge Cases [section_id: caveats-alternatives-edge-cases] [section_url: https://zigbook.net/chapters/49__compression-and-archives#caveats-alternatives-edge-cases] - Compression backends (especially zstd) may require larger buffers on older CPUs without BMI2; detect `builtin.cpu.features` before choosing lean windows.41 (41__cross-compilation-and-wasm.xml) - LZMA2 decoding still allocates internal state; stash a shared decoder if you process many small frames to avoid heap churn.10 (10__allocators-and-memory-management.xml) - For reproducible release archives, pin file ordering and timestamps explicitly—host filesystem metadata leaks otherwise.24 (24__zig-package-manager-deep.xml) # Chapter 50 — Random and Math [chapter_id: 50__random-and-math] [chapter_slug: random-and-math] [chapter_number: 50] [chapter_url: https://zigbook.net/chapters/50__random-and-math] ## Section: Overview [section_id: overview] [section_url: https://zigbook.net/chapters/50__random-and-math#overview] With compression pipelines in place from the previous chapter (49__compression-and-archives.xml), we now zoom in on the numeric engines that feed those workflows: deterministic pseudo-random number generators, well-behaved math helpers, and hashing primitives that balance speed and security. Zig 0.15.2 keeps these components modular—`std.Random` builds reproducible sequences, `std.math` provides careful tolerances and constants, and the stdlib splits hashing into non-crypto and crypto families so you can choose the right tool per workload. math.zig (https://github.com/ziglang/zig/tree/master/lib/std/math.zig)wyhash.zig (https://github.com/ziglang/zig/tree/master/lib/std/hash/wyhash.zig)sha2.zig (https://github.com/ziglang/zig/tree/master/lib/std/crypto/sha2.zig) ## Section: Learning Goals [section_id: learning-goals] [section_url: https://zigbook.net/chapters/50__random-and-math#learning-goals] - Seed, advance, and reproduce `std.Random` generators while sampling common distributions. Xoshiro256.zig (https://github.com/ziglang/zig/tree/master/lib/std/Random/Xoshiro256.zig) - Apply `std.math` utilities—constants, clamps, tolerances, and geometry helpers—to keep numeric code stable. hypot.zig (https://github.com/ziglang/zig/tree/master/lib/std/math/hypot.zig) - Distinguish fast hashers like Wyhash from cryptographic digests such as SHA-256, and wire both into file-processing jobs responsibly. ## Section: Random number foundations [section_id: random-foundations] [section_url: https://zigbook.net/chapters/50__random-and-math#random-foundations] Zig exposes pseudo-random generators as first-class values: you seed an engine, ask it for integers, floats, or indices, and your code owns the state transitions. That transparency gives you control over fuzzers, simulations, and deterministic tests. Random.zig (https://github.com/ziglang/zig/tree/master/lib/std/Random.zig) ### Subsection: Deterministic generators with reproducible sequences [section_id: random-foundations-prngs] [section_url: https://zigbook.net/chapters/50__random-and-math#random-foundations-prngs] `std.Random.DefaultPrng` wraps `Xoshiro256++`, seeding itself via SplitMix64 when you call `init(seed)`. From there you obtain a `Random` facade that exposes high-level helpers—ranges, shuffles, floats—while keeping the underlying state private. ```zig const std = @import("std"); pub fn main() !void { var stdout_buffer: [4096]u8 = undefined; var stdout_writer = std.fs.File.stdout().writer(&stdout_buffer); const stdout = &stdout_writer.interface; const seed: u64 = 0x0006_7B20; // 424,224 in decimal var prng = std.Random.DefaultPrng.init(seed); var rand = prng.random(); const dice_roll = rand.intRangeAtMost(u8, 1, 6); const coin = if (rand.boolean()) "heads" else "tails"; var ladder = [_]u8{ 0, 1, 2, 3, 4, 5 }; rand.shuffle(u8, ladder[0..]); const unit_float = rand.float(f64); var reproducible = [_]u32{ undefined, undefined, undefined }; var check_prng = std.Random.DefaultPrng.init(seed); var check_rand = check_prng.random(); for (&reproducible) |*slot| { slot.* = check_rand.int(u32); } try stdout.print("seed=0x{X:0>8}\n", .{seed}); try stdout.print("d6 roll -> {d}\n", .{dice_roll}); try stdout.print("coin flip -> {s}\n", .{coin}); try stdout.print("shuffled ladder -> {any}\n", .{ladder}); try stdout.print("unit float -> {d:.6}\n", .{unit_float}); try stdout.print("first three u32 -> {any}\n", .{reproducible}); try stdout.flush(); } ``` Run: ```shell $ zig run prng_sequences.zig ``` Output: ```shell seed=0x00067B20 d6 roll -> 5 coin flip -> tails shuffled ladder -> { 0, 4, 3, 2, 5, 1 } unit float -> 0.742435 first three u32 -> { 2135551917, 3874178402, 2563214192 } ``` TIP: The fairness guarantees of `uintLessThan` hinge on the generator’s uniform output; fall back to `uintLessThanBiased` when constant-time behavior matters more than perfect distribution. ### Subsection: Working with distributions and sampling heuristics [section_id: random-foundations-distributions] [section_url: https://zigbook.net/chapters/50__random-and-math#random-foundations-distributions] Beyond uniform draws, `Random.floatNorm` and `Random.floatExp` expose Ziggurat-backed normal and exponential samples—ideal for synthetic workloads or noise injection. ziggurat.zig (https://github.com/ziglang/zig/tree/master/lib/std/Random/ziggurat.zig) Weighted choices come from `weightedIndex`, while `.jump()` on Xoshiro engines deterministically leaps ahead by 2^128 steps to partition streams across threads without overlap. 29 (29__threads-and-atomics.xml) For cryptographic uses, swap to `std.crypto.random` or `std.Random.DefaultCsprng` to inherit ChaCha-based entropy rather than a fast-but-predictable PRNG. tlcsprng.zig (https://github.com/ziglang/zig/tree/master/lib/std/crypto/tlcsprng.zig) ## Section: Practical math utilities [section_id: math-utilities] [section_url: https://zigbook.net/chapters/50__random-and-math#math-utilities] The `std.math` namespace combines fundamental constants with measured utilities: clamps, approximate equality, and geometry helpers all share consistent semantics across CPU targets. ### Subsection: Numeric hygiene toolkit [section_id: math-utilities-toolkit] [section_url: https://zigbook.net/chapters/50__random-and-math#math-utilities-toolkit] Combining a handful of helpers—`sqrt`, `clamp`, approximate equality, and the golden ratio constant—keeps reporting code readable and portable. sqrt.zig (https://github.com/ziglang/zig/tree/master/lib/std/math/sqrt.zig) ```zig const std = @import("std"); pub fn main() !void { var stdout_buffer: [4096]u8 = undefined; var stdout_writer = std.fs.File.stdout().writer(&stdout_buffer); const stdout = &stdout_writer.interface; const m = std.math; const latencies = [_]f64{ 0.94, 1.02, 0.87, 1.11, 0.99, 1.05 }; var sum: f64 = 0; var sum_sq: f64 = 0; var minimum = latencies[0]; var maximum = latencies[0]; for (latencies) |value| { sum += value; sum_sq += value * value; minimum = @min(minimum, value); maximum = @max(maximum, value); } const mean = sum / @as(f64, @floatFromInt(latencies.len)); const rms = m.sqrt(sum_sq / @as(f64, @floatFromInt(latencies.len))); const normalized = m.clamp((mean - 0.8) / 0.6, 0.0, 1.0); const turn_degrees: f64 = 72.0; const turn_radians = turn_degrees * m.rad_per_deg; const right_angle = m.pi / 2.0; const approx_right = m.approxEqRel(f64, turn_radians, right_angle, 1e-12); const hyp = m.hypot(3.0, 4.0); try stdout.print("sample count -> {d}\n", .{latencies.len}); try stdout.print("min/max -> {d:.2} / {d:.2}\n", .{ minimum, maximum }); try stdout.print("mean -> {d:.3}\n", .{mean}); try stdout.print("rms -> {d:.3}\n", .{rms}); try stdout.print("normalized mean -> {d:.3}\n", .{normalized}); try stdout.print("72deg in rad -> {d:.6}\n", .{turn_radians}); try stdout.print("close to right angle? -> {s}\n", .{if (approx_right) "yes" else "no"}); try stdout.print("hypot(3,4) -> {d:.1}\n", .{hyp}); try stdout.print("phi constant -> {d:.9}\n", .{m.phi}); try stdout.flush(); } ``` Run: ```shell $ zig run math_inspector.zig ``` Output: ```shell sample count -> 6 min/max -> 0.87 / 1.11 mean -> 0.997 rms -> 1.000 normalized mean -> 0.328 72deg in rad -> 1.256637 close to right angle? -> no hypot(3,4) -> 5.0 phi constant -> 1.618033989 ``` NOTE: Prefer `approxEqRel` for large-magnitude comparisons and `approxEqAbs` near zero; both honor IEEE-754 edge cases without tripping NaNs. ### Subsection: Tolerances, scaling, and derived quantities [section_id: math-utilities-scaling] [section_url: https://zigbook.net/chapters/50__random-and-math#math-utilities-scaling] Angular conversions use `rad_per_deg`/`deg_per_rad`, while `hypot` preserves precision in Pythagorean calculations by avoiding catastrophic cancellation. When chaining transforms, keep intermediate results in `f64` even if your public API uses narrower floats—the mixed-type overloads in `std.math` do the right thing and avoid compiler warnings. 39 (39__performance-and-inlining.xml) ## Section: Hashing: reproducibility versus integrity [section_id: hashing-integrity] [section_url: https://zigbook.net/chapters/50__random-and-math#hashing-integrity] Zig splits hashing strategies sharply: `std.hash` families target speed and low collision rates for in-memory buckets, whereas `std.crypto.hash.sha2` delivers standardized digests for integrity checks or signature pipelines. ### Subsection: Non-cryptographic hashing for buckets [section_id: hashing-integrity-noncrypto] [section_url: https://zigbook.net/chapters/50__random-and-math#hashing-integrity-noncrypto] `std.hash.Wyhash.hash` produces a 64-bit value seeded however you like, ideal for hash maps or bloom filters where avalanche properties matter more than resistance to adversaries. If you need structured hashing with compile-time type awareness, `std.hash.autoHash` walks your fields recursively and feeds them into a configurable backend. 44 (44__collections-and-algorithms.xml)auto_hash.zig (https://github.com/ziglang/zig/tree/master/lib/std/hash/auto_hash.zig) ### Subsection: SHA-256 digest pipeline with pragmatic guardrails [section_id: hashing-integrity-sha256] [section_url: https://zigbook.net/chapters/50__random-and-math#hashing-integrity-sha256] Even when your CLI only needs a checksum, treat SHA-256 as an integrity primitive—not an authenticity guarantee—and document that difference for users. ```zig const std = @import("std"); pub fn main() !void { var stdout_buffer: [4096]u8 = undefined; var stdout_writer = std.fs.File.stdout().writer(&stdout_buffer); const stdout = &stdout_writer.interface; var gpa = std.heap.GeneralPurposeAllocator(.{}){}; defer std.debug.assert(gpa.deinit() == .ok); const allocator = gpa.allocator(); const args = try std.process.argsAlloc(allocator); defer std.process.argsFree(allocator, args); const input_path = if (args.len > 1) args[1] else "payload.txt"; var file = try std.fs.cwd().openFile(input_path, .{ .mode = .read_only }); defer file.close(); var sha256 = std.crypto.hash.sha2.Sha256.init(.{}); var buffer: [4096]u8 = undefined; while (true) { const read = try file.read(&buffer); if (read == 0) break; sha256.update(buffer[0..read]); } var digest: [std.crypto.hash.sha2.Sha256.digest_length]u8 = undefined; sha256.final(&digest); const sample = "payload preview"; const wyhash = std.hash.Wyhash.hash(0, sample); try stdout.print("wyhash(seed=0) {s} -> 0x{x:0>16}\n", .{ sample, wyhash }); const hex_digest = std.fmt.bytesToHex(digest, .lower); try stdout.print("sha256({s}) ->\n {s}\n", .{ input_path, hex_digest }); try stdout.print("(remember: sha256 certifies integrity, not authenticity.)\n", .{}); try stdout.flush(); } ``` Run: ```shell $ zig run hash_digest_tool.zig -- chapters-data/code/50__random-and-math/payload.txt ``` Output: ```shell wyhash(seed=0) payload preview -> 0x30297ecbb2bd0c02 sha256(chapters-data/code/50__random-and-math/payload.txt) -> 0498ca2116fb55b7a502d0bf3ad5d0e0b3f4e23ad919bdc0f9f151ca3637a6fa (remember: sha256 certifies integrity, not authenticity.) ``` TIP: When hashing large files, stream through a reusable buffer and reuse a single arena allocator for argument parsing to avoid churning the general-purpose allocator. 10 (10__allocators-and-memory-management.xml)fmt.zig (https://github.com/ziglang/zig/tree/master/lib/std/fmt.zig) ## Section: Notes & Caveats [section_id: notes-caveats] [section_url: https://zigbook.net/chapters/50__random-and-math#notes-caveats] - `Random` structs are not thread-safe; split distinct generators per worker or guard access with atomics to avoid shared-state races. 29 (29__threads-and-atomics.xml) - `std.math` functions honor IEEE-754 NaN propagation—never rely on comparisons after invalid operations without explicit checks. - Cryptographic digests should be paired with signature checks, HMACs, or trusted distribution; SHA-256 alone detects corruption, not tampering. hash_composition.zig (https://github.com/ziglang/zig/tree/master/lib/std/crypto/hash_composition.zig) ## Section: Exercises [section_id: exercises] [section_url: https://zigbook.net/chapters/50__random-and-math#exercises] - Replace `DefaultPrng` with `std.Random.DefaultCsprng` in the first example and measure the performance delta across build modes. 39 (39__performance-and-inlining.xml)ChaCha.zig (https://github.com/ziglang/zig/tree/master/lib/std/Random/ChaCha.zig) - Extend `math_inspector.zig` to compute confidence intervals using `approxEqRel` to flag outliers in a latency report. 47 (47__time-logging-and-progress.xml) - Modify `hash_digest_tool.zig` to compute and store SHA-256 digests for every file inside a TAR archive from Chapter 49 (49__compression-and-archives.xml), emitting a manifest alongside the archive. tar.zig (https://github.com/ziglang/zig/tree/master/lib/std/tar.zig) ## Section: Caveats, alternatives, edge cases [section_id: caveats-alternatives-edge-cases] [section_url: https://zigbook.net/chapters/50__random-and-math#caveats-alternatives-edge-cases] - Jump functions on Xoshiro mutate state irreversibly; snapshot your generator before calling `jump()` if you need to rewind later. - Avoid `bytesToHex` for streaming output on gigantic files—prefer incremental encoders to sidestep large stack allocations. - SHA-256 digests of enormous files (>4 GiB) must account for platform-specific path encodings; normalize UTF-8/UTF-16 earlier in your pipeline to avoid hashing different byte streams. 45 (45__text-formatting-and-unicode.xml) # Chapter 51 — Mem and Meta Utilities [chapter_id: 51__mem-and-meta-utilities] [chapter_slug: mem-and-meta-utilities] [chapter_number: 51] [chapter_url: https://zigbook.net/chapters/51__mem-and-meta-utilities] ## Section: Overview [section_id: overview] [section_url: https://zigbook.net/chapters/51__mem-and-meta-utilities#overview] Having wrangled randomness and numeric helpers in the previous chapter, we now turn to the slice plumbing and reflection primitives that glue many Zig subsystems together.50 (50__random-and-math.xml) Zig’s `std.mem` establishes predictable rules for tokenizing, trimming, searching, and copying arbitrarily shaped data, while `std.meta` exposes enough type information to build lightweight generic helpers without giving up static guarantees.mem.zig (https://github.com/ziglang/zig/tree/master/lib/std/mem.zig)meta.zig (https://github.com/ziglang/zig/tree/master/lib/std/meta.zig) Together they let you parse configuration files, introspect user-defined structs, and stitch together data pipelines with the same zero-cost abstractions used throughout the standard library. ## Section: Learning Goals [section_id: learning-goals] [section_url: https://zigbook.net/chapters/51__mem-and-meta-utilities#learning-goals] - Iterate across slices with `std.mem.tokenize*`, `std.mem.split*`, and search routines without allocating. - Normalize or rewrite slice contents in-place and aggregate results with `std.mem.join` and friends, even when working from stack buffers.heap.zig (https://github.com/ziglang/zig/tree/master/lib/std/heap.zig) - Reflect over struct fields using `std.meta.FieldEnum`, `std.meta.fields`, and `std.meta.stringToEnum` to build tiny schema-aware utilities. ## Section: Slice Plumbing with [section_id: mem-slice-plumbing] [section_url: https://zigbook.net/chapters/51__mem-and-meta-utilities#mem-slice-plumbing] Tokenization, splitting, and rewriting all revolve around the same idea: work with borrowed slices instead of allocating new strings. Most `std.mem` helpers therefore accept a borrowed buffer and return slices into the original data, leaving you in control of lifetimes and copying. ### Subsection: Tokenization Versus Splitting [section_id: mem-tokenization-vs-splitting] [section_url: https://zigbook.net/chapters/51__mem-and-meta-utilities#mem-tokenization-vs-splitting] The next example processes a faux configuration blob. It tokenizes lines, trims whitespace, hunts for `key=value` pairs, and normalizes mode names in-place before joining the remaining path list via a fixed buffer allocator. ```zig const std = @import("std"); const whitespace = " \t\r"; pub fn main() !void { var stdout_buffer: [4096]u8 = undefined; var stdout_writer = std.fs.File.stdout().writer(&stdout_buffer); const out = &stdout_writer.interface; const config = \\# site roots and toggles \\root = /srv/www \\root=/srv/cache \\mode = fast-render \\log-level = warn \\extra-paths = :/opt/tools:/opt/tools/bin: \\ \\# trailing noise we should ignore \\: ; var root_storage: [6][]const u8 = undefined; var root_count: usize = 0; var extra_storage: [8][]const u8 = undefined; var extra_count: usize = 0; var mode_buffer: [32]u8 = undefined; var normalized_mode: []const u8 = "slow"; var log_level: []const u8 = "info"; var lines = std.mem.tokenizeScalar(u8, config, '\n'); while (lines.next()) |line| { const trimmed = std.mem.trim(u8, line, whitespace); if (trimmed.len == 0 or std.mem.startsWith(u8, trimmed, "#")) continue; const eq_index = std.mem.indexOfScalar(u8, trimmed, '=') orelse continue; const key = std.mem.trim(u8, trimmed[0..eq_index], whitespace); const value = std.mem.trim(u8, trimmed[eq_index + 1 ..], whitespace); if (std.mem.eql(u8, key, "root")) { if (root_count < root_storage.len) { root_storage[root_count] = value; root_count += 1; } } else if (std.mem.eql(u8, key, "mode")) { if (value.len <= mode_buffer.len) { std.mem.copyForwards(u8, mode_buffer[0..value.len], value); const mode_view = mode_buffer[0..value.len]; std.mem.replaceScalar(u8, mode_view, '-', '_'); normalized_mode = mode_view; } } else if (std.mem.eql(u8, key, "log-level")) { log_level = value; } else if (std.mem.eql(u8, key, "extra-paths")) { var paths = std.mem.splitScalar(u8, value, ':'); while (paths.next()) |segment| { const cleaned = std.mem.trim(u8, segment, whitespace); if (cleaned.len == 0) continue; if (extra_count < extra_storage.len) { extra_storage[extra_count] = cleaned; extra_count += 1; } } } } var extras_join_buffer: [256]u8 = undefined; var extras_allocator = std.heap.FixedBufferAllocator.init(&extras_join_buffer); var extras_joined_slice: []u8 = &.{}; if (extra_count != 0) { extras_joined_slice = try std.mem.join(extras_allocator.allocator(), ", ", extra_storage[0..extra_count]); } const extras_joined: []const u8 = if (extra_count == 0) "(none)" else extras_joined_slice; try out.print("normalized mode -> {s}\n", .{normalized_mode}); try out.print("log level -> {s}\n", .{log_level}); try out.print("roots ({d})\n", .{root_count}); for (root_storage[0..root_count], 0..) |root, idx| { try out.print(" [{d}] {s}\n", .{ idx, root }); } try out.print("extra segments -> {s}\n", .{extras_joined}); try out.flush(); } ``` Run: ```shell $ zig run mem_token_workbench.zig ``` Output: ```shell normalized mode -> fast_render log level -> warn roots (2) [0] /srv/www [1] /srv/cache extra segments -> /opt/tools, /opt/tools/bin ``` TIP: Prefer `std.mem.tokenize*` variants when you want to skip delimiters entirely, and `std.mem.split*` when empty segments matter—for example, when you need to detect doubled separators. ### Subsection: Copying, Rewriting, and Aggregating Slices [section_id: mem-copy-rewrite] [section_url: https://zigbook.net/chapters/51__mem-and-meta-utilities#mem-copy-rewrite] `std.mem.copyForwards` guarantees safe overlap when copying forward, while `std.mem.replaceScalar` lets you normalize characters in-place without touching allocation. Once you have the slices you care about, use `std.mem.join` with a `std.heap.FixedBufferAllocator` to coalesce them into a single view without falling back to the general-purpose heap. Keep an eye on buffer lengths (as the example does for `mode_buffer`) so that the rewrite step stays bounds-safe. ## Section: Reflection Helpers with [section_id: meta-reflection] [section_url: https://zigbook.net/chapters/51__mem-and-meta-utilities#meta-reflection] Where `std.mem` keeps data moving, `std.meta` helps describe it. The library exposes field metadata, alignment, and enumerated tags so that you can build schema-aware tooling without macro systems or runtime type information. ### Subsection: Field-Driven Overrides with [section_id: meta-field-overrides] [section_url: https://zigbook.net/chapters/51__mem-and-meta-utilities#meta-field-overrides] This sample defines a `Settings` struct, prints a schema summary, and applies overrides parsed from a string by dispatching through `std.meta.FieldEnum`. Each assignment uses statically typed code yet supports dynamic key lookup via `std.meta.stringToEnum` and the struct’s own default values. ```zig const std = @import("std"); const Settings = struct { render: bool = false, retries: u8 = 1, mode: []const u8 = "slow", log_level: []const u8 = "info", extra_paths: []const u8 = "", }; const Field = std.meta.FieldEnum(Settings); const whitespace = " \t\r"; const raw_config = \\# overrides loaded from a repro case \\render = true \\retries = 4 \\mode = fast-render \\extra_paths = /srv/www:/srv/cache ; const ParseError = error{ UnknownKey, BadBool, BadInt, }; fn printValue(out: anytype, value: anytype) !void { const T = @TypeOf(value); switch (@typeInfo(T)) { .pointer => |ptr_info| switch (ptr_info.child) { u8 => if (ptr_info.size == .slice or ptr_info.size == .many or ptr_info.size == .c) { try out.print("{s}", .{value}); return; }, else => {}, }, else => {}, } try out.print("{any}", .{value}); } fn parseBool(value: []const u8) ParseError!bool { if (std.ascii.eqlIgnoreCase(value, "true") or std.mem.eql(u8, value, "1")) return true; if (std.ascii.eqlIgnoreCase(value, "false") or std.mem.eql(u8, value, "0")) return false; return error.BadBool; } fn applySetting(settings: *Settings, key: []const u8, value: []const u8) ParseError!void { const tag = std.meta.stringToEnum(Field, key) orelse return error.UnknownKey; switch (tag) { .render => settings.render = try parseBool(value), .retries => { const parsed = std.fmt.parseInt(u16, value, 10) catch return error.BadInt; settings.retries = std.math.cast(u8, parsed) orelse return error.BadInt; }, .mode => settings.mode = value, .log_level => settings.log_level = value, .extra_paths => settings.extra_paths = value, } } fn emitSchema(out: anytype) !void { try out.print("settings schema:\n", .{}); inline for (std.meta.fields(Settings)) |field| { const defaults = Settings{}; const default_value = @field(defaults, field.name); try out.print(" - {s}: {s} (align {d}) default=", .{ field.name, @typeName(field.type), std.meta.alignment(field.type) }); try printValue(out, default_value); try out.print("\n", .{}); } } fn dumpSettings(out: anytype, settings: Settings) !void { try out.print("resolved values:\n", .{}); inline for (std.meta.fields(Settings)) |field| { const value = @field(settings, field.name); try out.print(" {s} => ", .{field.name}); try printValue(out, value); try out.print("\n", .{}); } } pub fn main() !void { var stdout_buffer: [4096]u8 = undefined; var stdout_writer = std.fs.File.stdout().writer(&stdout_buffer); const out = &stdout_writer.interface; try emitSchema(out); var settings = Settings{}; var failures: usize = 0; var lines = std.mem.tokenizeScalar(u8, raw_config, '\n'); while (lines.next()) |line| { const trimmed = std.mem.trim(u8, line, whitespace); if (trimmed.len == 0 or std.mem.startsWith(u8, trimmed, "#")) continue; const eql = std.mem.indexOfScalar(u8, trimmed, '=') orelse { failures += 1; continue; }; const key = std.mem.trim(u8, trimmed[0..eql], whitespace); const raw = std.mem.trim(u8, trimmed[eql + 1 ..], whitespace); if (key.len == 0) { failures += 1; continue; } if (applySetting(&settings, key, raw)) |_| {} else |err| { failures += 1; try out.print(" warning: {s} -> {any}\n", .{ key, err }); } } try dumpSettings(out, settings); const tags = std.meta.tags(Field); try out.print("field tags visited: {any}\n", .{tags}); try out.print("parsing failures: {d}\n", .{failures}); try out.flush(); } ``` Run: ```shell $ zig run meta_struct_report.zig ``` Output: ```shell settings schema: - render: bool (align 1) default=false - retries: u8 (align 1) default=1 - mode: []const u8 (align 1) default=slow - log_level: []const u8 (align 1) default=info - extra_paths: []const u8 (align 1) default= resolved values: render => true retries => 4 mode => fast-render log_level => info extra_paths => /srv/www:/srv/cache field tags visited: { .render, .retries, .mode, .log_level, .extra_paths } parsing failures: 0 ``` NOTE: `std.meta.tags(FieldEnum(T))` materialises an array of field tags at comptime, making it cheap to track which fields a routine has touched without runtime reflection. ### Subsection: Schema Inspection Patterns [section_id: meta-schema-patterns] [section_url: https://zigbook.net/chapters/51__mem-and-meta-utilities#meta-schema-patterns] By combining `std.meta.fields` with `@field`, you can emit a documentation table or prepare a lightweight LSP schema for editor integrations. `std.meta.alignment` reports the natural alignment of each field type, while the field iterator exposes default values so you can display sensible fallbacks alongside user-supplied overrides. Because everything happens at compile time, the generated code compiles down to a handful of constants and direct loads. ## Section: Notes & Caveats [section_id: notes-caveats] [section_url: https://zigbook.net/chapters/51__mem-and-meta-utilities#notes-caveats] - When tokenizing, remember that the returned slices alias the original buffer; mutate or copy them before the source goes out of scope. - `std.mem.join` allocates through the supplied allocator—stack-buffer allocators work well for short joins, but switch to a general-purpose allocator as soon as you expect unbounded data. - `std.meta.stringToEnum` performs a linear scan for large enums; cache the result or build a lookup table when parsing untrusted input at scale. ## Section: Exercises [section_id: exercises] [section_url: https://zigbook.net/chapters/51__mem-and-meta-utilities#exercises] - Extend `mem_token_workbench.zig` to detect duplicate roots by sorting or deduplicating the slice list with `std.mem.sort` and `std.mem.indexOf` before joining. - Augment `meta_struct_report.zig` to emit JSON by pairing `std.meta.fields` with `std.json.StringifyStream`, keeping the compile-time schema but offering machine-readable output.32 (32__project-http-json-client.xml) - Add a `strict` flag to the override parser that requires every key in `FieldEnum(Settings)` to appear at least once, using `std.meta.tags` to track coverage.36 (36__style-and-best-practices.xml) ## Section: Caveats, Alternatives, Edge Cases [section_id: caveats-alternatives-edge-cases] [section_url: https://zigbook.net/chapters/51__mem-and-meta-utilities#caveats-alternatives-edge-cases] - If you need delimiter-aware iteration that preserves separators, fall back to `std.mem.SplitIterator`—tokenizers always drop delimiter slices. - For very large configuration blobs, consider `std.mem.terminated` and sentinel slices so you can stream sections without copying entire files into memory.28 (28__filesystem-and-io.xml) - `std.meta` intentionally exposes only compile-time data; if you need runtime reflection, you must generate it yourself (for example, via build steps that emit lookup tables). # Chapter 52 — Debug and Valgrind [chapter_id: 52__debug-and-valgrind] [chapter_slug: debug-and-valgrind] [chapter_number: 52] [chapter_url: https://zigbook.net/chapters/52__debug-and-valgrind] ## Section: Overview [section_id: overview] [section_url: https://zigbook.net/chapters/52__debug-and-valgrind#overview] After building slice tooling and lightweight reflection in the previous chapter (51__mem-and-meta-utilities.xml), we now turn to what happens when things go wrong. Zig’s diagnostics pipeline lives in `std.debug`, which controls panic strategies, offers stack unwinding, and exposes helpers for printing structured data. debug.zig (https://github.com/ziglang/zig/tree/master/lib/std/debug.zig) For memory instrumentation you have `std.valgrind`, a thin veneer over Valgrind’s client request protocol that keeps your custom allocators visible to Memcheck without ruining portability. valgrind.zig (https://github.com/ziglang/zig/tree/master/lib/std/valgrind.zig)memcheck.zig (https://github.com/ziglang/zig/tree/master/lib/std/valgrind/memcheck.zig) ## Section: Learning Goals [section_id: learning-goals] [section_url: https://zigbook.net/chapters/52__debug-and-valgrind#learning-goals] - Configure panic behavior and collect stack information with `std.debug`. - Use stderr-aware writers and stack capture APIs without leaking unstable addresses into logs. - Annotate custom allocations for Valgrind Memcheck and safely query leak counters at runtime. ## Section: Diagnostics with [section_id: std-debug-overview] [section_url: https://zigbook.net/chapters/52__debug-and-valgrind#std-debug-overview] `std.debug` is the standard library’s staging ground for assertions, panic hooks, and stack unwinding. The module keeps the default panic bridge (`std.debug.simple_panic`) alongside a configurable `FullPanic` helper that funnels every safety check into your own handler. simple_panic.zig (https://github.com/ziglang/zig/tree/master/lib/std/debug/simple_panic.zig) Whether you are instrumenting tests or tightening release builds, this is the layer that decides what happens when `unreachable` executes. ### Subsection: Panic strategies and safety modes [section_id: std-debug-panic-strategies] [section_url: https://zigbook.net/chapters/52__debug-and-valgrind#std-debug-panic-strategies] By default, a failed `std.debug.assert` or `unreachable` results in a call to `@panic`, which delegates to the active panic handler. You can override this globally by defining a root-level `pub fn panic(message: []const u8, trace: ?*std.builtin.StackTrace, ret_addr: ?usize) noreturn`, or compose a bespoke handler via `std.debug.FullPanic(custom)` to preserve Zig’s rich error messages while swapping termination semantics. This is especially useful in embedded or service-mode binaries where you prefer logging and clean shutdowns over aborting the process. Remember that safety features are mode-dependent—`std.debug.runtime_safety` evaluates to `false` in ReleaseFast and ReleaseSmall, so instrumentation should check that flag before assuming invariants are enforced. ### Subsection: Capturing stack frames and managing stderr [section_id: std-debug-stack-output] [section_url: https://zigbook.net/chapters/52__debug-and-valgrind#std-debug-stack-output] The following program demonstrates several `std.debug` primitives: printing to stderr, locking stderr for multi-line output, capturing a stack trace without exposing raw addresses, and reporting build parameters. ```zig const std = @import("std"); const builtin = @import("builtin"); pub fn main() !void { // Emit a quick note to stderr using the convenience helper. std.debug.print("[stderr] staged diagnostics\n", .{}); // Lock stderr explicitly for a multi-line message. { const writer = std.debug.lockStderrWriter(&.{}); defer std.debug.unlockStderrWriter(); writer.writeAll("[stderr] stack capture incoming\n") catch {}; } var stdout_buffer: [256]u8 = undefined; var stdout_writer = std.fs.File.stdout().writer(&stdout_buffer); const out = &stdout_writer.interface; // Capture a trimmed stack trace without printing raw addresses. var frame_storage: [8]usize = undefined; var trace = std.builtin.StackTrace{ .index = 0, .instruction_addresses = frame_storage[0..], }; std.debug.captureStackTrace(null, &trace); try out.print("frames captured -> {d}\n", .{trace.index}); // Guard a sentinel with the debug assertions that participate in safety mode. const marker = "panic probe"; std.debug.assert(marker.len == 11); var buffer = [_]u8{ 0x41, 0x42, 0x43, 0x44 }; std.debug.assertReadable(buffer[0..]); std.debug.assertAligned(&buffer, .@"1"); // Report build configuration facts gathered from std.debug. try out.print( "runtime_safety -> {s}\n", .{if (std.debug.runtime_safety) "enabled" else "disabled"}, ); try out.print( "optimize_mode -> {s}\n", .{@tagName(builtin.mode)}, ); // Show manual formatting against a fixed buffer, useful when stderr is locked. var scratch: [96]u8 = undefined; var stream = std.io.fixedBufferStream(&scratch); try stream.writer().print("captured slice -> {s}\n", .{marker}); try out.print("{s}", .{stream.getWritten()}); try out.flush(); } ``` Run: ```shell $ zig run debug_diagnostics_station.zig ``` Output: ```shell [stderr] staged diagnostics [stderr] stack capture incoming frames captured -> 4 runtime_safety -> enabled optimize_mode -> Debug captured slice -> panic probe ``` A few callouts: - `std.debug.print` always targets stderr, so it remains separate from any structured stdout reporting. - Use `std.debug.lockStderrWriter` when you need atomic multi-line diagnostics; the helper temporarily clears `std.Progress` overlays. - `std.debug.captureStackTrace` writes to a `std.builtin.StackTrace` buffer. Emitting only the frame count avoids leaking ASLR-sensitive addresses and keeps log output deterministic. builtin.zig (https://github.com/ziglang/zig/tree/master/lib/std/builtin.zig) - Formatter access comes from the writer interface returned by `std.fs.File.stdout().writer()`, which mirrors the approach from earlier chapters. ### Subsection: Introspecting symbols and binaries [section_id: std-debug-introspection] [section_url: https://zigbook.net/chapters/52__debug-and-valgrind#std-debug-introspection] `std.debug.getSelfDebugInfo()` opens the current binary’s DWARF or PDB tables on demand and caches them for subsequent lookups. With that handle you can resolve instruction addresses to `std.debug.Symbol` records that include function names, compilation units, and optional source locations. SelfInfo.zig (https://github.com/ziglang/zig/tree/master/lib/std/debug/SelfInfo.zig) You do not need to pay that cost in hot paths: store addresses (or stack snapshots) first, then resolve them lazily in telemetry tools or when generating a bug report. On platforms where debug info is stripped or unavailable, the API returns `error.MissingDebugInfo`, so wrap the lookup in a fallback that prints module names only. ## Section: Instrumenting with [section_id: std-valgrind-overview] [section_url: https://zigbook.net/chapters/52__debug-and-valgrind#std-valgrind-overview] `std.valgrind` mirrors Valgrind’s client requests while compiling down to no-ops when `builtin.valgrind_support` is false, keeping your binaries portable. You can detect Valgrind at runtime via `std.valgrind.runningOnValgrind()` (useful for suppressing self-tests that spawn massive workloads) and query accumulated error counts with `std.valgrind.countErrors()`. ### Subsection: Marking custom allocations for Memcheck [section_id: std-valgrind-memcheck] [section_url: https://zigbook.net/chapters/52__debug-and-valgrind#std-valgrind-memcheck] When you roll your own allocator, Memcheck cannot infer which buffers are live unless you annotate them. The following example shows the canonical pattern: announce a block, adjust its definedness, run a quick leak check, and free the block when done. ```zig const std = @import("std"); pub fn main() !void { var stdout_buffer: [256]u8 = undefined; var stdout_writer = std.fs.File.stdout().writer(&stdout_buffer); const out = &stdout_writer.interface; const on_valgrind = std.valgrind.runningOnValgrind() != 0; try out.print("running_on_valgrind -> {s}\n", .{if (on_valgrind) "yes" else "no"}); var arena_storage: [96]u8 = undefined; var arena = std.heap.FixedBufferAllocator.init(&arena_storage); const allocator = arena.allocator(); var span = try allocator.alloc(u8, 48); defer { std.valgrind.freeLikeBlock(span.ptr, 0); allocator.free(span); } // Announce a custom allocation to Valgrind so leak reports point at our call site. std.valgrind.mallocLikeBlock(span, 0, true); const label: [:0]const u8 = "workspace-span\x00"; const block_id = std.valgrind.memcheck.createBlock(span, label); defer _ = std.valgrind.memcheck.discard(block_id); std.valgrind.memcheck.makeMemDefined(span); std.valgrind.memcheck.makeMemNoAccess(span[32..]); std.valgrind.memcheck.makeMemDefinedIfAddressable(span[32..]); const leak_bytes = std.valgrind.memcheck.countLeaks(); try out.print("leaks_bytes -> {d}\n", .{leak_bytes.leaked}); std.valgrind.memcheck.doQuickLeakCheck(); const error_total = std.valgrind.countErrors(); try out.print("errors_seen -> {d}\n", .{error_total}); try out.flush(); } ``` Run: ```shell $ zig run valgrind_integration_probe.zig ``` Output: ```shell running_on_valgrind -> no leaks_bytes -> 0 errors_seen -> 0 ``` Even outside Valgrind the calls succeed—every request degrades to a stub when client support is absent—so you can leave the instrumentation in release binaries without gating on build flags. The sequence worth memorizing is: 1. `std.valgrind.mallocLikeBlock` immediately after you obtain memory from a custom allocator. 2. `std.valgrind.memcheck.createBlock` with a zero-terminated label so Memcheck reports use the name you expect. 3. Optional range adjustments such as `makeMemNoAccess` and `makeMemDefinedIfAddressable` when you deliberately poison or unpoison guard bytes. 4. A matching `std.valgrind.freeLikeBlock` (and `memcheck.discard`) before the underlying allocator releases the memory. ## Section: Notes & Caveats [section_id: notes-caveats] [section_url: https://zigbook.net/chapters/52__debug-and-valgrind#notes-caveats] - Stack capture relies on debug info; in stripped builds or unsupported targets, `std.debug.captureStackTrace` falls back to empty results, so wrap diagnostics with graceful degradation. - `std.debug.FullPanic` executes on every safety violation. Ensure the handler performs only async-signal-safe operations if you plan to log from multiple executor threads. - Valgrind annotations are cheap in native runs but do not cover sanitizer-based tooling—prefer compiler sanitizers (ASan/TSan) when you need deterministic CI coverage. 37 (37__illegal-behavior-and-safety-modes.xml) ## Section: Exercises [section_id: exercises] [section_url: https://zigbook.net/chapters/52__debug-and-valgrind#exercises] - Implement a custom panic handler that logs to a ring buffer using `std.debug.FullPanic`, then forwards to the default handler in debug mode. - Extend `debug_diagnostics_station.zig` so that stack captures are resolved to symbol names via `std.debug.getSelfDebugInfo()`, caching results to avoid repeated lookups. - Modify `valgrind_integration_probe.zig` to wrap a bump allocator: record every active span in a table, and call `std.valgrind.memcheck.doQuickLeakCheck()` only when the process shuts down. 10 (10__allocators-and-memory-management.xml) ## Section: Caveats, alternatives, edge cases [section_id: caveats-alternatives-edge-cases] [section_url: https://zigbook.net/chapters/52__debug-and-valgrind#caveats-alternatives-edge-cases] - `std.debug.dumpCurrentStackTrace` prints absolute addresses and source paths that vary per run because of ASLR; capture to an in-memory buffer and redact volatile fields before shipping telemetry. - Valgrind’s client requests depend on the `xchg`-based handshake and are no-ops on architectures that Valgrind does not support—`runningOnValgrind()` will always return zero there. - Memcheck annotations do not replace structured testing; combine them with Zig’s leak detection (`zig test --detect-leaks`) for deterministic regression coverage. 13 (13__testing-and-leak-detection.xml) # Chapter 53 — Project [chapter_id: 53__project-top-k-word-frequency-analyzer] [chapter_slug: project-top-k-word-frequency-analyzer] [chapter_number: 53] [chapter_url: https://zigbook.net/chapters/53__project-top-k-word-frequency-analyzer] ## Section: Overview [section_id: overview] [section_url: https://zigbook.net/chapters/53__project-top-k-word-frequency-analyzer#overview] The debugging chapter introduced tools for explaining why a program misbehaves.52 (52__debug-and-valgrind.xml) This project leans on similar discipline to build a deterministic text analytics utility: feed it a log excerpt, collect the most frequent tokens, and emit timing data for each phase. We will combine the tokenization helpers from `std.mem`, hashed collections from `std`, the `heap` sorter, and the `Timer` API to produce reproducible rankings with measured costs.mem.zig (https://github.com/ziglang/zig/tree/master/lib/std/mem.zig)hash_map.zig (https://github.com/ziglang/zig/tree/master/lib/std/hash_map.zig)sort.zig (https://github.com/ziglang/zig/tree/master/lib/std/sort.zig)time.zig (https://github.com/ziglang/zig/tree/master/lib/std/time.zig) ## Section: Learning Goals [section_id: learning-goals] [section_url: https://zigbook.net/chapters/53__project-top-k-word-frequency-analyzer#learning-goals] - Build an end-to-end I/O pipeline that reads a corpus, normalizes text, and accumulates counts with `std.StringHashMap` and `std.ArrayList`.array_list.zig (https://github.com/ziglang/zig/tree/master/lib/std/array_list.zig) - Rank frequencies deterministically using an explicit comparator that resolves ties without depending on hash map iteration order. - Capture per-phase timings with `std.time.Timer` to validate regressions and communicate performance expectations. ## Section: Designing the Pipeline [section_id: pipeline-design] [section_url: https://zigbook.net/chapters/53__project-top-k-word-frequency-analyzer#pipeline-design] Our analyzer accepts an optional file path and an optional `k` parameter (`top k` tokens); both default to a bundled corpus and `5`, respectively. We read the entire file into memory for simplicity, but the normalization and counting loop is written to operate linearly so it can be adapted to stream chunks later. A `GeneralPurposeAllocator` backs all dynamic structures, and an arena-friendly workflow (duplicating strings only on first sight) keeps allocations proportional to the vocabulary size.heap.zig (https://github.com/ziglang/zig/tree/master/lib/std/heap.zig)process.zig (https://github.com/ziglang/zig/tree/master/lib/std/process.zig) Tokenization happens with `std.mem.tokenizeAny`, configured with a conservative separator set that trims whitespace, punctuation, and markup characters. Each token is lowercased in a reusable `std.ArrayList(u8)` before attempting insertion into the map; if the token already exists, only the count increments, keeping temporary allocations bounded. ## Section: Counting and Ranking [section_id: count-and-rank] [section_url: https://zigbook.net/chapters/53__project-top-k-word-frequency-analyzer#count-and-rank] The complete utility demonstrates StringHashMap, ArrayList, sorting, and timing side by side. ```zig const std = @import("std"); const Separators = " \t\r\n,.;:!?\"'()[]{}<>-/\\|`~*_"; const Entry = struct { word: []const u8, count: usize, }; pub fn main() !void { var gpa = std.heap.GeneralPurposeAllocator(.{}){}; defer if (gpa.deinit() == .leak) @panic("memory leak"); const allocator = gpa.allocator(); var stdout_buffer: [256]u8 = undefined; var stdout_writer = std.fs.File.stdout().writer(&stdout_buffer); const out = &stdout_writer.interface; const args = try std.process.argsAlloc(allocator); defer std.process.argsFree(allocator, args); const path = if (args.len >= 2) args[1] else "chapters-data/code/53__project-top-k-word-frequency-analyzer/sample_corpus.txt"; const top_k: usize = blk: { if (args.len >= 3) { const value = std.fmt.parseInt(usize, args[2], 10) catch { try out.print("invalid top-k value: {s}\n", .{args[2]}); return error.InvalidArgument; }; break :blk if (value == 0) 1 else value; } break :blk 5; }; var timer = try std.time.Timer.start(); const corpus = try std.fs.cwd().readFileAlloc(allocator, path, 1024 * 1024 * 4); defer allocator.free(corpus); const read_ns = timer.lap(); var scratch = try std.ArrayList(u8).initCapacity(allocator, 32); defer scratch.deinit(allocator); var frequencies = std.StringHashMap(usize).init(allocator); defer frequencies.deinit(); var total_tokens: usize = 0; var it = std.mem.tokenizeAny(u8, corpus, Separators); while (it.next()) |raw| { scratch.clearRetainingCapacity(); try scratch.appendSlice(allocator, raw); const slice = scratch.items; for (slice) |*byte| { byte.* = std.ascii.toLower(byte.*); } if (slice.len == 0) continue; const gop = try frequencies.getOrPut(slice); if (gop.found_existing) { gop.value_ptr.* += 1; } else { const owned = try allocator.dupe(u8, slice); gop.key_ptr.* = owned; gop.value_ptr.* = 1; } total_tokens += 1; } const tokenize_ns = timer.lap(); var entries = try std.ArrayList(Entry).initCapacity(allocator, frequencies.count()); defer { for (entries.items) |entry| allocator.free(entry.word); entries.deinit(allocator); } var map_it = frequencies.iterator(); while (map_it.next()) |kv| { try entries.append(allocator, .{ .word = kv.key_ptr.*, .count = kv.value_ptr.* }); } const entry_slice = entries.items; std.sort.heap(Entry, entry_slice, {}, struct { fn lessThan(_: void, a: Entry, b: Entry) bool { if (a.count == b.count) return std.mem.lessThan(u8, a.word, b.word); return a.count > b.count; } }.lessThan); const sort_ns = timer.lap(); const unique_words = entries.items.len; const limit = if (unique_words < top_k) unique_words else top_k; try out.print("source -> {s}\n", .{path}); try out.print("tokens -> {d}, unique -> {d}\n", .{ total_tokens, unique_words }); try out.print("top {d} words:\n", .{limit}); var index: usize = 0; while (index < limit) : (index += 1) { const entry = entry_slice[index]; try out.print(" {d:>2}. {s} -> {d}\n", .{ index + 1, entry.word, entry.count }); } try out.print("timings (ns): read={d}, tokenize={d}, sort={d}\n", .{ read_ns, tokenize_ns, sort_ns }); try out.flush(); } ``` Run: ```shell $ zig run chapters-data/code/53__project-top-k-word-frequency-analyzer/topk_word_frequency.zig ``` Output: ```shell source -> chapters-data/code/53__project-top-k-word-frequency-analyzer/sample_corpus.txt tokens -> 102, unique -> 86 top 5 words: 1. the -> 6 2. a -> 3 3. and -> 3 4. are -> 2 5. latency -> 2 timings (ns): read=284745, tokenize=3390822, sort=236725 ``` `std.StringHashMap` stores the canonical lowercase spellings, and a separate `std.ArrayList` collects the final `(word, count)` pairs for sorting. We choose `std.sort.heap` because it is deterministic, has no allocator dependencies, and performs well on small datasets; the comparator sorts primarily by descending count and secondarily by lexical ordering to keep ties stable. This is important when rerunning analyses across runs or machines—the field team can diff resulting CSVs without surprises. ## Section: Timing and Reproducibility [section_id: timing-and-reproducibility] [section_url: https://zigbook.net/chapters/53__project-top-k-word-frequency-analyzer#timing-and-reproducibility] A single `Timer` instance measures three phases: file ingestion, tokenization, and sorting. We call `lap()` after each phase to reset the zero point while recording elapsed nanoseconds, making it easy to spot which step dominates. Because the analyzer normalizes case and uses deterministic sorting, the output for a given corpus remains identical across runs, allowing timing deltas to be attributed to hardware or toolchain changes rather than nondeterministic ordering. For regressions, rerun with a larger `k` or a different corpus: ```shell $ zig run chapters-data/code/53__project-top-k-word-frequency-analyzer/topk_word_frequency.zig -- chapters-data/code/53__project-top-k-word-frequency-analyzer/sample_corpus.txt 10 ``` The optional arguments let you keep the binary scriptable—drop it into CI, compare output artifacts, and alert when the timing budget changes by more than a threshold. When integrating into bigger systems, the map-building loop can be swapped to stream from `stdin` or a TCP socket while preserving the same deterministic ranking rules.File.zig (https://github.com/ziglang/zig/tree/master/lib/std/fs/File.zig) ## Section: Notes & Caveats [section_id: notes-caveats] [section_url: https://zigbook.net/chapters/53__project-top-k-word-frequency-analyzer#notes-caveats] - `StringHashMap` does not free stored keys automatically; this sample releases them explicitly before dropping the map to keep the general-purpose allocator leak checker happy. - The tokenizer is ASCII-focused. For full Unicode segmentation, pair the pipeline with `std.unicode.ScalarIterator` or integrate ICU bindings.45 (45__text-formatting-and-unicode.xml) - Reading the entire corpus into memory simplifies the tutorial but may not suit multi-gigabyte logs. Swap `readFileAlloc` for chunked `readAll` loops or memory-mapped files when scaling.28 (28__filesystem-and-io.xml) ## Section: Exercises [section_id: exercises] [section_url: https://zigbook.net/chapters/53__project-top-k-word-frequency-analyzer#exercises] - Emit the report as JSON by serializing the sorted slice, then compare diff friendliness with the textual version.32 (32__project-http-json-client.xml)json.zig (https://github.com/ziglang/zig/tree/master/lib/std/json.zig) - Replace the single-threaded analyzer with a two-phase pipeline: shard tokens across threads, then merge hash maps before sorting. Measure the benefit using `Timer` and summarize the scaling.29 (29__threads-and-atomics.xml) - Add a `--stopwords` option that loads a newline-delimited ignore list, removes those tokens before counting, and reports how many candidates were filtered out.36 (36__style-and-best-practices.xml) ## Section: Caveats, Alternatives, Edge Cases [section_id: caveats-alternatives] [section_url: https://zigbook.net/chapters/53__project-top-k-word-frequency-analyzer#caveats-alternatives] - For streaming environments, consider `std.PriorityQueue` to maintain the top `k` incrementally instead of recording the entire histogram before sorting.priority_queue.zig (https://github.com/ziglang/zig/tree/master/lib/std/priority_queue.zig) - If performance requirements outgrow heap sort, experiment with `std.sort.pdq` or bucket-based approaches while keeping the deterministic comparator contract intact. - To support multi-locale text, layer in normalization (NFC/NFKC) and use Unicode-aware casing helpers; the comparator may need locale-specific collation to keep results intuitive.45 (45__text-formatting-and-unicode.xml) # Chapter 54 — Project [chapter_id: 54__project-zip-unzip-with-progress] [chapter_slug: project-zip-unzip-with-progress] [chapter_number: 54] [chapter_url: https://zigbook.net/chapters/54__project-zip-unzip-with-progress] ## Section: Overview [section_id: overview] [section_url: https://zigbook.net/chapters/54__project-zip-unzip-with-progress#overview] The previous project focused on deterministic text analytics; now we bundle those artifacts and the surrounding diagnostics into a reproducible archive pipeline. 53 (53__project-top-k-word-frequency-analyzer.xml) We will write a minimalist ZIP creator that streams files into memory, emits the central directory, then verifies extraction while reporting incremental progress. The program leans on the standard library’s ZIP reader, manual header encoding, `StringHashMap` bookkeeping for CRC32 checks, and structured status updates through `std.Progress`. zip.zig (https://github.com/ziglang/zig/tree/master/lib/std/zip.zig)hash_map.zig (https://github.com/ziglang/zig/tree/master/lib/std/hash_map.zig)crc.zig (https://github.com/ziglang/zig/tree/master/lib/std/hash/crc.zig)Progress.zig (https://github.com/ziglang/zig/tree/master/lib/std/Progress.zig) ## Section: Learning Goals [section_id: learning-goals] [section_url: https://zigbook.net/chapters/54__project-zip-unzip-with-progress#learning-goals] - Assemble a ZIP archive from scratch by writing Local File Headers, the Central Directory, and the End of Central Directory record in the correct order while honoring size and offset constraints. - Capture deterministic integrity metrics (CRC32, SHA-256) alongside the bundle so continuous integration can validate both structure and content on every run. crypto.zig (https://github.com/ziglang/zig/tree/master/lib/std/crypto.zig) - Surface analyst-friendly progress messages that stay scriptable by disabling the animated renderer and emitting plain-text checkpoints with `std.Progress`. ## Section: Designing the pipeline [section_id: pipeline-design] [section_url: https://zigbook.net/chapters/54__project-zip-unzip-with-progress#pipeline-design] The workflow runs in three phases: seed sample files, build an archive, and extract plus verify. Each phase increments the root progress node, producing deterministic console summaries that double as acceptance criteria. All filesystem operations occur under a temporary directory managed by `std.testing.tmpDir`, keeping the real workspace clean. 47 (47__time-logging-and-progress.xml)testing.zig (https://github.com/ziglang/zig/tree/master/lib/std/testing.zig) For archival metadata, we reuse the same relative paths when writing headers and when later validating the extracted files. Storing the CRC32 and byte count per path inside a `StringHashMap` gives us a straightforward way to diff expectations against actual outputs after extraction. ## Section: Archive assembly [section_id: archive-assembly] [section_url: https://zigbook.net/chapters/54__project-zip-unzip-with-progress#archive-assembly] Because Zig 0.15.2 ships a ZIP reader but not a writer, we build the archive in memory using an `ArrayList(u8)`, appending each component in sequence: Local File Header, filename, file bytes. Every header field is written with explicit little-endian helpers so the result is portable across architectures. Once the payloads land in the blob, we append the Central Directory (one record per file) followed by the End of Central Directory record, mirroring the structures defined in the PKWARE APPNOTE and encoded in `std.zip`. array_list.zig (https://github.com/ziglang/zig/tree/master/lib/std/array_list.zig)fmt.zig (https://github.com/ziglang/zig/tree/master/lib/std/fmt.zig) While writing headers we ensure sizes and offsets fit in 32-bit fields (sticking to the classic ZIP subset) and duplicate the filename once into the map so we can free resources deterministically later. After the archive image is complete, we persist it to disk and compute a SHA-256 digest for downstream regressions—the digest is rendered with `std.fmt.bytesToHex` so it can be compared inline without any extra tooling. ## Section: Extraction and verification [section_id: extraction-and-verification] [section_url: https://zigbook.net/chapters/54__project-zip-unzip-with-progress#extraction-and-verification] Extraction reuses the standard library iterator, which walks through each Central Directory record and hands the data stream to `std.zip.Entry.extract`; we normalize the root folder name through `std.zip.Diagnostics` so we can surface it to the caller. After each file lands on disk, we compute CRC32 again and compare the byte count against the recorded expectation. Any mismatch fails the program immediately, making it safe to embed in CI pipelines or deployment hooks. `std.Progress` nodes drive the console output: the root node tracks the three high-level stages, while child nodes count through the file list during seeding, building, and verification. Because printing is disabled, the final messages are ordinary text lines (rendered via a buffered stdout writer) that can be diffed verbatim in automated tests. 47 (47__time-logging-and-progress.xml) ## Section: End-to-end implementation [section_id: code-listing] [section_url: https://zigbook.net/chapters/54__project-zip-unzip-with-progress#code-listing] ```zig const std = @import("std"); const SampleFile = struct { path: []const u8, contents: []const u8, }; const sample_files = [_]SampleFile{ .{ .path = "input/metrics.txt", .contents = "uptime=420s\nrequests=1312\nerrors=3\n" }, .{ .path = "input/inventory.json", .contents = "{\n \"service\": \"telemetry\",\n \"shards\": [\"alpha\", \"beta\", \"gamma\"]\n}\n" }, .{ .path = "input/logs/app.log", .contents = "[info] ingest started\n[warn] queue delay=87ms\n[info] ingest completed\n" }, .{ .path = "input/README.md", .contents = "# Telemetry bundle\n\nSynthetic records used for the zip/unzip progress demo.\n" }, }; const EntryMetrics = struct { crc32: u32, size: usize, }; const BuildSummary = struct { bytes_written: usize, sha256: [32]u8, }; const VerifySummary = struct { files_checked: usize, total_bytes: usize, extracted_root: []const u8, owns_root: bool, }; const archive_path = "artifact/telemetry.zip"; const extract_root = "replay"; fn seedSamples(dir: std.fs.Dir, progress: *std.Progress.Node) !struct { files: usize, bytes: usize } { var total_bytes: usize = 0; for (sample_files) |sample| { if (std.fs.path.dirname(sample.path)) |parent| { try dir.makePath(parent); } var file = try dir.createFile(sample.path, .{ .truncate = true }); defer file.close(); try file.writeAll(sample.contents); total_bytes += sample.contents.len; progress.completeOne(); } return .{ .files = sample_files.len, .bytes = total_bytes }; } const EntryRecord = struct { name: []const u8, crc32: u32, size: u32, offset: u32, }; fn makeLocalHeader(name_len: u16, crc32: u32, size: u32) [30]u8 { var header: [30]u8 = undefined; header[0] = 'P'; header[1] = 'K'; header[2] = 3; header[3] = 4; std.mem.writeInt(u16, header[4..6], 20, .little); std.mem.writeInt(u16, header[6..8], 0, .little); std.mem.writeInt(u16, header[8..10], 0, .little); std.mem.writeInt(u16, header[10..12], 0, .little); std.mem.writeInt(u16, header[12..14], 0, .little); std.mem.writeInt(u32, header[14..18], crc32, .little); std.mem.writeInt(u32, header[18..22], size, .little); std.mem.writeInt(u32, header[22..26], size, .little); std.mem.writeInt(u16, header[26..28], name_len, .little); std.mem.writeInt(u16, header[28..30], 0, .little); return header; } fn makeCentralHeader(entry: EntryRecord) [46]u8 { var header: [46]u8 = undefined; header[0] = 'P'; header[1] = 'K'; header[2] = 1; header[3] = 2; std.mem.writeInt(u16, header[4..6], 0x0314, .little); std.mem.writeInt(u16, header[6..8], 20, .little); std.mem.writeInt(u16, header[8..10], 0, .little); std.mem.writeInt(u16, header[10..12], 0, .little); std.mem.writeInt(u16, header[12..14], 0, .little); std.mem.writeInt(u16, header[14..16], 0, .little); std.mem.writeInt(u32, header[16..20], entry.crc32, .little); std.mem.writeInt(u32, header[20..24], entry.size, .little); std.mem.writeInt(u32, header[24..28], entry.size, .little); const name_len_u16 = @as(u16, @intCast(entry.name.len)); std.mem.writeInt(u16, header[28..30], name_len_u16, .little); std.mem.writeInt(u16, header[30..32], 0, .little); std.mem.writeInt(u16, header[32..34], 0, .little); std.mem.writeInt(u16, header[34..36], 0, .little); std.mem.writeInt(u16, header[36..38], 0, .little); const unix_mode: u32 = 0o100644 << 16; std.mem.writeInt(u32, header[38..42], unix_mode, .little); std.mem.writeInt(u32, header[42..46], entry.offset, .little); return header; } fn makeEndRecord(cd_size: u32, cd_offset: u32, entry_count: u16) [22]u8 { var footer: [22]u8 = undefined; footer[0] = 'P'; footer[1] = 'K'; footer[2] = 5; footer[3] = 6; std.mem.writeInt(u16, footer[4..6], 0, .little); std.mem.writeInt(u16, footer[6..8], 0, .little); std.mem.writeInt(u16, footer[8..10], entry_count, .little); std.mem.writeInt(u16, footer[10..12], entry_count, .little); std.mem.writeInt(u32, footer[12..16], cd_size, .little); std.mem.writeInt(u32, footer[16..20], cd_offset, .little); std.mem.writeInt(u16, footer[20..22], 0, .little); return footer; } fn buildArchive( allocator: std.mem.Allocator, dir: std.fs.Dir, metrics: *std.StringHashMap(EntryMetrics), progress: *std.Progress.Node, ) !BuildSummary { if (std.fs.path.dirname(archive_path)) |parent| { try dir.makePath(parent); } var entries = try std.ArrayList(EntryRecord).initCapacity(allocator, sample_files.len); defer entries.deinit(allocator); try metrics.ensureTotalCapacity(sample_files.len); var blob: std.ArrayList(u8) = .empty; defer blob.deinit(allocator); for (sample_files) |sample| { if (sample.path.len > std.math.maxInt(u16)) return error.NameTooLong; var file = try dir.openFile(sample.path, .{}); defer file.close(); const max_len = 64 * 1024; const data = try file.readToEndAlloc(allocator, max_len); defer allocator.free(data); if (data.len > std.math.maxInt(u32)) return error.InputTooLarge; if (blob.items.len > std.math.maxInt(u32)) return error.ArchiveTooLarge; var crc = std.hash.crc.Crc32.init(); crc.update(data); const digest = crc.final(); const offset_u32 = @as(u32, @intCast(blob.items.len)); const size_u32 = @as(u32, @intCast(data.len)); const name_len_u16 = @as(u16, @intCast(sample.path.len)); const header = makeLocalHeader(name_len_u16, digest, size_u32); try blob.appendSlice(allocator, header[0..]); try blob.appendSlice(allocator, sample.path); try blob.appendSlice(allocator, data); try entries.append(allocator, .{ .name = sample.path, .crc32 = digest, .size = size_u32, .offset = offset_u32, }); const gop = try metrics.getOrPut(sample.path); if (!gop.found_existing) { gop.key_ptr.* = try allocator.dupe(u8, sample.path); } gop.value_ptr.* = .{ .crc32 = digest, .size = data.len }; progress.completeOne(); } const central_offset_usize = blob.items.len; if (central_offset_usize > std.math.maxInt(u32)) return error.ArchiveTooLarge; const central_offset = @as(u32, @intCast(central_offset_usize)); for (entries.items) |entry| { const header = makeCentralHeader(entry); try blob.appendSlice(allocator, header[0..]); try blob.appendSlice(allocator, entry.name); } const central_size = @as(u32, @intCast(blob.items.len - central_offset_usize)); const footer = makeEndRecord(central_size, central_offset, @as(u16, @intCast(entries.items.len))); try blob.appendSlice(allocator, footer[0..]); var zip_file = try dir.createFile(archive_path, .{ .truncate = true, .read = true }); defer zip_file.close(); try zip_file.writeAll(blob.items); var sha256 = std.crypto.hash.sha2.Sha256.init(.{}); sha256.update(blob.items); var digest_bytes: [32]u8 = undefined; sha256.final(&digest_bytes); return .{ .bytes_written = blob.items.len, .sha256 = digest_bytes }; } fn extractAndVerify( allocator: std.mem.Allocator, dir: std.fs.Dir, metrics: *const std.StringHashMap(EntryMetrics), progress: *std.Progress.Node, ) !VerifySummary { try dir.makePath(extract_root); var dest_dir = try dir.openDir(extract_root, .{ .access_sub_paths = true, .iterate = true }); defer dest_dir.close(); var file = try dir.openFile(archive_path, .{}); defer file.close(); var read_buf: [4096]u8 = undefined; var reader = file.reader(&read_buf); var diagnostics = std.zip.Diagnostics{ .allocator = allocator }; defer diagnostics.deinit(); try std.zip.extract(dest_dir, &reader, .{ .diagnostics = &diagnostics }); var files_checked: usize = 0; var total_bytes: usize = 0; for (sample_files) |sample| { var out_file = try dest_dir.openFile(sample.path, .{}); defer out_file.close(); const data = try out_file.readToEndAlloc(allocator, 64 * 1024); defer allocator.free(data); const expected = metrics.get(sample.path) orelse return error.ExpectedEntryMissing; var crc = std.hash.crc.Crc32.init(); crc.update(data); if (crc.final() != expected.crc32 or data.len != expected.size) { return error.VerificationFailed; } files_checked += 1; total_bytes += data.len; progress.completeOne(); } var result_root: []const u8 = ""; var owns_root = false; if (diagnostics.root_dir.len > 0) { result_root = try allocator.dupe(u8, diagnostics.root_dir); owns_root = true; } return .{ .files_checked = files_checked, .total_bytes = total_bytes, .extracted_root = result_root, .owns_root = owns_root, }; } pub fn main() !void { var gpa = std.heap.GeneralPurposeAllocator(.{}){}; defer { const leak_status = gpa.deinit(); std.debug.assert(leak_status == .ok); } const allocator = gpa.allocator(); var stdout_buffer: [512]u8 = undefined; var stdout_writer = std.fs.File.stdout().writer(&stdout_buffer); const out = &stdout_writer.interface; var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); var metrics = std.StringHashMap(EntryMetrics).init(allocator); defer { var it = metrics.iterator(); while (it.next()) |kv| { allocator.free(kv.key_ptr.*); } metrics.deinit(); } var progress_root = std.Progress.start(.{ .root_name = "zip-pipeline", .estimated_total_items = 3, .disable_printing = true, }); defer progress_root.end(); var stage_seed = progress_root.start("seed", sample_files.len); const seeded = try seedSamples(tmp.dir, &stage_seed); stage_seed.end(); try out.print("[1/3] seeded samples -> files={d}, bytes={d}\n", .{ seeded.files, seeded.bytes }); var stage_build = progress_root.start("build", sample_files.len); const build_summary = try buildArchive(allocator, tmp.dir, &metrics, &stage_build); stage_build.end(); const hex_digest = std.fmt.bytesToHex(build_summary.sha256, .lower); try out.print("[2/3] built archive -> bytes={d}\n sha256={s}\n", .{ build_summary.bytes_written, hex_digest[0..] }); var stage_verify = progress_root.start("verify", sample_files.len); const verify_summary = try extractAndVerify(allocator, tmp.dir, &metrics, &stage_verify); stage_verify.end(); defer if (verify_summary.owns_root) allocator.free(verify_summary.extracted_root); try out.print( "[3/3] extracted + verified -> files={d}, bytes={d}, root={s}\n", .{ verify_summary.files_checked, verify_summary.total_bytes, verify_summary.extracted_root }, ); try out.flush(); } ``` Run: ```shell $ zig run zip_progress_pipeline.zig ``` Output: ```shell [1/3] seeded samples -> files=4, bytes=250 [2/3] built archive -> bytes=716 sha256=4a13a3dc1e6ef90c252b0cc797ff14456aa28c670cafbc9d27a025b0079b05d5 [3/3] extracted + verified -> files=4, bytes=250, root=input ``` The verification step intentionally duplicates the extracted root string when diagnostics discover a common prefix; the summary frees that buffer afterward to keep the general-purpose allocator clean. This mirrors good hygiene for CLI utilities that stream large archives through temporary directories. 52 (52__debug-and-valgrind.xml) ## Section: Notes & Caveats [section_id: notes-caveats] [section_url: https://zigbook.net/chapters/54__project-zip-unzip-with-progress#notes-caveats] - The writer sticks to the classic (non-Zip64) subset; once files exceed 4 GiB you must upgrade the headers and extra fields, or delegate to a dedicated ZIP library. 44 (44__collections-and-algorithms.xml) - Progress nodes are nested but printing is disabled; if you want live TTY updates, drop `.disable_printing = true` and let the renderer clear frames. Remember that doing so sacrifices determinism in captured logs. 47 (47__time-logging-and-progress.xml) - CRC32 confirms integrity but not authenticity. Combine the SHA-256 digest with a signature or attach the archive to a `zig build` step for reproducible deployment pipelines. 39 (39__performance-and-inlining.xml) ## Section: Exercises [section_id: exercises] [section_url: https://zigbook.net/chapters/54__project-zip-unzip-with-progress#exercises] - Extend the builder to emit Zip64 records when any file crosses the 4 GiB boundary. Keep the legacy path for small bundles and write regression tests that validate both. 33 (33__c-interop-import-export-abi.xml) - Replace the in-memory blob with a streaming writer that flushes to disk in chunks; compare throughput and memory consumption under `perf` or `zig build test` with large synthetic files. 41 (41__cross-compilation-and-wasm.xml) - Add a command-line flag that accepts an ignore list (glob patterns) before archiving, then report the exact number of skipped files alongside the existing totals. 36 (36__style-and-best-practices.xml)Dir.zig (https://github.com/ziglang/zig/tree/master/lib/std/fs/Dir.zig) ## Section: Caveats, alternatives, edge cases [section_id: caveats-alternatives] [section_url: https://zigbook.net/chapters/54__project-zip-unzip-with-progress#caveats-alternatives] - Streaming archives straight to stdout is great for pipelines but makes verification trickier; consider writing to a temporary file first so you can re-open it for checksums before shipping it onward. 28 (28__filesystem-and-io.xml)File.zig (https://github.com/ziglang/zig/tree/master/lib/std/fs/File.zig) - ZIP encryption is intentionally out of scope. If you need confidentiality, wrap the resulting file with `std.crypto` primitives or switch to formats like encrypted tarballs with age or minisign. 45 (45__text-formatting-and-unicode.xml) - For multi-gigabyte corpora, read inputs in chunks and update CRC32 incrementally rather than calling `readToEndAlloc`; otherwise the temporary allocator will balloon. 10 (10__allocators-and-memory-management.xml) # Chapter 55 — Appendix A. Style Guide Highlights [chapter_id: 55__style-guide-highlights] [chapter_slug: style-guide-highlights] [chapter_number: 55] [chapter_url: https://zigbook.net/chapters/55__style-guide-highlights] ## Section: Overview [section_id: overview] [section_url: https://zigbook.net/chapters/55__style-guide-highlights#overview] Teams stay nimble when their naming, comments, and module layout follow a predictable rhythm. This appendix distills the house style into a quick reference that you can keep open while reviewing pull requests or scaffolding new modules. Zig 0.15.2 tightened formatter output, stabilized doc comment handling, and clarified testing ergonomics; adopting those defaults means less time negotiating conventions and more time verifying behavior.v0.15.2 (https://ziglang.org/download/0.15.1/release-notes.html) ## Section: Learning Goals [section_id: learning-goals] [section_url: https://zigbook.net/chapters/55__style-guide-highlights#learning-goals] - Audit a module quickly by scanning for the canonical ordering of doc comments, types, functions, and tests. - Describe what “tight error vocabulary” means in Zig and when to prefer bespoke error sets over `anyerror`. - Wire deterministic tests alongside the code they document without sacrificing readability in larger files. _Refs: _ ## Section: Voice & Naming at a Glance [section_id: voice-naming] [section_url: https://zigbook.net/chapters/55__style-guide-highlights#voice-naming] Readable code starts with alignment between prose and identifiers: the doc comment should speak the same nouns that the exported symbol implements, while helper functions keep verbs short and active.fmt.zig (https://github.com/ziglang/zig/tree/master/lib/std/fmt.zig) Following this pattern lets reviewers focus on semantics instead of debating word choice. ### Subsection: Naming, Comments, and Writers [section_id: naming-comments] [section_url: https://zigbook.net/chapters/55__style-guide-highlights#naming-comments] This example pairs module-level narration with focused doc comments and uses a fixed buffer writer so the tests never touch the allocator.Io.zig (https://github.com/ziglang/zig/tree/master/lib/std/Io.zig) ```zig //! Demonstrates naming and documentation conventions for a small diagnostic helper. const std = @import("std"); /// Represents a labelled temperature reading captured during diagnostics. pub const TemperatureReading = struct { label: []const u8, value_celsius: f32, /// Writes the reading to the provided writer using canonical casing and units. pub fn format(self: TemperatureReading, writer: anytype) !void { try writer.print("{s}: {d:.1}°C", .{ self.label, self.value_celsius }); } }; /// Creates a reading with the given label and temperature value in Celsius. pub fn createReading(label: []const u8, value_celsius: f32) TemperatureReading { return .{ .label = label, .value_celsius = value_celsius, }; } test "temperature readings print with consistent label casing" { const reading = createReading("CPU", 72.25); var backing: [64]u8 = undefined; var stream = std.io.fixedBufferStream(&backing); try reading.format(stream.writer()); const rendered = stream.getWritten(); try std.testing.expectEqualStrings("CPU: 72.3°C", rendered); } ``` Run: ```shell $ zig test 01_naming_and_comments.zig ``` Output: ```shell All 1 tests passed. ``` TIP: Formatting the descriptive sentence before the code encourages readers to skim the type signature and the test together; keeping terminology aligned with the doc comment mirrors the advice in Chapter 36.36 (36__style-and-best-practices.xml) ### Subsection: Tight Error Vocabularies [section_id: error-vocabulary] [section_url: https://zigbook.net/chapters/55__style-guide-highlights#error-vocabulary] Precise error sets balance empathy for callers with lightweight control flow; instead of returning `anyerror`, we list exactly the states the parser can reach and promote them to public API surface.math.zig (https://github.com/ziglang/zig/tree/master/lib/std/math.zig) ```zig //! Keeps error vocabulary tight for a numeric parser so callers can react precisely. const std = @import("std"); /// Enumerates the failure modes that the parser can surface to its callers. pub const ParseCountError = error{ EmptyInput, InvalidDigit, Overflow, }; /// Parses a decimal counter while preserving descriptive error information. pub fn parseCount(input: []const u8) ParseCountError!u32 { if (input.len == 0) return ParseCountError.EmptyInput; var acc: u64 = 0; for (input) |char| { if (char < '0' or char > '9') return ParseCountError.InvalidDigit; const digit: u64 = @intCast(char - '0'); acc = acc * 10 + digit; if (acc > std.math.maxInt(u32)) return ParseCountError.Overflow; } return @intCast(acc); } test "parseCount reports invalid digits precisely" { try std.testing.expectEqual(@as(u32, 42), try parseCount("42")); try std.testing.expectError(ParseCountError.InvalidDigit, parseCount("4a")); try std.testing.expectError(ParseCountError.EmptyInput, parseCount("")); try std.testing.expectError(ParseCountError.Overflow, parseCount("42949672960")); } ``` Run: ```shell $ zig test 02_error_vocabulary.zig ``` Output: ```shell All 1 tests passed. ``` NOTE: The test suite demonstrates that each branch stays reachable, preventing dead strings and teaching consumers which names to `switch` on without reading the implementation.36 (36__style-and-best-practices.xml) ### Subsection: Module Layout Checklist [section_id: module-layout] [section_url: https://zigbook.net/chapters/55__style-guide-highlights#module-layout] When a file exports configuration helpers, keep the public façade first, collect private validators underneath, and end with table-driven tests that read as documentation.12 (12__config-as-data.xml) ```zig //! Highlights a layered module layout with focused helper functions and tests. const std = @import("std"); /// Errors that can emerge while normalizing user-provided retry policies. pub const RetryPolicyError = error{ ZeroAttempts, ExcessiveDelay, }; /// Encapsulates retry behaviour for a network client, including sensible defaults. pub const RetryPolicy = struct { max_attempts: u8 = 3, delay_ms: u32 = 100, /// Indicates whether exponential backoff is active. pub fn isBackoffEnabled(self: RetryPolicy) bool { return self.delay_ms > 0 and self.max_attempts > 1; } }; /// Partial options provided by configuration files or CLI flags. pub const PartialRetryOptions = struct { max_attempts: ?u8 = null, delay_ms: ?u32 = null, }; /// Builds a retry policy from optional overrides while keeping default reasoning centralized. pub fn makeRetryPolicy(options: PartialRetryOptions) RetryPolicy { return RetryPolicy{ .max_attempts = options.max_attempts orelse 3, .delay_ms = options.delay_ms orelse 100, }; } fn validate(policy: RetryPolicy) RetryPolicyError!RetryPolicy { if (policy.max_attempts == 0) return RetryPolicyError.ZeroAttempts; if (policy.delay_ms > 60_000) return RetryPolicyError.ExcessiveDelay; return policy; } /// Produces a validated policy, emphasising the flow from raw input to constrained output. pub fn finalizeRetryPolicy(options: PartialRetryOptions) RetryPolicyError!RetryPolicy { const policy = makeRetryPolicy(options); return validate(policy); } test "finalize rejects zero attempts" { try std.testing.expectError( RetryPolicyError.ZeroAttempts, finalizeRetryPolicy(.{ .max_attempts = 0 }), ); } test "finalize accepts defaults" { const policy = try finalizeRetryPolicy(.{}); try std.testing.expectEqual(@as(u8, 3), policy.max_attempts); try std.testing.expect(policy.isBackoffEnabled()); } ``` Run: ```shell $ zig test 03_module_layout.zig ``` Output: ```shell All 2 tests passed. ``` TIP: Locating the error set at the top keeps the type graph obvious and mirrors how `std.testing` materializes invariants right next to the code that depends on them.testing.zig (https://github.com/ziglang/zig/tree/master/lib/std/testing.zig) ## Section: Patterns to Keep on Hand [section_id: patterns] [section_url: https://zigbook.net/chapters/55__style-guide-highlights#patterns] - Reserve `//!` for module-level narration and `///` for API documentation so generated references keep a consistent voice across packages.36 (36__style-and-best-practices.xml) - Pair every exposed helper with a focused test block; Zig’s test runner makes colocated tests free, and they double as executable usage examples. - When the formatter reflows signatures, accept its judgment—consistency between editors and CI was one of the major quality-of-life improvements in 0.15.x. ## Section: Notes & Caveats [section_id: notes-caveats] [section_url: https://zigbook.net/chapters/55__style-guide-highlights#notes-caveats] - Do not suppress warnings from `zig fmt`; instead, adjust the code so the defaults succeed and document any unavoidable divergence in your contributing guide.36 (36__style-and-best-practices.xml) - Keep project-local lint scripts in sync with upstream Zig releases so chore churn stays low during toolchain upgrades. - If your API emits container types from `std`, reference their exact field names in doc comments—callers can jump to `zig/lib/std` directly to confirm semantics.hash_map.zig (https://github.com/ziglang/zig/tree/master/lib/std/hash_map.zig) ## Section: Exercises [section_id: exercises] [section_url: https://zigbook.net/chapters/55__style-guide-highlights#exercises] - Rewrite one of your recent modules by grouping constants, types, functions, and tests in the order shown above, then run `zig fmt` to confirm the structure stays stable.36 (36__style-and-best-practices.xml) - Extend `parseCount` to accept underscores for readability while maintaining a strict error vocabulary; add targeted tests for the new branch. - Generate HTML documentation for a project using `zig build doc` and review how `//!` and `///` comments surface—tune the prose until the output reads smoothly. ## Section: Alternatives & Edge Cases [section_id: caveats-alternatives-edge-cases] [section_url: https://zigbook.net/chapters/55__style-guide-highlights#caveats-alternatives-edge-cases] - Some teams prefer fully separated test files; if you do, adopt the same naming and doc comment patterns so search results stay predictable.36 (36__style-and-best-practices.xml) - For modules that expose comptime-heavy APIs, include a `test "comptime"` block so these guidelines still deliver runnable coverage.15 (15__comptime-and-reflection.xml) - When vendoring third-party code, annotate deviations from this style in a short README so reviewers know the divergence is intentional.Build.zig (https://github.com/ziglang/zig/tree/master/lib/std/Build.zig) # Chapter 56 — Appendix B. Builtins Quick Reference [chapter_id: 56__builtins-quick-reference] [chapter_slug: builtins-quick-reference] [chapter_number: 56] [chapter_url: https://zigbook.net/chapters/56__builtins-quick-reference] ## Section: Overview [section_id: overview] [section_url: https://zigbook.net/chapters/56__builtins-quick-reference#overview] `@builtins` are the compiler’s verbs; they describe how Zig thinks about types, pointers, and program structure, and they are available in every file without imports. After experimenting with compile-time programming in Part III, this appendix captures the most common builtins, their intent, and the surface-level contracts you should remember when reading or writing metaprogramming-heavy Zig. 15 (15__comptime-and-reflection.xml) The 0.15.2 release stabilized several introspection helpers (`@typeInfo`, `@hasDecl`, `@field`) and clarified truncation semantics for new integer sizes, making it practical to rely on the behaviors summarized here. v0.15.2 (https://ziglang.org/download/0.15.1/release-notes.html) ## Section: Learning Goals [section_id: learning-goals] [section_url: https://zigbook.net/chapters/56__builtins-quick-reference#learning-goals] - Spot the difference between reflection builtins, arithmetic helpers, and control builtins when scanning a codebase. - Combine type inspection builtins to build adapters that work with user-provided types. - Verify the runtime behavior of numeric conversions at the edges of range and safety modes. ## Section: Core Reflection Builtins [section_id: core-reflection] [section_url: https://zigbook.net/chapters/56__builtins-quick-reference#core-reflection] Reflection builtins give us structured information about user types without grabbing raw pointers or discarding safety checks. 15 (15__comptime-and-reflection.xml) The example below shows how to form a documented summary of any struct, including comptime fields, optional payloads, and nested arrays. ```zig //! Summarizes struct metadata using @typeInfo and @field. const std = @import("std"); fn describeStruct(comptime T: type, writer: anytype) !void { const info = @typeInfo(T); switch (info) { .@"struct" => |struct_info| { try writer.print("struct {s} has {d} fields", .{ @typeName(T), struct_info.fields.len }); inline for (struct_info.fields, 0..) |field, index| { try writer.print("\n {d}: {s} : {s}", .{ index, field.name, @typeName(field.type) }); } }, else => try writer.writeAll("not a struct"), } } test "describe struct reports field metadata" { const Sample = struct { id: u32, value: ?f64, }; var buffer: [256]u8 = undefined; var stream = std.io.fixedBufferStream(&buffer); try describeStruct(Sample, stream.writer()); const summary = stream.getWritten(); try std.testing.expect(std.mem.containsAtLeast(u8, summary, 1, "id")); try std.testing.expect(std.mem.containsAtLeast(u8, summary, 1, "value")); } test "describe struct rejects non-struct types" { var buffer: [32]u8 = undefined; var stream = std.io.fixedBufferStream(&buffer); try describeStruct(u8, stream.writer()); const summary = stream.getWritten(); try std.testing.expectEqualStrings("not a struct", summary); } ``` Run: ```shell $ zig test 01_struct_introspection.zig ``` Output: ```shell All 2 tests passed. ``` TIP: Use `@typeInfo` plus `@field` inside inline loops so the compiler still optimizes away branches after specialization. 17 (17__generic-apis-and-type-erasure.xml) ## Section: Value Extraction Helpers [section_id: value-extraction] [section_url: https://zigbook.net/chapters/56__builtins-quick-reference#value-extraction] Builtins such as `@field`, `@hasField`, and `@fieldParentPtr` let you map runtime data back to compile-time declarations without violating Zig’s strict aliasing rules. The following snippet shows how to surface parent pointers while maintaining const-correctness. meta.zig (https://github.com/ziglang/zig/tree/master/lib/std/meta.zig) ```zig //! Demonstrates `@fieldParentPtr` to recover container pointers safely. const std = @import("std"); const Node = struct { id: u32, payload: Payload, }; const Payload = struct { node_ptr: *const Node, value: []const u8, }; fn makeNode(id: u32, value: []const u8) Node { var node = Node{ .id = id, .payload = undefined, }; node.payload = Payload{ .node_ptr = &node, .value = value, }; return node; } test "parent pointer recovers owning node" { var node = makeNode(7, "ready"); const parent: *const Node = @fieldParentPtr("payload", &node.payload); try std.testing.expectEqual(@as(u32, 7), parent.id); } test "field access respects const rules" { var node = makeNode(3, "go"); const parent: *const Node = @fieldParentPtr("payload", &node.payload); try std.testing.expectEqualStrings("go", parent.payload.value); } ``` Run: ```shell $ zig test 02_parent_ptr_lookup.zig ``` Output: ```shell All 2 tests passed. ``` NOTE: `@fieldParentPtr` assumes the child pointer is valid and properly aligned; combine it with `std.debug.assert` in debug builds to catch accidental misuse early. 37 (37__illegal-behavior-and-safety-modes.xml) ## Section: Numeric Safety Builtins [section_id: numeric-safety] [section_url: https://zigbook.net/chapters/56__builtins-quick-reference#numeric-safety] Numeric conversions are where undefined behavior often hides; Zig makes truncation explicit via `@intCast`, `@intFromFloat`, and `@truncate`, which all obey safety-mode semantics. 37 (37__illegal-behavior-and-safety-modes.xml) 0.15.2 refined the diagnostics these builtins emit when overflow occurs, making them reliable guards in debug builds. ```zig //! Exercises numeric conversion builtins with guarded tests. const std = @import("std"); fn toU8Lossy(value: u16) u8 { return @truncate(value); } fn toI32(value: f64) i32 { return @intFromFloat(value); } fn widenU16(value: u8) u16 { return @intCast(value); } test "truncate discards high bits" { try std.testing.expectEqual(@as(u8, 0x34), toU8Lossy(0x1234)); } test "intFromFloat matches floor for positive range" { try std.testing.expectEqual(@as(i32, 42), toI32(42.9)); } test "intCast widens without loss" { try std.testing.expectEqual(@as(u16, 255), widenU16(255)); } ``` Run: ```shell $ zig test 03_numeric_conversions.zig ``` Output: ```shell All 3 tests passed. ``` TIP: Wrap lossy conversions in small helper functions so the intent stays readable and you can centralize assertions around shared digit logic. 10 (10__allocators-and-memory-management.xml) ## Section: Comptime Control & Guards [section_id: comptime-control] [section_url: https://zigbook.net/chapters/56__builtins-quick-reference#comptime-control] `@compileError`, `@panic`, `@setEvalBranchQuota`, and `@inComptime` give you direct control over compile-time execution; they are the safety valves that keep metaprogramming deterministic and transparent. 15 (15__comptime-and-reflection.xml) The short example below guards vector widths at compile time and raises the evaluation branch quota before computing a small Fibonacci number during analysis. ```zig //! Demonstrates compile-time guards using @compileError and @setEvalBranchQuota. const std = @import("std"); fn ensureVectorLength(comptime len: usize) type { if (len < 2) { @compileError("invalid vector length; expected at least 2 lanes"); } return @Vector(len, u8); } fn boundedFib(comptime quota: u32, comptime n: u32) u64 { @setEvalBranchQuota(quota); return comptimeFib(n); } fn comptimeFib(comptime n: u32) u64 { if (n <= 1) return n; return comptimeFib(n - 1) + comptimeFib(n - 2); } test "guard accepts valid size" { const Vec = ensureVectorLength(4); const info = @typeInfo(Vec); try std.testing.expectEqual(@as(usize, 4), info.vector.len); // Uncommenting the next line triggers the compile-time guard: // const invalid = ensureVectorLength(1); } test "branch quota enables deeper recursion" { const result = comptime boundedFib(1024, 12); try std.testing.expectEqual(@as(u64, 144), result); } ``` Run: ```shell $ zig test 04_comptime_guards.zig ``` Output: ```shell All 2 tests passed. ``` CAUTION: `@compileError` stops the compilation unit immediately; use it sparingly and prefer returning an error when runtime validation is cheaper. Leave a commented-out call (as in the example) to document the failure mode without breaking the build. 12 (12__config-as-data.xml) ## Section: Cross-Checking Patterns [section_id: patterns] [section_url: https://zigbook.net/chapters/56__builtins-quick-reference#patterns] - Drive refactors with `@hasDecl` and `@hasField` before depending on optional features from user types; this matches the defensive style introduced in Chapter 17 (17__generic-apis-and-type-erasure.xml). - Combine `@TypeOf`, `@typeInfo`, and `@fieldParentPtr` to keep diagnostics clear in validation code—the trio makes it easy to print structural information when invariants fail. - Remember that some builtins (like `@This`) depend on lexical scope; reorganizing your file can silently change their meaning, so rerun tests after every major rearrangement. 36 (36__style-and-best-practices.xml) ## Section: Notes & Caveats [section_id: notes-caveats] [section_url: https://zigbook.net/chapters/56__builtins-quick-reference#notes-caveats] - Builtins that interact with the allocator (`@alignCast`, `@ptrCast`) still obey Zig’s aliasing rules; rely on `std.mem` helpers when in doubt. 3 (03__data-fundamentals.xml) - `@setEvalBranchQuota` is global to the current compile-time execution context; keep quotas narrow to avoid masking infinite recursion. 15 (15__comptime-and-reflection.xml) - Some experimental builtins appear in nightly builds but not in 0.15.2—pin your tooling before adopting new names. ## Section: Exercises [section_id: exercises] [section_url: https://zigbook.net/chapters/56__builtins-quick-reference#exercises] - Build a diagnostic helper that prints the tag names of any union using `@typeInfo.union`. 17 (17__generic-apis-and-type-erasure.xml) - Extend the numeric conversions example to emit a human-readable diff between bit patterns before and after truncation. fmt.zig (https://github.com/ziglang/zig/tree/master/lib/std/fmt.zig) - Write a compile-time guard that rejects structs lacking a `name` field, then integrate it into a generic formatter pipeline. 36 (36__style-and-best-practices.xml) ## Section: Alternatives & Edge Cases [section_id: caveats-alternatives-edge-cases] [section_url: https://zigbook.net/chapters/56__builtins-quick-reference#caveats-alternatives-edge-cases] - Prefer higher-level std helpers when a builtin duplicates existing behavior—the standard library often wraps edge cases for you. 43 (43__stdlib-index.xml) - Reflection against anonymous structs can produce compiler-generated names; cache them in your own metadata if user-facing logs need stability. 12 (12__config-as-data.xml) - When interfacing with C, remember that some builtins (e.g. `@ptrCast`) can affect calling conventions; double-check the ABI section before deploying. 33 (33__c-interop-import-export-abi.xml) # Chapter 57 — Appendix C. Error-Handling Patterns Cookbook [chapter_id: 57__error-handling-patterns-cookbook] [chapter_slug: error-handling-patterns-cookbook] [chapter_number: 57] [chapter_url: https://zigbook.net/chapters/57__error-handling-patterns-cookbook] ## Section: Overview [section_id: overview] [section_url: https://zigbook.net/chapters/57__error-handling-patterns-cookbook#overview] Chapter 4 introduced the mechanics of Zig’s error unions, `try`, and `errdefer`; this appendix turns those ideas into a quick-reference cookbook you can consult while sketching new APIs or refactoring existing ones. Each recipe tightens the link between domain-specific error vocabularies and the diagnostic messages your users ultimately see. Zig 0.15.2 refined diagnostics around integer casts and allocator failures, making it easier to rely on precise error propagation in both debug and release-safe builds.v0.15.2 (https://ziglang.org/download/0.15.1/release-notes.html) ## Section: Learning Goals [section_id: learning-goals] [section_url: https://zigbook.net/chapters/57__error-handling-patterns-cookbook#learning-goals] - Layer domain-specific error sets on top of standard Zig I/O failures without losing precision. - Guard heap-backed transformations with `errdefer` so that every exit path pairs allocations and deallocations. - Translate internal error unions into clear, actionable messages for logs and user interfaces. _Refs: _ ## Section: Layered Error Vocabularies [section_id: layered-error-vocabularies] [section_url: https://zigbook.net/chapters/57__error-handling-patterns-cookbook#layered-error-vocabularies] When a subsystem introduces its own error conditions, refine the vocabulary instead of throwing everything into `anyerror`. The pattern below composes a configuration-specific union from parsing failures and simulated I/O errors so the caller never loses track of `NotFound` versus `InvalidPort`.4 (04__errors-resource-cleanup.xml) The `catch |err| switch` idiom keeps the mapping honest and mirrors how `std.fmt.parseInt` surfaces parsing issues.fmt.zig (https://github.com/ziglang/zig/tree/master/lib/std/fmt.zig) ```zig //! Demonstrates layering domain-specific error sets when loading configuration. const std = @import("std"); pub const ParseError = error{ MissingField, InvalidPort, }; pub const SourceError = error{ NotFound, PermissionDenied, }; pub const LoadError = SourceError || ParseError; const SimulatedSource = struct { payload: ?[]const u8 = null, failure: ?SourceError = null, fn fetch(self: SimulatedSource) SourceError![]const u8 { if (self.failure) |err| return err; return self.payload orelse SourceError.NotFound; } }; fn parsePort(text: []const u8) ParseError!u16 { var iter = std.mem.splitScalar(u8, text, '='); const key = iter.next() orelse return ParseError.MissingField; const value = iter.next() orelse return ParseError.MissingField; if (!std.mem.eql(u8, key, "PORT")) return ParseError.MissingField; return std.fmt.parseInt(u16, value, 10) catch ParseError.InvalidPort; } pub fn loadPort(source: SimulatedSource) LoadError!u16 { const line = source.fetch() catch |err| switch (err) { SourceError.NotFound => return LoadError.NotFound, SourceError.PermissionDenied => return LoadError.PermissionDenied, }; return parsePort(line) catch |err| switch (err) { ParseError.MissingField => return LoadError.MissingField, ParseError.InvalidPort => return LoadError.InvalidPort, }; } test "successful load yields parsed port" { const source = SimulatedSource{ .payload = "PORT=8080" }; try std.testing.expectEqual(@as(u16, 8080), try loadPort(source)); } test "parse errors bubble through composed union" { const source = SimulatedSource{ .payload = "HOST=example" }; try std.testing.expectError(LoadError.MissingField, loadPort(source)); } test "source failures remain precise" { const source = SimulatedSource{ .failure = SourceError.PermissionDenied }; try std.testing.expectError(LoadError.PermissionDenied, loadPort(source)); } ``` Run: ```shell $ zig test 01_layered_error_sets.zig ``` Output: ```shell All 3 tests passed. ``` TIP: Preserve the original error names all the way to your API boundary—callers can branch on `LoadError.PermissionDenied` explicitly, which is more robust than string matching or sentinel values.36 (36__style-and-best-practices.xml) ## Section: errdefer for Balanced Cleanup [section_id: errdefer-patterns] [section_url: https://zigbook.net/chapters/57__error-handling-patterns-cookbook#errdefer-patterns] String assembly and JSON shaping often allocate temporary buffers; forgetting to free them when a validation step fails leads straight to leaks. By pairing `std.ArrayListUnmanaged` with `errdefer`, the next recipe guarantees both success and failure paths clean up correctly while still returning a convenient owned slice.13 (13__testing-and-leak-detection.xml) Every allocation helper used here ships in the standard library, so the same structure scales to more complex builders.array_list.zig (https://github.com/ziglang/zig/tree/master/lib/std/array_list.zig) ```zig //! Shows how errdefer keeps allocations balanced when joining user snippets. const std = @import("std"); pub const SnippetError = error{EmptyInput} || std.mem.Allocator.Error; pub fn joinUpperSnippets(allocator: std.mem.Allocator, parts: []const []const u8) SnippetError![]u8 { if (parts.len == 0) return SnippetError.EmptyInput; var list = std.ArrayListUnmanaged(u8){}; errdefer list.deinit(allocator); for (parts, 0..) |part, index| { if (index != 0) try list.append(allocator, ' '); for (part) |ch| try list.append(allocator, std.ascii.toUpper(ch)); } return list.toOwnedSlice(allocator); } test "joinUpperSnippets capitalizes and joins input" { const allocator = std.testing.allocator; const result = try joinUpperSnippets(allocator, &[_][]const u8{ "zig", "cookbook" }); defer allocator.free(result); try std.testing.expectEqualStrings("ZIG COOKBOOK", result); } test "joinUpperSnippets surfaces empty-input error" { const allocator = std.testing.allocator; try std.testing.expectError(SnippetError.EmptyInput, joinUpperSnippets(allocator, &[_][]const u8{})); } ``` Run: ```shell $ zig test 02_errdefer_join_upper.zig ``` Output: ```shell All 2 tests passed. ``` NOTE: Because the standard testing allocator trips on leaks automatically, exercising both the success and error branches doubles as a regression harness for future edits.13 (13__testing-and-leak-detection.xml) ## Section: Translating Errors for Humans [section_id: reporting-bridges] [section_url: https://zigbook.net/chapters/57__error-handling-patterns-cookbook#reporting-bridges] Even the best-crafted error sets need to land with empathetic language. The final pattern demonstrates how to keep the original `ApiError` for programmatic callers while producing human-readable prose for logs or UI copy.36 (36__style-and-best-practices.xml)`std.io.fixedBufferStream` makes the output deterministic for tests, and the dedicated formatter isolates messaging from control flow.log.zig (https://github.com/ziglang/zig/tree/master/lib/std/log.zig) ```zig //! Bridges domain errors to user-facing log messages. const std = @import("std"); pub const ApiError = error{ NotFound, RateLimited, Backend, }; fn describeApiError(err: ApiError, writer: anytype) !void { switch (err) { ApiError.NotFound => try writer.writeAll("resource not found; check identifier"), ApiError.RateLimited => try writer.writeAll("rate limit exceeded; retry later"), ApiError.Backend => try writer.writeAll("upstream dependency failed; escalate"), } } const Action = struct { outcomes: []const ?ApiError, index: usize = 0, fn invoke(self: *Action) ApiError!void { if (self.index >= self.outcomes.len) return; const outcome = self.outcomes[self.index]; self.index += 1; if (outcome) |err| { return err; } } }; pub fn runAndReport(action: *Action, writer: anytype) !void { action.invoke() catch |err| { try writer.writeAll("Request failed: "); try describeApiError(err, writer); return; }; try writer.writeAll("Request succeeded"); } test "runAndReport surfaces friendly error message" { var action = Action{ .outcomes = &[_]?ApiError{ApiError.NotFound} }; var buffer: [128]u8 = undefined; var stream = std.io.fixedBufferStream(&buffer); try runAndReport(&action, stream.writer()); const message = stream.getWritten(); try std.testing.expectEqualStrings("Request failed: resource not found; check identifier", message); } test "runAndReport acknowledges success" { var action = Action{ .outcomes = &[_]?ApiError{null} }; var buffer: [64]u8 = undefined; var stream = std.io.fixedBufferStream(&buffer); try runAndReport(&action, stream.writer()); const message = stream.getWritten(); try std.testing.expectEqualStrings("Request succeeded", message); } ``` Run: ```shell $ zig test 03_error_reporting_bridge.zig ``` Output: ```shell All 2 tests passed. ``` TIP: Keep the bridge function pure—it should only depend on the error payload and a writer—so consumers can swap logging backends or capture diagnostics in-memory during tests.36 (36__style-and-best-practices.xml) ## Section: Patterns to Keep on Hand [section_id: patterns] [section_url: https://zigbook.net/chapters/57__error-handling-patterns-cookbook#patterns] - Bubble lower-level errors verbatim until the last responsible boundary, then convert them in one place to keep invariants obvious.4 (04__errors-resource-cleanup.xml) - Treat `errdefer` as a handshake: every allocation or file open should have a matching cleanup within the same scope.fs.zig (https://github.com/ziglang/zig/tree/master/lib/std/fs.zig) - Give each public error union a dedicated formatter so documentation and user messaging never drift apart.36 (36__style-and-best-practices.xml) ## Section: Notes & Caveats [section_id: notes-caveats] [section_url: https://zigbook.net/chapters/57__error-handling-patterns-cookbook#notes-caveats] - Merging error sets with `||` preserves tags but not payload data; if you need structured payloads, reach for tagged unions instead. - Allocator-backed helpers should surface `std.mem.Allocator.Error` directly—callers expect to `try` allocations just like standard library containers. - The recipes here assume debug or release-safe builds; in release-fast you may want additional logging for branches that would otherwise fire `unreachable`.37 (37__illegal-behavior-and-safety-modes.xml) ## Section: Exercises [section_id: exercises] [section_url: https://zigbook.net/chapters/57__error-handling-patterns-cookbook#exercises] - Extend `loadPort` so it returns a structured configuration object with both host and port, then enumerate the resulting composite error set.4 (04__errors-resource-cleanup.xml) - Add a streaming variant of `joinUpperSnippets` that writes to a user-supplied writer instead of allocating, and compare its ergonomics.Io.zig (https://github.com/ziglang/zig/tree/master/lib/std/Io.zig) - Teach `runAndReport` to redact identifiers before logging by injecting a formatter callback—verify with unit tests that both success and failure paths respect the hook.36 (36__style-and-best-practices.xml) ## Section: Alternatives & Edge Cases [section_id: caveats-alternatives-edge-cases] [section_url: https://zigbook.net/chapters/57__error-handling-patterns-cookbook#caveats-alternatives-edge-cases] - For long-running services, consider wrapping retry loops with exponential backoff and jitter; Chapter 29 revisits the concurrency implications.29 (29__threads-and-atomics.xml) - If your error bridge needs localization, store message IDs alongside the error tags and let higher layers format the final string. - Embedded targets with tiny allocators may prefer stack-based buffers or fixed `std.BoundedArray` instances over heap-backed arrays to avoid `OutOfMemory`.10 (10__allocators-and-memory-management.xml) # Chapter 58 — Appendix D. Mapping C/Rust Idioms → Zig Constructs [chapter_id: 58__mapping-c-rust-idioms] [chapter_slug: mapping-c-rust-idioms] [chapter_number: 58] [chapter_url: https://zigbook.net/chapters/58__mapping-c-rust-idioms] ## Section: Overview [section_id: overview] [section_url: https://zigbook.net/chapters/58__mapping-c-rust-idioms#overview] C and Rust establish the mental models that many Zig developers bring along: manual `malloc`/`free`, RAII destructors, `Option`, `Result`, and trait objects. This appendix translates those habits into idiomatic Zig so you can port real codebases without fighting the language. Zig’s tightened pointer alignment rules (`@alignCast`) and improved allocator diagnostics show up repeatedly when wrapping foreign APIs. v0.15.2 (https://ziglang.org/download/0.15.1/release-notes.html) ## Section: Learning Goals [section_id: learning-goals] [section_url: https://zigbook.net/chapters/58__mapping-c-rust-idioms#learning-goals] - Swap manual resource cleanup for `defer`/`errdefer` while preserving the control you expect from C. - Express Rust-inspired `Option`/`Result` logic with Zig optionals and error unions in a composable way. - Adapt callback- or trait-based polymorphism to Zig’s `comptime` generics and pointer shims. ## Section: Translating C Resource Lifetimes [section_id: c-resource-lifetimes] [section_url: https://zigbook.net/chapters/58__mapping-c-rust-idioms#c-resource-lifetimes] C programmers habitually pair every `malloc` with a matching `free`. Zig lets you encode the same intent with `errdefer` and structured error sets so buffers never leak even when validation fails. 4 (04__errors-resource-cleanup.xml) The following example contrasts a direct translation with a Zig-first helper that frees memory automatically, highlighting how allocator errors compose with domain errors. mem.zig (https://github.com/ziglang/zig/tree/master/lib/std/mem.zig) ```zig //! Reinvents a C-style buffer duplication with Zig's defer-based cleanup. const std = @import("std"); pub const NormalizeError = error{InvalidCharacter} || std.mem.Allocator.Error; pub fn duplicateAlphaUpper(allocator: std.mem.Allocator, input: []const u8) NormalizeError![]u8 { const buffer = try allocator.alloc(u8, input.len); errdefer allocator.free(buffer); for (buffer, input) |*dst, src| switch (src) { 'a'...'z', 'A'...'Z' => dst.* = std.ascii.toUpper(src), else => return NormalizeError.InvalidCharacter, }; return buffer; } pub fn cStyleDuplicateAlphaUpper(allocator: std.mem.Allocator, input: []const u8) NormalizeError![]u8 { const buffer = try allocator.alloc(u8, input.len); var ok = false; defer if (!ok) allocator.free(buffer); for (buffer, input) |*dst, src| switch (src) { 'a'...'z', 'A'...'Z' => dst.* = std.ascii.toUpper(src), else => return NormalizeError.InvalidCharacter, }; ok = true; return buffer; } test "duplicateAlphaUpper releases buffer on failure" { const allocator = std.testing.allocator; try std.testing.expectError(NormalizeError.InvalidCharacter, duplicateAlphaUpper(allocator, "zig-0")); } test "c style duplicate succeeds with valid input" { const allocator = std.testing.allocator; const dup = try cStyleDuplicateAlphaUpper(allocator, "zig"); defer allocator.free(dup); try std.testing.expectEqualStrings("ZIG", dup); } ``` Run: ```shell $ zig test 01_c_style_cleanup.zig ``` Output: ```shell All 2 tests passed. ``` TIP: The explicit `NormalizeError` union tracks both allocator failures and validation failures, a pattern encouraged throughout Chapter 10’s allocator tour (10__allocators-and-memory-management.xml). ## Section: Mirroring Rust’s Option and Result Types [section_id: rust-option-result] [section_url: https://zigbook.net/chapters/58__mapping-c-rust-idioms#rust-option-result] Rust’s `Option` maps cleanly to Zig’s `?T`, while `Result` becomes an error union (`E!T`) with rich tags instead of stringly typed messages. 4 (04__errors-resource-cleanup.xml) This recipe pulls a configuration value from newline-separated text, first with an optional search and then with a domain-specific error union that converts parsing failures into caller-friendly diagnostics. fmt.zig (https://github.com/ziglang/zig/tree/master/lib/std/fmt.zig) ```zig //! Mirrors Rust's Option and Result idioms with Zig optionals and error unions. const std = @import("std"); pub fn findPortLine(env: []const u8) ?[]const u8 { var iter = std.mem.splitScalar(u8, env, '\n'); while (iter.next()) |line| { if (std.mem.startsWith(u8, line, "PORT=")) { return line["PORT=".len..]; } } return null; } pub const ParsePortError = error{ Missing, Invalid, }; pub fn parsePort(env: []const u8) ParsePortError!u16 { const raw = findPortLine(env) orelse return ParsePortError.Missing; return std.fmt.parseInt(u16, raw, 10) catch ParsePortError.Invalid; } test "findPortLine returns optional when key absent" { try std.testing.expectEqual(@as(?[]const u8, null), findPortLine("HOST=zig-lang")); } test "parsePort converts parse errors into domain error set" { try std.testing.expectEqual(@as(u16, 8080), try parsePort("PORT=8080\n")); try std.testing.expectError(ParsePortError.Missing, parsePort("HOST=zig")); try std.testing.expectError(ParsePortError.Invalid, parsePort("PORT=xyz")); } ``` Run: ```shell $ zig test 02_rust_option_result.zig ``` Output: ```shell All 2 tests passed. ``` NOTE: Because Zig separates optional discovery from error propagation, you can reuse `findPortLine` for fast-path checks while `parsePort` handles the slower, fallible work—mirroring the Rust pattern of splitting `Option::map` from `Result::map_err`. 17 (17__generic-apis-and-type-erasure.xml) ## Section: Bridging Traits and Function Pointers [section_id: callback-bridges] [section_url: https://zigbook.net/chapters/58__mapping-c-rust-idioms#callback-bridges] Both C and Rust lean on callbacks—either raw function pointers with context payloads or trait objects with explicit `self` parameters. Zig models the same abstraction with `*anyopaque` shims plus `comptime` adapters, so you keep type safety and zero-cost indirection. 33 (33__c-interop-import-export-abi.xml) The example below shows a C-style callback and a trait-like `handle` method reused via the same legacy bridge, relying on Zig’s pointer casts and alignment assertions. builtin.zig (https://github.com/ziglang/zig/tree/master/lib/std/builtin.zig) ```zig //! Converts a C function-pointer callback pattern into type-safe Zig shims. const std = @import("std"); pub const LegacyCallback = *const fn (ctx: *anyopaque) void; fn callLegacy(callback: LegacyCallback, ctx: *anyopaque) void { callback(ctx); } const Counter = struct { value: u32, }; fn incrementShim(ctx: *anyopaque) void { const counter: *Counter = @ptrCast(@alignCast(ctx)); counter.value += 1; } pub fn incrementViaLegacy(counter: *Counter) void { callLegacy(incrementShim, counter); } pub fn dispatchWithContext(comptime Handler: type, ctx: *Handler) void { const shim = struct { fn invoke(raw: *anyopaque) void { const typed: *Handler = @ptrCast(@alignCast(raw)); Handler.handle(typed); } }; callLegacy(shim.invoke, ctx); } const Stats = struct { total: u32 = 0, fn handle(self: *Stats) void { self.total += 2; } }; test "incrementViaLegacy integrates with C-style callback" { var counter = Counter{ .value = 0 }; incrementViaLegacy(&counter); try std.testing.expectEqual(@as(u32, 1), counter.value); } test "dispatchWithContext adapts trait-like handle method" { var stats = Stats{}; dispatchWithContext(Stats, &stats); try std.testing.expectEqual(@as(u32, 2), stats.total); } ``` Run: ```shell $ zig test 03_callback_bridge.zig ``` Output: ```shell All 2 tests passed. ``` TIP: The additional `@alignCast` calls reflect a 0.15.2 footgun—pointer casts now assert alignment, so leave them in place when wrapping `*anyopaque` handles from C libraries. v0.15.2 (https://ziglang.org/download/0.15.1/release-notes.html) ## Section: Patterns to Keep on Hand [section_id: patterns] [section_url: https://zigbook.net/chapters/58__mapping-c-rust-idioms#patterns] - Keep allocator cleanup localized with `errdefer` while exposing typed results, so C ports stay leak-free without sprawling `goto` blocks. 4 (04__errors-resource-cleanup.xml) - Convert foreign enums into Zig error unions early, then re-export a focused error set at your module boundary. 57 (57__error-handling-patterns-cookbook.xml) - Implement trait-style behavior with `comptime` structs that expose a small interface (`handle`, `format`, etc.), letting the optimizer inline the call sites. 15 (15__comptime-and-reflection.xml) ## Section: Notes & Caveats [section_id: notes-caveats] [section_url: https://zigbook.net/chapters/58__mapping-c-rust-idioms#notes-caveats] - Manual allocation helpers should surface `std.mem.Allocator.Error` explicitly so callers can continue propagating failures transparently. - When porting Rust crates that rely on drop semantics, audit every branch for `return` or `break` expressions—Zig will not automatically invoke destructors. 36 (36__style-and-best-practices.xml) - Function-pointer shims must respect calling conventions; if the C API expects `extern fn`, annotate your shim accordingly before shipping. 33 (33__c-interop-import-export-abi.xml) ## Section: Exercises [section_id: exercises] [section_url: https://zigbook.net/chapters/58__mapping-c-rust-idioms#exercises] - Extend the normalization helper to tolerate underscores by translating them to hyphens, and add tests covering both success and failure cases. 10 (10__allocators-and-memory-management.xml) - Modify `parsePort` to return a struct containing both host and port, then document how the combined error union expands. 57 (57__error-handling-patterns-cookbook.xml) - Generalize `dispatchWithContext` so it accepts a compile-time list of handlers, mirroring Rust’s trait object vtables. 15 (15__comptime-and-reflection.xml) ## Section: Alternatives & Edge Cases [section_id: caveats-alternatives-edge-cases] [section_url: https://zigbook.net/chapters/58__mapping-c-rust-idioms#caveats-alternatives-edge-cases] - Some C libraries expect you to allocate with their custom functions—wrap those allocators in a shim that implements the `std.mem.Allocator` interface, so the rest of your Zig code stays uniform. 10 (10__allocators-and-memory-management.xml) - When porting Rust `Option` that owns heap data, consider returning a slice plus length sentinel instead of duplicating ownership semantics. 3 (03__data-fundamentals.xml) - If your callback bridge crosses threads, add synchronization primitives from Chapter 29 before mutating shared state. 29 (29__threads-and-atomics.xml) # Chapter 59 — Appendix E. Advanced Inline Assembly [chapter_id: 59__advanced-inline-assembly] [chapter_slug: advanced-inline-assembly] [chapter_number: 59] [chapter_url: https://zigbook.net/chapters/59__advanced-inline-assembly] ## Section: Overview [section_id: overview] [section_url: https://zigbook.net/chapters/59__advanced-inline-assembly#overview] Inline assembly grants you the power to reach below Zig’s abstractions when you need one-off instructions, interoperability with legacy ABIs, or access to processor features not yet wrapped by the standard library. 33 (33__c-interop-import-export-abi.xml) Zig 0.15.2 hardened inline assembly by enforcing alignment checks for pointer casts and providing clearer constraint diagnostics, making it both safer and easier to debug than previous releases. v0.15.2 (https://ziglang.org/download/0.15.1/release-notes.html) ## Section: Learning Goals [section_id: learning-goals] [section_url: https://zigbook.net/chapters/59__advanced-inline-assembly#learning-goals] - Recognize the structure of Zig’s GNU-style inline assembly blocks and map operands to registers or memory. - Apply register and clobber constraints to orchestrate data flow between Zig variables and machine instructions. - Guard architecture-specific snippets with compile-time checks so your build fails fast on unsupported targets. ## Section: Shaping Assembly Blocks [section_id: asm-shapes] [section_url: https://zigbook.net/chapters/59__advanced-inline-assembly#asm-shapes] Zig adopts the familiar GCC/Clang inline assembly layout: a template string followed by colon-separated outputs, inputs, and clobbers. Start with simple arithmetic to get comfortable with operand binding before you reach for more exotic instructions. The first example uses `addl` to combine two 32-bit values, binding both operands to registers without touching memory. x86_64.zig (https://github.com/ziglang/zig/tree/master/lib/std/os/plan9/x86_64.zig) ```zig //! Minimal inline assembly example that adds two integers. const std = @import("std"); pub fn addAsm(a: u32, b: u32) u32 { var result: u32 = undefined; asm volatile ("addl %[lhs], %[rhs]\n\t" : [out] "=r" (result), : [lhs] "r" (a), [rhs] "0" (b), ); return result; } test "addAsm produces sum" { try std.testing.expectEqual(@as(u32, 11), addAsm(5, 6)); } ``` Run: ```shell $ zig test chapters-data/code/59__advanced-inline-assembly/01_inline_add.zig ``` Output: ```shell All 1 tests passed. ``` TIP: Operand placeholders such as `%[lhs]` reference the symbolic names you assign in the constraint list; keeping those names mnemonic pays off once your templates grow beyond a single instruction. 58 (58__mapping-c-rust-idioms.xml) ## Section: Register Choreography Without Footguns [section_id: register-choreography] [section_url: https://zigbook.net/chapters/59__advanced-inline-assembly#register-choreography] More complex snippets often need bidirectional operands (read/write) or additional bookkeeping once the instruction finishes. The `xchg` sequence below swaps two integers entirely in registers, then writes the updated values back to Zig-managed memory. 4 (04__errors-resource-cleanup.xml) Guarding the function with `@compileError` prevents accidental use on non-x86 platforms, while the `+r` constraint indicates that each operand is both read and written. pie.zig (https://github.com/ziglang/zig/tree/master/lib/std/pie.zig) ```zig //! Swaps two words using the x86 xchg instruction with memory constraints. const std = @import("std"); const builtin = @import("builtin"); pub fn swapXchg(a: *u32, b: *u32) void { if (builtin.cpu.arch != .x86_64) @compileError("swapXchg requires x86_64"); var lhs = a.*; var rhs = b.*; asm volatile ("xchgl %[left], %[right]" : [left] "+r" (lhs), [right] "+r" (rhs), ); a.* = lhs; b.* = rhs; } test "swapXchg swaps values" { var lhs: u32 = 1; var rhs: u32 = 2; swapXchg(&lhs, &rhs); try std.testing.expectEqual(@as(u32, 2), lhs); try std.testing.expectEqual(@as(u32, 1), rhs); } ``` Run: ```shell $ zig test chapters-data/code/59__advanced-inline-assembly/02_xchg_swap.zig ``` Output: ```shell All 1 tests passed. ``` NOTE: Because the swap operates only on registers, you stay clear of tricky memory constraints; when you do need to touch memory directly, add an explicit `"memory"` clobber so Zig’s optimizer does not reorder surrounding loads or stores. 36 (36__style-and-best-practices.xml) ## Section: Observability and Guard Rails [section_id: observability] [section_url: https://zigbook.net/chapters/59__advanced-inline-assembly#observability] Once you trust the syntax, inline assembly becomes a precision tool for hardware-provided counters or instructions not yet surfaced elsewhere. Reading the x86 time-stamp counter with `rdtsc` gives you cycle-level timing while demonstrating multi-output constraints and the new alignment assertions introduced in 0.15.x. 39 (39__performance-and-inlining.xml) The example bundles the low and high halves of the counter into a `u64` and falls back to a compile error on non-x86_64 targets. ```zig //! Reads the x86 time stamp counter using inline assembly outputs. const std = @import("std"); const builtin = @import("builtin"); pub fn readTimeStampCounter() u64 { if (builtin.cpu.arch != .x86_64) @compileError("rdtsc example requires x86_64"); var lo: u32 = undefined; var hi: u32 = undefined; asm volatile ("rdtsc" : [low] "={eax}" (lo), [high] "={edx}" (hi), ); return (@as(u64, hi) << 32) | @as(u64, lo); } test "readTimeStampCounter returns non-zero" { const a = readTimeStampCounter(); const b = readTimeStampCounter(); // The counter advances monotonically; allow equality in case calls land in the same cycle. try std.testing.expect(b >= a); } ``` Run: ```shell $ zig test chapters-data/code/59__advanced-inline-assembly/03_rdtsc.zig ``` Output: ```shell All 1 tests passed. ``` CAUTION: Instructions like `rdtsc` can reorder around other operations; consider pairing them with serializing instructions (e.g. `lfence`) or explicit memory clobbers when precise measurement matters. 39 (39__performance-and-inlining.xml) ## Section: Patterns to Keep on Hand [section_id: patterns] [section_url: https://zigbook.net/chapters/59__advanced-inline-assembly#patterns] - Wrap architecture-specific blocks in `if (builtin.cpu.arch != …) @compileError` guards so cross-compilation fails early. 41 (41__cross-compilation-and-wasm.xml) - Prefer register-only operands when prototyping—once the logic is correct, introduce memory operands and clobbers deliberately. 33 (33__c-interop-import-export-abi.xml) - Treat inline assembly as an escape hatch; if the standard library (or builtins) exposes the instruction, prefer that higher-level API to stay portable. mem.zig (https://github.com/ziglang/zig/tree/master/lib/std/mem.zig) ## Section: Notes & Caveats [section_id: notes-caveats] [section_url: https://zigbook.net/chapters/59__advanced-inline-assembly#notes-caveats] - Inline assembly is target-specific; always document the minimum CPU features required and consider feature probes before executing the block. 29 (29__threads-and-atomics.xml) - Clobber lists matter—forgetting `"cc"` or `"memory"` may lead to miscompilations that only surface under optimization. 36 (36__style-and-best-practices.xml) - When mixing Zig and foreign ABIs, double-check the calling convention and register preservation rules; the compiler will not save registers for you. builtin.zig (https://github.com/ziglang/zig/tree/master/lib/std/builtin.zig) ## Section: Exercises [section_id: exercises] [section_url: https://zigbook.net/chapters/59__advanced-inline-assembly#exercises] - Add an `lfence` instruction before `rdtsc` and measure the impact on stability; compare results in Debug and ReleaseFast builds. 39 (39__performance-and-inlining.xml) - Extend `swapXchg` with a `"memory"` clobber and benchmark the difference when swapping values in a tight loop. time.zig (https://github.com/ziglang/zig/tree/master/lib/std/time.zig) - Rewrite `addAsm` using a compile-time format string that emits `add` or `sub` based on a boolean parameter. 15 (15__comptime-and-reflection.xml) ## Section: Alternatives & Edge Cases [section_id: caveats-alternatives-edge-cases] [section_url: https://zigbook.net/chapters/59__advanced-inline-assembly#caveats-alternatives-edge-cases] - Some instructions (e.g., privileged system calls) require elevated privileges—wrap them in runtime checks so they never execute inadvertently. 48 (48__process-and-environment.xml) - On microarchitectures with out-of-order execution, pair timing reads with fences to avoid skewed measurements. 39 (39__performance-and-inlining.xml) - For portable timing, prefer `std.time.Timer` or platform APIs and reserve inline assembly for truly architecture-specific hot paths. # Chapter 60 — Appendix F. Advanced Result Location Semantics [chapter_id: 60__advanced-result-location-semantics] [chapter_slug: advanced-result-location-semantics] [chapter_number: 60] [chapter_url: https://zigbook.net/chapters/60__advanced-result-location-semantics] ## Section: Overview [section_id: overview] [section_url: https://zigbook.net/chapters/60__advanced-result-location-semantics#overview] Result Location Semantics (RLS) are the quiet engine that powers Zig’s zero-copy aggregates, type inference, and efficient error propagation. After experimenting with inline assembly in Appendix E, we now dive back into the compiler to see how Zig steers values directly into their final home. It eliminates temporaries whether you build structs, unions, or manually fill caller-provided buffers. 59 (59__advanced-inline-assembly.xml) Zig 0.15.2 clarifies RLS diagnostics around pointer alignment and optional result pointers, making it easier to reason about where your data lives during construction. v0.15.2 (https://ziglang.org/download/0.15.1/release-notes.html) ## Section: Learning Goals [section_id: learning-goals] [section_url: https://zigbook.net/chapters/60__advanced-result-location-semantics#learning-goals] - Trace how struct literals and coercions forward result locations to every field without hidden copies. - Apply explicit result pointers when you want to reuse caller-owned storage while still offering a value-returning API. - Combine unions with RLS so each variant writes directly into its own payload without allocating scratch buffers at runtime. ## Section: Struct Forwarding in Practice [section_id: struct-forwarding] [section_url: https://zigbook.net/chapters/60__advanced-result-location-semantics#struct-forwarding] When you assign a struct literal to a variable, Zig rewrites the operation into a series of field writes, allowing each sub-expression to inherit the final destination. The first recipe summarizes a handful of sensor readings into a `Report`, demonstrating how nested literals (`range` inside `Report`) inherit result locations transitively. math.zig (https://github.com/ziglang/zig/tree/master/lib/std/math.zig) ```zig //! Builds a statistics report using struct literals that forward into the caller's result location. const std = @import("std"); pub const Report = struct { range: struct { min: u8, max: u8, }, buckets: [4]u32, }; pub fn buildReport(values: []const u8) Report { var histogram = [4]u32{ 0, 0, 0, 0 }; if (values.len == 0) { return .{ .range = .{ .min = 0, .max = 0 }, .buckets = histogram, }; } var current_min: u8 = std.math.maxInt(u8); var current_max: u8 = 0; for (values) |value| { current_min = @min(current_min, value); current_max = @max(current_max, value); const bucket_index = value / 64; histogram[bucket_index] += 1; } return .{ .range = .{ .min = current_min, .max = current_max }, .buckets = histogram, }; } test "buildReport summarises range and bucket counts" { const data = [_]u8{ 3, 19, 64, 129, 200 }; const report = buildReport(&data); try std.testing.expectEqual(@as(u8, 3), report.range.min); try std.testing.expectEqual(@as(u8, 200), report.range.max); try std.testing.expectEqualSlices(u32, &[_]u32{ 2, 1, 1, 1 }, &report.buckets); } ``` Run: ```shell $ zig test chapters-data/code/60__advanced-result-location-semantics/01_histogram_report.zig ``` Output: ```shell All 1 tests passed. ``` TIP: Because the literal `.{ .range = …, .buckets = histogram }` writes field-by-field, you can safely seed `histogram` with `var` data—no temporary copy of the 16-byte array is ever produced. 36 (36__style-and-best-practices.xml) ## Section: Manual Result Pointers for Reuse [section_id: manual-result-pointers] [section_url: https://zigbook.net/chapters/60__advanced-result-location-semantics#manual-result-pointers] Sometimes you want both worlds: a value-returning helper for ergonomic callers and an in-place variant for hot loops that reuse storage. By exposing a `parseInto` routine that receives a `*Numbers`, you determine the result location explicitly while still offering `parseNumbers` that benefits from automatic elision. 4 (04__errors-resource-cleanup.xml) Note how the slice method accepts `*const Numbers`; returning a slice from a by-value parameter would point at a temporary and violate safety rules. mem.zig (https://github.com/ziglang/zig/tree/master/lib/std/mem.zig) ```zig //! Demonstrates manual result locations by filling a struct through a pointer parameter. const std = @import("std"); pub const ParseError = error{ TooManyValues, InvalidNumber, }; pub const Numbers = struct { len: usize = 0, data: [16]u32 = undefined, pub fn slice(self: *const Numbers) []const u32 { return self.data[0..self.len]; } }; pub fn parseInto(result: *Numbers, text: []const u8) ParseError!void { result.* = Numbers{}; result.data = std.mem.zeroes([16]u32); var tokenizer = std.mem.tokenizeAny(u8, text, ", "); while (tokenizer.next()) |word| { if (result.len == result.data.len) return ParseError.TooManyValues; const value = std.fmt.parseInt(u32, word, 10) catch return ParseError.InvalidNumber; result.data[result.len] = value; result.len += 1; } } pub fn parseNumbers(text: []const u8) ParseError!Numbers { var scratch: Numbers = undefined; try parseInto(&scratch, text); return scratch; } test "parseInto fills caller-provided storage" { var numbers: Numbers = .{}; try parseInto(&numbers, "7,11,42"); try std.testing.expectEqualSlices(u32, &[_]u32{ 7, 11, 42 }, numbers.slice()); } test "parseNumbers returns the same shape without extra copies" { const owned = try parseNumbers("1 2 3"); try std.testing.expectEqual(@as(usize, 3), owned.len); try std.testing.expectEqualSlices(u32, &[_]u32{ 1, 2, 3 }, owned.data[0..owned.len]); } ``` Run: ```shell $ zig test chapters-data/code/60__advanced-result-location-semantics/02_numbers_parse_into.zig ``` Output: ```shell All 2 tests passed. ``` NOTE: Resetting `Numbers` with a fresh value and zeroing the backing array ensures the result location is ready for reuse even if the previous parse only filled part of the buffer. 57 (57__error-handling-patterns-cookbook.xml) ## Section: Union Variants and Branch-Specific Destinations [section_id: union-forwarding] [section_url: https://zigbook.net/chapters/60__advanced-result-location-semantics#union-forwarding] Unions expose the same mechanics: once the compiler knows which variant you are constructing, it wires the payload’s result location to the appropriate field. The lookup helper below either streams bytes into a `Resource` payload or returns metadata for malformed queries, without allocating interim buffers. The same approach scales to streaming parsers, FFI bridges, or caches that must avoid heap traffic. ```zig //! Demonstrates union construction that forwards nested result locations. const std = @import("std"); pub const Resource = struct { name: []const u8, payload: [32]u8, }; pub const LookupResult = union(enum) { hit: Resource, miss: void, malformed: []const u8, }; const CatalogEntry = struct { name: []const u8, data: []const u8, }; pub fn lookup(name: []const u8, catalog: []const CatalogEntry) LookupResult { for (catalog) |entry| { if (std.mem.eql(u8, entry.name, name)) { var buffer: [32]u8 = undefined; const len = @min(buffer.len, entry.data.len); std.mem.copyForwards(u8, buffer[0..len], entry.data[0..len]); return .{ .hit = .{ .name = entry.name, .payload = buffer } }; } } if (name.len == 0) return .{ .malformed = "empty identifier" }; return .miss; } test "lookup returns hit variant with payload" { const items = [_]CatalogEntry{ .{ .name = "alpha", .data = "hello" }, .{ .name = "beta", .data = "world" }, }; const result = lookup("beta", &items); switch (result) { .hit => |res| { try std.testing.expectEqualStrings("beta", res.name); try std.testing.expectEqualStrings("world", res.payload[0..5]); }, else => try std.testing.expect(false), } } test "lookup surfaces malformed input" { const items = [_]CatalogEntry{.{ .name = "alpha", .data = "hello" }}; const result = lookup("", &items); switch (result) { .malformed => |msg| try std.testing.expectEqualStrings("empty identifier", msg), else => try std.testing.expect(false), } } ``` Run: ```shell $ zig test chapters-data/code/60__advanced-result-location-semantics/03_union_forwarding.zig ``` Output: ```shell All 2 tests passed. ``` CAUTION: When copying into fixed-size buffers, clamp the length as shown, so you do not accidentally write past the payload. If you require full-length retention, switch to a slice field and pair it with lifetimes that outlive the union value. 10 (10__allocators-and-memory-management.xml) ## Section: Patterns to Keep on Hand [section_id: patterns] [section_url: https://zigbook.net/chapters/60__advanced-result-location-semantics#patterns] - Treat `return .{ … };` as sugar for field-wise writes—the compiler already knows the destination, so lean on literals for clarity. 36 (36__style-and-best-practices.xml) - Offer pointer-based `*_into` variants when parsing or formatting—they turn RLS into a conscious API lever instead of an implicit optimization. 4 (04__errors-resource-cleanup.xml) - When unions carry large payloads, construct them inline so variants do not require heap allocations or temporary buffers. 8 (08__user-types-structs-enums-unions.xml) ## Section: Notes & Caveats [section_id: notes-caveats] [section_url: https://zigbook.net/chapters/60__advanced-result-location-semantics#notes-caveats] - Return slices from by-value methods (like `fn slice(self: Numbers)`) capture a temporary copy; prefer pointer receivers to keep the result location stable. - Many standard-library builders accept result pointers—read their signatures before re-implementing similar plumbing yourself. fmt.zig (https://github.com/ziglang/zig/tree/master/lib/std/fmt.zig) - RLS bypasses no validation: if a sub-expression fails (for example, parsing errors), the partially written destination remains in your control, so remember to reset or discard it before reuse. 57 (57__error-handling-patterns-cookbook.xml) ## Section: Exercises [section_id: exercises] [section_url: https://zigbook.net/chapters/60__advanced-result-location-semantics#exercises] - Extend `buildReport` to parameterize the bucket size, then inspect how nested loops still forward their destinations without copies. 36 (36__style-and-best-practices.xml) - Add overflow detection to `parseInto`, so it rejects integers above a configurable limit, resetting the result buffer when the error fires. 57 (57__error-handling-patterns-cookbook.xml) - Teach `lookup` to stream into a caller-provided scratch buffer when the payload exceeds 32 bytes, mirroring the pointer-based pattern from the previous section. 4 (04__errors-resource-cleanup.xml) ## Section: Alternatives & Edge Cases [section_id: caveats-alternatives-edge-cases] [section_url: https://zigbook.net/chapters/60__advanced-result-location-semantics#caveats-alternatives-edge-cases] - For `comptime` constructs, result locations may exist entirely in compile-time memory; use `@TypeOf` to confirm whether your data ever escapes to runtime. 15 (15__comptime-and-reflection.xml) - When interfacing with C APIs that expect you to manage buffers, combine RLS with `extern` structs, so you match their layout while still avoiding intermediate copies. 33 (33__c-interop-import-export-abi.xml) - Profile hot paths before micro-optimizing: sometimes using `std.ArrayList` or a streaming writer is clearer, and RLS will still erase intermediate temporaries for you. array_list.zig (https://github.com/ziglang/zig/tree/master/lib/std/array_list.zig) # Chapter 61 — The Simplicity You’ve Earned [chapter_id: 61__the-simplicity-you-earned] [chapter_slug: the-simplicity-you-earned] [chapter_number: 61] [chapter_url: https://zigbook.net/chapters/61__the-simplicity-you-earned] ## Section: Overview: [section_id: overview] [section_url: https://zigbook.net/chapters/61__the-simplicity-you-earned#overview] Sixty chapters ago, you wrote `Hello, world!` and wondered what `std.debug.print` actually did. Now you understand stdout buffering, result location semantics, cross-compilation targets, and the difference between Debug and ReleaseFast builds. You have journeyed through complexity and emerged with something precious: the simplicity on the other side. 0 (00__zigbook_introduction.xml) This final chapter isn’t about teaching new concepts—it’s about recognizing what you’ve become. You started as a student of Zig. You end as its practitioner, armed with the understanding to build systems that are transparent, efficient, and entirely your own. ## Section: What You’ve Mastered: [section_id: learning-goals] [section_url: https://zigbook.net/chapters/61__the-simplicity-you-earned#learning-goals] - By completing this book, you have: - Understood how files become modules and modules form programs through explicit imports and discovery rules. - Mastered manual memory management with allocators as first-class parameters, not hidden runtime machinery. - Wielded compile-time execution to generate code, validate invariants, and build zero-cost abstractions. - Navigated error propagation, resource cleanup, and safety modes without a garbage collector or exceptions. - Built real projects: from CLI tools to parallel algorithms, from GPU compute to self-hosting build systems. - Cross-compiled to WASM, interfaced with C, and profiled hot paths without leaving Zig’s toolchain. You didn’t just learn Zig—you learned to think in systems. ## Section: Looking Back Through New Eyes [section_id: looking-back] [section_url: https://zigbook.net/chapters/61__the-simplicity-you-earned#looking-back] Return for a moment to the program that started it all: ```zig const std = @import("std"); pub fn main() void { std.debug.print("Hello, world!\n", .{}); } ``` When you first ran this, it was magic. Five lines, one command, text on the screen. Simple. But was it simple? Or was it hiding complexity? - Now you know: - `const std = @import("std")` triggers module resolution—the compiler searches its bundled library, resolves the import graph, and binds `std` as a namespace at compile time. #Import (https://ziglang.org/documentation/master/#Import) - `pub fn main()` is discovered by `std.start`, which generates the actual entry point and error-handling wrapper your OS calls. 1 (01__boot-basics.xml) - `std.debug.print` writes to stderr, unbuffered, using platform-specific syscalls abstracted by Zig’s standard library. 1 (01__boot-basics.xml) - The newline `\n` is a single byte—no hidden encoding magic, no locale lookups, just `0x0A` in the output stream. What seemed simple was actually standing on sixty chapters of depth. But here’s the revelation: now that you understand the depth, it becomes simple again. This is not the simplicity of ignorance. This is the simplicity you’ve earned. ## Section: The Simplicity on the Other Side of Complexity: [section_id: the-other-side-of-complexity] [section_url: https://zigbook.net/chapters/61__the-simplicity-you-earned#the-other-side-of-complexity] QUOTE ( Oliver Wendell Holmes Sr. ): I would not give a fig for the simplicity this side of complexity, but I would give my life for the simplicity on the other side of complexity. Zig embodies this philosophy at every level. Manual memory management is complex—until you understand allocators as composable interfaces, then it becomes simple and powerful. You decide when to allocate, which strategy fits your constraints, and how to verify correctness through testing allocators and leak detection. 10 (10__allocators-and-memory-management.xml) Compile-time execution seems like magic—until you understand that `comptime` is just normal Zig code running in the compiler’s interpreter, then it becomes a transparent metaprogramming tool. You see exactly when code runs, what data persists to the binary, and how to balance compile-time cost against runtime performance. 15 (15__comptime-and-reflection.xml) Error handling feels tedious—until you internalize that `try` is explicit control flow and `errdefer` guarantees cleanup, then it becomes reliable resource management. No hidden exceptions unwinding the stack, no runtime overhead in ReleaseFast, just values that document failure paths in their types. 4 (04__errors-resource-cleanup.xml) At every turn, Zig refuses to hide complexity behind abstraction. Instead, it gives you the tools to understand complexity so deeply that it dissolves into simplicity. This is the language’s gift: not hiding complexity, but taming it through transparency. ## Section: The Program That Knows Itself [section_id: the-program-that-knows-itself] [section_url: https://zigbook.net/chapters/61__the-simplicity-you-earned#the-program-that-knows-itself] To demonstrate the simplicity you’ve earned, consider one final program…​ A quine. Here is a complete, working quine in Zig: ```zig const std = @import("std"); pub fn main() !void { const data = "const std = @import(\"std\");\n\npub fn main() !void {{\n const data = \"{f}\";\n var buf: [1024]u8 = undefined;\n var w = std.fs.File.stdout().writer(&buf);\n try w.interface.print(data, .{{std.zig.fmtString(data)}});\n try w.interface.flush();\n}}\n"; var buf: [1024]u8 = undefined; var w = std.fs.File.stdout().writer(&buf); try w.interface.print(data, .{std.zig.fmtString(data)}); try w.interface.flush(); } ``` Run: ```shell $ zig run quine.zig > output.zig $ diff quine.zig output.zig (no output - they are identical) ``` Output: ```shell const std = @import("std"); pub fn main() !void { const data = "const std = @import(\"std\");\n\npub fn main() !void {{\n const data = \"{f}\";\n var buf: [1024]u8 = undefined;\n var w = std.fs.File.stdout().writer(&buf);\n try w.interface.print(data, .{{std.zig.fmtString(data)}});\n try w.interface.flush();\n}}\n"; var buf: [1024]u8 = undefined; var w = std.fs.File.stdout().writer(&buf); try w.interface.print(data, .{std.zig.fmtString(data)}); try w.interface.flush(); } ``` Look at what this program does: it contains its own structure as data, then uses that data to reconstruct itself through formatting. The string `data` holds the template. The formatter `std.zig.fmtString` escapes special characters so they print literally. The buffered writer `w` accumulates output and flushes it to stdout. 46 (46__io-and-stream-adapters.xml) - Every piece is something you understand: - `var buf: [1024]u8` allocates stack storage—no hidden heap, no allocator needed. 3 (03__data-fundamentals.xml) - `std.fs.File.stdout().writer(&buf)` creates a buffered writer following Zig 0.15.2’s explicit buffer management. 1 (01__boot-basics.xml) - `std.zig.fmtString(data)` returns a formatter that escapes quotes, newlines, and backslashes so they survive the print-and-scan cycle. zig.zig (https://github.com/ziglang/zig/blob/master/lib/std/zig.zig) - The double-brace `{{` escapes literal braces in the format string, just like you learned in Chapter 45. 45 (45__text-formatting-and-unicode.xml) - `try w.interface.flush()` is explicit—you control when buffered bytes reach the OS. 4 (04__errors-resource-cleanup.xml) This program knows itself completely. It understands its own structure well enough to reproduce it without external help. And you? You now know Zig completely enough to do the same—to build programs that understand themselves, that control their own resources, that compile to any target with full transparency. The quine is not just a clever trick. It’s a metaphor: mastery is the ability to create things that recreate themselves. ## Section: The Cycle Continues: [section_id: the-cycle-continues] [section_url: https://zigbook.net/chapters/61__the-simplicity-you-earned#the-cycle-continues] Zig bootstraps itself. The compiler is written in Zig, compiled by an earlier version of itself, continuously evolving through self-hosting. github.com/ziglang/zig (https://github.com/ziglang/zig) The standard library tests itself. Every function, every data structure, every algorithm includes `test` blocks that verify correctness during `zig build test`. The build system builds itself. `build.zig` is Zig code that describes how to compile Zig projects, including the compiler’s own build graph. This isn’t recursion for its own sake—it’s confidence. Zig trusts itself because it has earned that trust through transparency and verification at every layer. And now, you’ve earned that same confidence. You started not knowing what a slice was. You end understanding result location semantics. You started printing to stderr with `std.debug.print`. You end streaming through buffered writers, adapters, and compression pipelines. You started running `zig run hello.zig`. You end orchestrating multi-package workspaces with vendored dependencies and cross-compilation targets. Zig trusts you because you’ve earned that trust. You know where every byte lives. You know when the compiler runs your code. You know the cost of every abstraction. The simplicity you see in this final line: ```zig return 0; ``` That simplicity is not accidental. It’s the result of sixty chapters of deliberate design, careful learning, and earned understanding. ## Section: Where to Go From Here: [section_id: where-to-go-from-here] [section_url: https://zigbook.net/chapters/61__the-simplicity-you-earned#where-to-go-from-here] ### Subsection: Contribute to the Ecosystem [section_id: _contribute_to_the_ecosystem] [section_url: https://zigbook.net/chapters/61__the-simplicity-you-earned#_contribute_to_the_ecosystem] Zig is young, evolving, and hungry for contributions. The community values clarity, correctness, and practical solutions over complexity. CONTRIBUTING.md (https://github.com/ziglang/zig/blob/master/CONTRIBUTING.md) - Found a bug? Report it with a minimal reproduction—your debugging skills are sharp now. 13 (13__testing-and-leak-detection.xml) - Missing a feature in the standard library? Propose it, prototype it, test it. 36 (36__style-and-best-practices.xml) - See unclear documentation? You understand the concepts deeply—help others learn. 0 (00__zigbook_introduction.xml) Every open-source contribution, no matter how small, moves the ecosystem forward. ### Subsection: Deepen Your Understanding [section_id: _deepen_your_understanding] [section_url: https://zigbook.net/chapters/61__the-simplicity-you-earned#_deepen_your_understanding] Zig is pre-1.0—stability is coming, but features still shift. Stay current: - Follow the release notes (https://ziglang.org/download/) for each version. Breaking changes are documented with migration paths. - Read the compiler source (https://github.com/ziglang/zig) when you want to understand how something works, not just what it does. 38 (38__zig-cli-deep-dive.xml) - Join the community: GitHub discussions (https://github.com/ziglang/zig/issues), Ziggit forums (https://ziggit.dev/). Ask questions, answer questions, learn from others' code. Mastery is not a destination—it’s a continuous practice. ### Subsection: Teach Others [section_id: _teach_others] [section_url: https://zigbook.net/chapters/61__the-simplicity-you-earned#_teach_others] You’ve walked the path from beginner to practitioner. That perspective is invaluable for those just starting. - Write tutorials, blog posts, or example code repositories that explain what confused you when you were learning. - Mentor newcomers in forums and chat rooms—your recent journey makes you an excellent guide. ziggit.dev (https://ziggit.dev/) - Contribute to this book: open issues, propose improvements, add examples that clarified concepts for you. github.com/zigbook/zigbook (https://github.com/zigbook/zigbook) Teaching is how you solidify your own understanding and give back to the community that helped you. ## Section: Farewell and Forward [section_id: farewell] [section_url: https://zigbook.net/chapters/61__the-simplicity-you-earned#farewell] The Zigbook ends. Your Zig journey does not. You have the tools. You have the knowledge. You have the simplicity on the other side of complexity. Thank you for reading the Zigbook. Thank you for caring about understanding, not just using. Thank you for choosing a language that respects your intelligence and rewards your curiosity. You came for syntax. You leave with a philosophy. Build well. Build clearly. Build your own path. Your turn. `return 0;` Written with care by . Contributions welcome at .