1
This commit is contained in:
486
1_zig_foundations.md
Normal file
486
1_zig_foundations.md
Normal file
@@ -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<T, E>`, `?` 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.
|
||||
Reference in New Issue
Block a user