# Chapter 01 — Zig Foundations > **Background assumed:** You write embedded Rust. You know what a stack frame is. You've fought the borrow checker. You're not afraid of unsafe. > **Goal:** Get Zig installed, understand the project structure, read and write basic Zig, and map it to what you already know from Rust. --- ## 1.1 What Is Zig and Why Does It Matter to You Zig is a systems language with three core design goals: - **No hidden control flow.** No operator overloading, no destructors, no implicit anything. - **Comptime instead of macros/generics.** One powerful mechanism replaces Rust's trait system, macros, and const generics. - **First-class C interop.** You can `@cImport` a C header and call it directly, no binding generator needed. For embedded developers specifically: Zig produces freestanding binaries, has zero runtime, compiles to any LLVM target (including bare-metal ARM, RISC-V, AVR), and its `build.zig` can replace your entire Makefile + linker script toolchain. ### vs Rust — the key philosophical difference | Topic | Rust | Zig | |---|---|---| | Safety model | Compiler enforces memory safety | You are responsible; tools help | | Generics | Traits + monomorphization | `comptime` — run code at compile time | | Error handling | `Result`, `?` operator | Error unions `!T`, `try` | | Allocations | Often implicit via `Box`, `Vec` | Always explicit, always passed in | | Build system | Cargo | `build.zig` — it's just Zig code | | C interop | `bindgen` + `unsafe` block | `@cImport` — done | | Hidden control flow | Destructors run on drop | Nothing runs unless you write it | --- ## 1.2 Installing Zig Zig releases frequently. Always use the **latest stable** or **nightly (master)** depending on your needs. For learning, use stable. ```bash # Option 1: Official binary (recommended) # Download from https://ziglang.org/download/ # Extract and add to PATH # Option 2: zigup (version manager, like rustup) # https://github.com/marler8997/zigup zigup 0.14.0 # Verify zig version # zig 0.14.0 (or similar) ``` ### Editor support ```bash # ZLS — Zig Language Server (equivalent to rust-analyzer) # https://github.com/zigtools/zls # VS Code: install the "Zig Language" extension, it auto-downloads ZLS # Neovim: use mason.nvim or build ZLS manually ``` --- ## 1.3 Project Structure ### Hello World from scratch ```bash mkdir zig-learn && cd zig-learn zig init ``` This generates: ``` zig-learn/ ├── build.zig ← Build script (written in Zig) ├── build.zig.zon ← Package manifest (like Cargo.toml) └── src/ ├── main.zig ← Executable entry point └── root.zig ← Library entry point ``` ### `build.zig.zon` — the manifest ```zig .{ .name = "zig-learn", .version = "0.0.1", .dependencies = .{}, .paths = .{""}, } ``` This is a Zig data structure, not TOML/JSON. The `.field = value` syntax is called a **struct literal** — you'll see it everywhere. ### `build.zig` — the build script ```zig const std = @import("std"); pub fn build(b: *std.Build) void { const target = b.standardTargetOptions(.{}); const optimize = b.standardOptimizeOption(.{}); const exe = b.addExecutable(.{ .name = "zig-learn", .root_source_file = b.path("src/main.zig"), .target = target, .optimize = optimize, }); b.installArtifact(exe); const run_cmd = b.addRunArtifact(exe); const run_step = b.step("run", "Run the app"); run_step.dependOn(&run_cmd.step); } ``` Think of `build.zig` as a Rust `build.rs` that also replaces `Cargo.toml`'s `[profile]` and `[[bin]]` sections — but it's a full programming language, not a config file. ### Common build commands ```bash zig build # Build (output in zig-out/) zig build run # Build and run zig build test # Run tests zig build -Doptimize=ReleaseSafe # Release with safety checks zig build -Doptimize=ReleaseFast # Release, max speed zig build -Doptimize=ReleaseSmall # Release, min size (embedded!) zig build -Dtarget=thumbv7m-freestanding-none # Cross-compile ``` `ReleaseSafe` is Zig's equivalent of Rust's release mode — runtime panics still fire on overflow and out-of-bounds. `ReleaseFast` turns them off (like `unsafe` everywhere). `ReleaseSmall` is your friend for microcontrollers. --- ## 1.4 Your First Zig File ```zig // src/main.zig const std = @import("std"); // Import standard library pub fn main() void { std.debug.print("Hello, Zig!\n", .{}); } ``` ### Breaking this down | Piece | What it is | |---|---| | `const std = @import("std")` | Import a module. `@import` is a builtin function (all builtins start with `@`) | | `pub fn main() void` | Public function, no return value. `pub` = visible to other files | | `std.debug.print(...)` | Print to stderr. Takes a format string and a tuple of args | | `.{}` | Anonymous struct literal (empty tuple here) | ### Format strings work like this: ```zig const x: u32 = 42; const name = "world"; std.debug.print("x={d}, name={s}\n", .{ x, name }); // {d} = decimal integer // {s} = string // {any} = print anything (debug format) // {x} = hex // {b} = binary ``` This is similar to `println!("{x}={}", name, x)` in Rust, but the args are a **tuple**, not variadic macros. --- ## 1.5 Variables and Constants ```zig const x = 42; // Immutable, type inferred as comptime_int var y: u32 = 10; // Mutable, explicit type y += 1; // OK const z: f32 = 3.14; // Immutable float ``` ### Key rules 1. `const` = immutable. This is the default — prefer it. 2. `var` = mutable. Must be explicitly typed OR assigned from a typed value. 3. **Unused variables are a compile error.** Use `_ = x;` to suppress. 4. **Undefined behavior on use of undefined values** — Zig has `undefined` as a value but using it is UB. ```zig var buf: [64]u8 = undefined; // Declared but not initialized // In debug builds, Zig fills this with 0xAA to catch bugs early ``` This is like Rust's `MaybeUninit<[u8; 64]>`, but less ceremonial. ### Types at a glance ```zig // Integers u8, u16, u32, u64, u128 // unsigned i8, i16, i32, i64, i128 // signed usize, isize // pointer-sized (like Rust's usize/isize) u1, u7, u21 // arbitrary bit widths! great for embedded // Floats f16, f32, f64, f80, f128 // Boolean bool // true / false // Special comptime types (only exist at compile time) comptime_int // untyped integer literal comptime_float // untyped float literal type // a type IS a value at comptime // No-value void // like Rust's () noreturn // function never returns (like Rust's !) ``` --- ## 1.6 Functions ```zig fn add(a: u32, b: u32) u32 { return a + b; } pub fn main() void { const result = add(3, 4); std.debug.print("result={d}\n", .{result}); } ``` ### Differences from Rust ```rust // Rust: last expression is the return value fn add(a: u32, b: u32) -> u32 { a + b // implicit return } ``` ```zig // Zig: explicit return always fn add(a: u32, b: u32) u32 { return a + b; // required } ``` Zig does allow block expressions that evaluate to a value, but functions always need `return`. ### Function parameters are immutable ```zig fn increment(x: u32) u32 { x += 1; // COMPILE ERROR: cannot assign to constant return x + 1; // do this instead } ``` If you need to mutate, copy into a local `var`: ```zig fn process(data: u32) u32 { var local = data; local += 1; return local; } ``` --- ## 1.7 Control Flow Overview Full detail in Chapter 03, but here's enough to write exercises: ```zig // if / else const x: i32 = 10; if (x > 5) { std.debug.print("big\n", .{}); } else { std.debug.print("small\n", .{}); } // if as expression const label = if (x > 5) "big" else "small"; // while var i: u32 = 0; while (i < 5) : (i += 1) { std.debug.print("{d}\n", .{i}); } // The `: (i += 1)` is the "continue expression" — runs after each iteration // for over a range for (0..5) |i| { std.debug.print("{d}\n", .{i}); } // for over a slice const arr = [_]u32{ 10, 20, 30 }; for (arr) |val| { std.debug.print("{d}\n", .{val}); } ``` --- ## 1.8 The `@import` System and Multiple Files ```zig // src/math.zig pub fn square(x: u32) u32 { return x * x; } // src/main.zig const std = @import("std"); const math = @import("math.zig"); // relative path pub fn main() void { std.debug.print("{d}\n", .{math.square(5)}); } ``` `@import` returns a **struct type** containing all `pub` declarations of the file. This is how Zig does modules — no `mod` keyword, no `use` paths, just files and imports. --- ## 1.9 Panic and Safety Zig has runtime safety checks in `Debug` and `ReleaseSafe` modes: - Integer overflow → **panic** - Out-of-bounds slice access → **panic** - Null pointer dereference → **panic** - Unreachable code reached → **panic** ```zig var x: u8 = 255; x += 1; // panic: integer overflow (in Debug/ReleaseSafe) // wraps silently in ReleaseFast/ReleaseSmall ``` For embedded, you can define a custom panic handler: ```zig pub fn panic(msg: []const u8, ...) noreturn { // blink LED, log to UART, halt while (true) {} } ``` This is the equivalent of Rust's `#[panic_handler]` — and it works the same way. --- ## 1.10 Zig vs Rust — Quick Syntax Cheatsheet | Concept | Rust | Zig | |---|---|---| | Immutable binding | `let x = 5;` | `const x = 5;` | | Mutable binding | `let mut x = 5;` | `var x: u32 = 5;` | | Function | `fn foo(a: u32) -> u32 {}` | `fn foo(a: u32) u32 {}` | | Import | `use std::fmt;` | `const fmt = @import("std").fmt;` | | Struct | `struct Foo { x: u32 }` | `const Foo = struct { x: u32 };` | | Enum | `enum Color { Red, Green }` | `const Color = enum { red, green };` | | Print debug | `println!("{:?}", x)` | `std.debug.print("{any}\n", .{x})` | | Assert | `assert!(x == 1)` | `std.debug.assert(x == 1)` | | Unreachable | `unreachable!()` | `unreachable` | | Cast | `x as u32` | `@intCast(x)` or `@as(u32, x)` | | Block return | Implicit last expr | `break :blk value` | | Undefined | `MaybeUninit` | `undefined` | | Panic | `panic!("msg")` | `@panic("msg")` | --- ## Exercises These exercises are meant to be written from scratch in a fresh `zig init` project. Resist looking up anything beyond what's in this chapter. --- ### Exercise 1 — Hello, Details Modify the generated `main.zig` to print the following exactly: ``` Name: HAL-9000 Version: 0.1.0 Debug: true ``` **Requirements:** - Store each value in a `const` with an appropriate type. - Use a **single** `std.debug.print` call with one format string and a tuple of all three values. - Use `{s}` for strings, `{d}` for the version numbers (print major and minor separately as integers), `{any}` for the boolean. --- ### Exercise 2 — Basic Arithmetic Functions Create a file `src/calc.zig` with the following four functions, all `pub`: - `add(a: i32, b: i32) i32` - `sub(a: i32, b: i32) i32` - `mul(a: i32, b: i32) i32` - `divFloor(a: i32, b: i32) i32` — integer division, truncating toward zero In `main.zig`, import `calc.zig` and print the results of: - `add(100, -42)` - `sub(0, 99)` - `mul(-3, 7)` - `divFloor(17, 5)` Expected output: ``` add: 58 sub: -99 mul: -21 div: 3 ``` --- ### Exercise 3 — Loops and Accumulation Write a function `sumUpTo(n: u32) u64` that returns the sum of all integers from 0 to `n` inclusive, using a `while` loop with a continue expression. Then write a second version `sumUpToFor(n: u32) u64` using a `for` loop over a range. Call both with `n = 100` and print the results. They should both print `5050`. **Bonus:** Add a `comptime` assert inside `main` that the value for `n = 10` equals `55`. (Hint: call the function with a `comptime` argument — if the function can be evaluated at comptime, Zig will do it.) --- ### Exercise 4 — FizzBuzz, Zig Style Print FizzBuzz from 1 to 30: - Divisible by 15 → print `FizzBuzz` - Divisible by 3 → print `Fizz` - Divisible by 5 → print `Buzz` - Otherwise → print the number **Requirements:** - Use a `for (1..31)` range loop. - Use `if` / `else if` / `else` — no `switch` yet (that's Chapter 03). - Use `@mod(i, 3)` for modulo (not `%` — in Zig, `%` is the remainder operator and `@mod` is true modulo for signed integers). --- ### Exercise 5 — Separate Build Target In your `build.zig`, add a **second executable** called `"calc-runner"` that compiles `src/calc.zig` as a standalone binary with its own `main` function (you'll need to add a `pub fn main()` to `calc.zig` temporarily, or create `src/calc_main.zig`). Add a build step called `"calc"` that runs it: ```bash zig build calc # should run your second binary ``` This exercise is about getting comfortable with `build.zig` as code, not config. --- ## Summary By the end of this chapter you should be able to: - [ ] Install Zig and set up ZLS in your editor - [ ] Use `zig init`, `zig build`, and `zig build run` - [ ] Read and write basic Zig syntax — variables, functions, loops - [ ] Import across multiple files with `@import` - [ ] Understand the difference between `Debug`, `ReleaseSafe`, `ReleaseFast`, `ReleaseSmall` - [ ] Map core Rust concepts to their Zig equivalents - [ ] Add a second build target to `build.zig` --- **Next:** Chapter 02 — Types & Variables — where we go deep on Zig's type system, arbitrary-width integers, optionals, type coercion rules, and `comptime_int` vs concrete types.