From 34082dd908dad5b74285f2b64e8571358c506227 Mon Sep 17 00:00:00 2001 From: Priec Date: Fri, 6 Mar 2026 22:56:59 +0100 Subject: [PATCH] 1 --- 1_zig_foundations.md | 486 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 486 insertions(+) create mode 100644 1_zig_foundations.md diff --git a/1_zig_foundations.md b/1_zig_foundations.md new file mode 100644 index 0000000..aac8871 --- /dev/null +++ b/1_zig_foundations.md @@ -0,0 +1,486 @@ +# 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.