Zig Architect
Use this when designing Zig modules at ChainSafe — lodestar-z-shaped work (consensus libraries consumed by Lodestar across a C ABI) or any Zig library. Full reference: languages/zig/architect.md.
When Zig (and when not)
Zig earns its place where the realistic alternative is C: small, performance-critical, memory-controlled libraries consumed across an FFI boundary — hashing, SSZ encode/decode, Merkleization. It is not the default for services or tooling (those are TypeScript/Go/Rust/Solidity/Python). Introducing Zig outside that niche is an org-level decision — raise it with the curator and capture the rationale in an ADR first.
Key Zig-specific decisions
Project layout
build.zig+build.zig.zonat the root;build.zig.zonpins dependencies andminimum_zig_version.- Public modules declared in
build.zigviab.addModule— the module list is the public surface (lodestar-z exposesssz,hashing,persistent_merkle_tree,consensus_types). src/(root.zig / main.zig),test/,bench/,bindings/. Files are structs; one primary type per file.
Allocator strategy (the central decision)
- Inject, never capture. Every allocating function/type takes an
std.mem.Allocator; none reaches for a global. This is what makes code testable, leak-checkable, and FFI-usable. - Match the allocator to the lifetime — arena for operation-scoped bulk frees, fixed-buffer for bounded hot paths, leak-detecting GPA in tests/debug.
- Ownership is explicit across returns and the C ABI. Pair every acquire with
defer/errdefer.
Error model
- Explicit error sets at public boundaries so callers
switchexhaustively; inference (!T) for internal helpers. tryto propagate,errdeferto unwind partial construction.catch unreachableonly where success is a proven invariant — never on fallible I/O (UB in unsafe builds).
Build mode and safety posture
- ReleaseSafe is the default for anything where a wrong answer matters (all of lodestar-z). Runtime safety stays on.
- ReleaseFast only on measured hot paths, with
@setRuntimeSafety(false)scoped tightly and justified. Overflow and bounds checks become UB there — never assume them for correctness.
Comptime over macros
Generics are functions returning type (fn UintType(comptime bits: u16) type). Keep comptime surfaces small and documented; comptime that could be runtime costs compile time and readability.
The C ABI is a contract
Bindings cross a C ABI (export fn, callconv(.C), extern layout). Treat the exported surface as a versioned public API with explicit cross-boundary memory ownership — breaking it breaks a downstream client in another language.
Pre-1.0 reality
Zig is pre-1.0; pin minimum_zig_version and the CI compiler, wrap heavily-used std APIs, and budget for migration on upgrades.
ADR shape for Zig modules
Allocator ownership · error set · build-mode/safety posture · comptime surface · exported C ABI + memory ownership · Zig version floor · invariants impacted (.invariants).
Anti-patterns at design time
- A hidden/global allocator.
- ReleaseFast for correctness-critical code without a measured, scoped justification.
- Comptime where runtime would do.
- Leaking allocator ownership across the C ABI without a documented contract.
catch unreachableon fallible operations.
Related
- Full reference:
languages/zig/architect.md - Sister roles:
chainsafe-zig-developer,chainsafe-zig-reviewer - Framework:
invariants/invariants-framework.md - Workflow:
chainsafe-research-plan-implement - Upstream: Zig Language Reference, Learn Zig