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