Zig: язык системного программирования
Концепции
Zig — компилируемый язык без GC, скрытых аллокаций и скрытого control flow.
Цель: замена C/C++ с лучшей безопасностью, читаемостью и tooling.
Компиляция:
.zig → IR → (LLVM backend | x86 self-hosted backend) → native binary
Ключевые принципы:
- Явное лучше неявного (нет скрытых аллокаций, скрытых control flow)
- Comptime вместо макросов и шаблонов
- Аллокатор — параметр, а не глобальное состояние
- Ошибки — значения (error unions), не исключения
- Нет оператора ||, нет скрытых приведений типов
- Один и тот же синтаксис для comptime и runtime кодаТекущая стабильная версия: 0.15.2 (октябрь 2025) В разработке: 0.16.0-dev (новый async I/O, fibers) Цель 1.0: 2026, осталось 4 приоритета: производительность, полировка языка, качество std, формальная спецификация.
Версии:
0.13.0 (июнь 2024) — LLVM 18, rework std.Progress
0.14.0 (март 2025) — labeled switch, --watch, incremental compilation
0.15.1 (август 2025) — x86 backend по умолчанию, "Writergate" I/O overhaul
0.15.2 (октябрь 2025) — багфикс-патч
0.16.0-dev — std.Io, fibers, async/await возвращаетсяУстановка
# Arch
sudo pacman -S zig
# Ubuntu / Debian (через snap или вручную)
snap install zig --classic --beta
# или скачать бинарник
curl -LO https://ziglang.org/download/0.15.2/zig-linux-x86_64-0.15.2.tar.xz
tar xf zig-linux-x86_64-0.15.2.tar.xz
sudo mv zig-linux-x86_64-0.15.2 /opt/zig
sudo ln -s /opt/zig/zig /usr/local/bin/zig
# macOS
brew install zig
# Проверка
zig version # 0.15.2
zig env # полная информация о средеСоздание проекта
mkdir myproject && cd myproject
zig init
# Создаёт:
# build.zig — конфигурация сборки (на Zig)
# build.zig.zon — манифест зависимостей
# src/main.zig — точка входа
# src/root.zig — библиотечный модуль
# Сборка и запуск
zig build # собрать
zig build run # собрать и запустить
zig build test # запустить тесты
# Быстрый запуск файла без build.zig
zig run src/main.zig
# Кросс-компиляция
zig build -Dtarget=aarch64-linux-gnu
zig build -Dtarget=x86_64-windows-gnu
# Режимы оптимизации
zig build -Doptimize=Debug # по умолчанию, с проверками
zig build -Doptimize=ReleaseSafe # оптимизирован, с проверками
zig build -Doptimize=ReleaseFast # максимум скорости
zig build -Doptimize=ReleaseSmall # минимум размераСистема типов
Примитивные типы
// Целые числа — точная ширина
const a: u8 = 255;
const b: i32 = -42;
const c: u64 = 0;
const d: usize = @sizeOf(u64); // платформо-зависимый размер (как size_t)
const e: isize = -1; // знаковый usize
// Числа с плавающей точкой
const f: f32 = 3.14;
const g: f64 = 2.718281828;
// Bool
const h: bool = true;
// Comptime integer — без фиксированной ширины, только в comptime
const big: comptime_int = 1 << 128;
// Void — тип без значения (как unit)
const v: void = {};Массивы и слайсы
// Массив — фиксированный размер, на стеке
const arr = [_]u32{ 1, 2, 3, 4 }; // тип: [4]u32
const zeros: [64]u8 = @splat(0); // заполнить нулями (с 0.14)
const matrix = [3][3]f32{ // многомерный
.{ 1, 0, 0 },
.{ 0, 1, 0 },
.{ 0, 0, 1 },
};
// Слайс — fat pointer (указатель + длина)
const slice: []const u32 = arr[1..3]; // элементы [1] и [2]
const all: []const u32 = &arr; // весь массив как слайс
// Sentinel-terminated slice — как C-строка, но безопаснее
const hello: [:0]const u8 = "hello"; // нуль-терминированный
const raw: [*:0]const u8 = hello.ptr; // сырой указатель с sentinel
// Итерация
for (arr) |val| {
_ = val;
}
for (arr, 0..) |val, i| { // с индексом
_ = val;
_ = i;
}Optional типы
// ?T — значение или null. Для указателей — без дополнительной памяти.
var maybe: ?u32 = null;
maybe = 42;
// Развёртывание через if
if (maybe) |val| {
// val = 42
_ = val;
} else {
// null
}
// orelse — значение по умолчанию
const value = maybe orelse 0;
// .? — unwrap с паникой если null (только в debug-сборках полезно)
const forced = maybe.?;
_ = forced;Error union типы
// Именованный набор ошибок
const FileError = error{
NotFound,
AccessDenied,
DiskFull,
};
// Функция, возвращающая ошибку или значение
fn openConfig(path: []const u8) FileError!Config {
_ = path;
return error.NotFound;
}
// !T — inferred error set (компилятор выводит набор ошибок)
fn doWork() !void {
// ошибки автоматически собираются из вызываемых функций
}Структуры
const Vec3 = struct {
x: f32,
y: f32,
z: f32 = 0.0, // значение по умолчанию
const Self = @This();
// Метод — первый аргумент self
pub fn length(self: Self) f32 {
return @sqrt(self.x * self.x + self.y * self.y + self.z * self.z);
}
// "Статический" метод — без self
pub fn zero() Self {
return .{ .x = 0, .y = 0, .z = 0 };
}
// Мутирующий метод — указатель на self
pub fn scale(self: *Self, factor: f32) void {
self.x *= factor;
self.y *= factor;
self.z *= factor;
}
};
// Инициализация
const v = Vec3{ .x = 1.0, .y = 2.0 }; // z = 0.0 по умолчанию
const origin = Vec3.zero();
_ = origin;
var mutable = Vec3{ .x = 1.0, .y = 1.0 };
mutable.scale(2.0);Enum
const Color = enum(u8) {
red = 0,
green = 1,
blue = 2,
pub fn isWarm(self: Color) bool {
return self == .red;
}
};
const c = Color.green;
const val: u8 = @intFromEnum(c); // 1
const back = @enumFromInt(val); // Color.green (тип выводится)
_ = back;Tagged union
const Token = union(enum) {
number: f64,
string: []const u8,
eof, // без payload
pub fn isEnd(self: Token) bool {
return self == .eof;
}
};
const tok = Token{ .number = 3.14 };
// Pattern matching через switch
switch (tok) {
.number => |n| std.debug.print("number: {d}\n", .{n}),
.string => |s| std.debug.print("string: {s}\n", .{s}),
.eof => std.debug.print("end\n", .{}),
}Обработка ошибок
Zig использует error union (!T) и error set вместо исключений. Нет скрытого control flow: каждая точка возможной ошибки явно помечена.
try, catch, errdefer
const std = @import("std");
fn readFile(allocator: std.mem.Allocator, path: []const u8) ![]u8 {
// try — развернуть значение или вернуть ошибку вызывающему
const file = try std.fs.cwd().openFile(path, .{});
defer file.close(); // defer — выполнится при выходе из scope
// errdefer — выполнится ТОЛЬКО если функция вернёт ошибку
const buf = try allocator.alloc(u8, 4096);
errdefer allocator.free(buf);
const bytes_read = try file.readAll(buf);
return buf[0..bytes_read];
// если всё ok — errdefer НЕ выполнится, вызывающий владеет buf
}
fn processFile(path: []const u8) void {
const allocator = std.heap.page_allocator;
// catch — обработать ошибку локально
const data = readFile(allocator, path) catch |err| {
std.debug.print("ошибка: {}\n", .{err});
return;
};
defer allocator.free(data);
// catch с значением по умолчанию
const config = readFile(allocator, "config.txt") catch &[_]u8{};
_ = config;
_ = data;
}errdefer с захватом ошибки
fn initialize(allocator: std.mem.Allocator) !*Resource {
const r = try allocator.create(Resource);
errdefer |err| {
std.log.err("init failed: {}", .{err});
allocator.destroy(r);
}
try r.setup();
return r;
}switch по ошибкам
fn openConfig(path: []const u8) !Config {
const file = std.fs.cwd().openFile(path, .{}) catch |err| switch (err) {
error.FileNotFound => return Config.defaults(),
error.AccessDenied => {
std.log.err("нет доступа к {s}", .{path});
return err;
},
else => return err,
};
defer file.close();
_ = file;
return Config.defaults();
}Error return traces
В debug-сборках Zig предоставляет трассировку возврата ошибок, которая показывает цепочку try, через которую прошла ошибка. Это не stack trace, а trace пути ошибки через код.
Аллокаторы
Zig не имеет GC и скрытых аллокаций. Память управляется через явный интерфейс std.mem.Allocator, который передаётся как параметр.
Доступные аллокаторы
const std = @import("std");
pub fn main() !void {
// 1. GeneralPurposeAllocator (GPA) — безопасный, ловит утечки и use-after-free
var gpa_state: std.heap.GeneralPurposeAllocator(.{}) = .init;
defer {
const check = gpa_state.deinit();
if (check == .leak) @panic("memory leak detected");
}
const gpa = gpa_state.allocator();
// 2. ArenaAllocator — пакетное освобождение, идеален для запросов/фаз
var arena_state = std.heap.ArenaAllocator.init(std.heap.page_allocator);
defer arena_state.deinit(); // освобождает ВСЁ разом
const arena = arena_state.allocator();
// 3. FixedBufferAllocator — без кучи, из стекового/статического буфера
var buf: [4096]u8 = undefined;
var fba = std.heap.FixedBufferAllocator.init(&buf);
const fixed = fba.allocator();
// 4. page_allocator — прямые страницы ОС, грубый
const page = std.heap.page_allocator;
// 5. c_allocator — обёртка над malloc/free для C interop
const c_alloc = std.heap.c_allocator;
_ = gpa;
_ = arena;
_ = fixed;
_ = page;
_ = c_alloc;
}Паттерны работы с памятью
const std = @import("std");
const Allocator = std.mem.Allocator;
// Функция, которая аллоцирует, принимает аллокатор параметром
fn createBuffer(allocator: Allocator, size: usize) ![]u8 {
return try allocator.alloc(u8, size);
}
// RAII через defer
fn processData(allocator: Allocator) !void {
const buffer = try allocator.alloc(u8, 1024);
defer allocator.free(buffer); // гарантированное освобождение
const result = try allocator.create(Result);
errdefer allocator.destroy(result); // освобождение только при ошибке
try populateResult(result, buffer);
}
// Arena для request-scoped работы
fn handleRequest(parent: Allocator, body: []const u8) !Response {
var arena_state = std.heap.ArenaAllocator.init(parent);
defer arena_state.deinit(); // все временные аллокации — разом
const arena = arena_state.allocator();
const parsed = try parse(arena, body);
const validated = try validate(arena, parsed);
return try buildResponse(parent, validated); // результат — через parent
}Тестовый аллокатор
// std.testing.allocator — ловит утечки в тестах
test "no leaks" {
const allocator = std.testing.allocator;
const data = try allocator.alloc(u8, 100);
defer allocator.free(data);
// если забыть defer — тест УПАДЁТ с информацией об утечке
}Comptime: метапрограммирование
Comptime: выполнение кода во время компиляции. Тот же синтаксис, что и для runtime, но работает в compile-time. Заменяет макросы, шаблоны C++ и кодогенерацию.
Основы
// Comptime-вычисление
const len = comptime blk: {
const s = "hello";
break :blk s.len;
};
// Comptime-параметр — основа обобщённого программирования (generics)
fn max(comptime T: type, a: T, b: T) T {
return if (a > b) a else b;
}
// Компилятор генерирует специализированный код для каждого типа
const result = max(u32, 10, 20);
const float_result = max(f64, 1.5, 2.5);Обобщённые структуры данных
fn LinkedList(comptime T: type) type {
return struct {
const Self = @This();
const Node = struct {
data: T,
next: ?*Node = null,
};
head: ?*Node = null,
len: usize = 0,
pub fn prepend(self: *Self, allocator: std.mem.Allocator, value: T) !void {
const node = try allocator.create(Node);
node.* = .{ .data = value, .next = self.head };
self.head = node;
self.len += 1;
}
pub fn toSlice(self: Self, allocator: std.mem.Allocator) ![]T {
var result = try allocator.alloc(T, self.len);
var current = self.head;
var i: usize = 0;
while (current) |node| {
result[i] = node.data;
current = node.next;
i += 1;
}
return result;
}
};
}
// Использование — как шаблоны, но через обычный вызов функции
var list: LinkedList(i32) = .{};Интроспекция типов
fn debugPrint(value: anytype) void {
const T = @TypeOf(value);
const info = @typeInfo(T);
switch (info) {
// 0.15: теги в нижнем регистре, ключевые слова — через @""
.int => std.debug.print("int: {}\n", .{value}),
.float => std.debug.print("float: {d}\n", .{value}),
.pointer => std.debug.print("pointer\n", .{}),
.@"struct" => {
inline for (std.meta.fields(T)) |field| {
std.debug.print("{s}: {any}\n", .{
field.name,
@field(value, field.name),
});
}
},
else => std.debug.print("other type\n", .{}),
}
}Comptime строки
// Генерация строк в compile-time
fn makeFieldName(comptime prefix: []const u8, comptime index: usize) []const u8 {
return prefix ++ std.fmt.comptimePrint("{}", .{index});
}
const names = comptime blk: {
var result: [3][]const u8 = undefined;
for (0..3) |i| {
result[i] = makeFieldName("field_", i);
}
break :blk result;
};
// names = { "field_0", "field_1", "field_2" }Ограничения comptime
- Нет I/O и системных вызовов
- Нет доступа к runtime-состоянию
- Нет inline assembly
- Циклы должны завершаться (нет бесконечных)
- Гермети чно, воспроизводимо, безопасно, кешируемо
Управляющие конструкции
if, while, for
// if — также выражение
const abs_val = if (x < 0) -x else x;
// if с optional
if (optional_value) |val| {
// val развёрнут
_ = val;
} else {
// null
}
// while
var i: usize = 0;
while (i < 10) : (i += 1) { // "продолжение" — аналог for (;;i++)
if (i == 5) continue;
if (i == 8) break;
}
// while с optional
while (iterator.next()) |item| {
process(item);
}
// for — по слайсам и ranges
for (items) |item| {
_ = item;
}
for (items, 0..) |item, index| { // с индексом
_ = item;
_ = index;
}
for (0..10) |i| { // range
_ = i;
}switch
const result = switch (value) {
1, 2, 3 => "small",
4...10 => "medium", // range (включительно)
else => "large",
};
// switch по tagged union — exhaustive (компилятор проверяет все варианты)
switch (token) {
.number => |n| handleNumber(n),
.string => |s| handleString(s),
.eof => return,
}Labeled switch (с 0.14), для state machines
const State = enum { start, identifier, number, done };
fn tokenize(input: []const u8) void {
var i: usize = 0;
const state: State = .start;
// Labeled switch — continue переходит к другому case
// без overhead цикла (компилятор: unconditional branch)
switch (state) {
.start => {
if (i >= input.len) {
continue :sw .done;
}
if (input[i] >= 'a' and input[i] <= 'z') {
continue :sw .identifier;
}
if (input[i] >= '0' and input[i] <= '9') {
continue :sw .number;
}
continue :sw .done;
},
.identifier => {
i += 1;
continue :sw .start;
},
.number => {
i += 1;
continue :sw .start;
},
.done => return,
}
}defer и errdefer
fn doWork() !void {
const resource = try acquire();
defer release(resource); // ВСЕГДА выполнится при выходе из scope
const buffer = try allocate();
errdefer free(buffer); // только если функция вернёт ошибку
try use(resource, buffer);
// если ok — errdefer НЕ выполнится
}
// Порядок: defer-ы выполняются в обратном порядке (LIFO)
{
defer std.debug.print("3\n", .{});
defer std.debug.print("2\n", .{});
defer std.debug.print("1\n", .{});
}
// Вывод: 1, 2, 3Стандартная библиотека
Вывод (0.15, Writergate)
const std = @import("std");
pub fn main() !void {
// 0.15: явный буфер, обязательный flush
var stdout_buf: [4096]u8 = undefined;
var stdout_writer = std.fs.File.stdout().writer(&stdout_buf);
const stdout = &stdout_writer.interface;
try stdout.print("Hello, {s}!\n", .{"world"});
try stdout.flush(); // КРИТИЧНО: без flush вывод потеряется
// Для отладки — std.debug.print (без буфера, сразу в stderr)
std.debug.print("debug: {}\n", .{42});
}ArrayList (0.15, Unmanaged)
const std = @import("std");
pub fn main() !void {
var gpa_state: std.heap.GeneralPurposeAllocator(.{}) = .init;
defer _ = gpa_state.deinit();
const allocator = gpa_state.allocator();
// 0.15: ArrayListUnmanaged — основной тип
// Аллокатор передаётся в каждый мутирующий вызов
var list: std.ArrayListUnmanaged(i32) = .{};
defer list.deinit(allocator);
try list.append(allocator, 42);
try list.append(allocator, 99);
try list.appendSlice(allocator, &.{ 1, 2, 3 });
// Чтение — без аллокатора
const last = list.pop();
_ = last;
for (list.items) |item| {
_ = item;
}
// Сортировка
std.mem.sort(i32, list.items, {}, std.sort.asc(i32));
}HashMap
const std = @import("std");
pub fn main() !void {
var gpa_state: std.heap.GeneralPurposeAllocator(.{}) = .init;
defer _ = gpa_state.deinit();
const allocator = gpa_state.allocator();
// StringHashMap — ключи []const u8
var map = std.StringHashMapUnmanaged(u32){};
defer map.deinit(allocator);
try map.put(allocator, "one", 1);
try map.put(allocator, "two", 2);
if (map.get("one")) |val| {
std.debug.print("one = {}\n", .{val});
}
// AutoHashMap — для произвольных ключей
var auto_map = std.AutoHashMapUnmanaged(u64, []const u8){};
defer auto_map.deinit(allocator);
try auto_map.put(allocator, 42, "answer");
// Итерация
var it = map.iterator();
while (it.next()) |entry| {
std.debug.print("{s}: {}\n", .{ entry.key_ptr.*, entry.value_ptr.* });
}
}Строки и работа с памятью
const std = @import("std");
pub fn main() void {
// Строки — просто []const u8
const hello: []const u8 = "Hello, World!";
_ = hello;
// Сравнение
const equal = std.mem.eql(u8, "abc", "abc"); // true
_ = equal;
// Поиск
if (std.mem.indexOf(u8, "hello world", "world")) |pos| {
_ = pos; // 6
}
// Начинается/заканчивается
const starts = std.mem.startsWith(u8, "hello", "hel"); // true
_ = starts;
// Токенизация (разбиение по разделителю)
var it = std.mem.tokenizeScalar(u8, "hello world foo", ' ');
while (it.next()) |token| {
_ = token; // "hello", "world", "foo"
}
// Split (сохраняет пустые элементы)
var it2 = std.mem.splitScalar(u8, "a,,b,c", ',');
while (it2.next()) |part| {
_ = part; // "a", "", "b", "c"
}
}Форматирование строк
const std = @import("std");
pub fn main() !void {
var gpa_state: std.heap.GeneralPurposeAllocator(.{}) = .init;
defer _ = gpa_state.deinit();
const allocator = gpa_state.allocator();
// Форматирование в аллоцированную строку
const msg = try std.fmt.allocPrint(allocator, "user_{d}_score_{d}", .{ 42, 100 });
defer allocator.free(msg);
// Форматирование в буфер (без аллокации)
var buf: [256]u8 = undefined;
const result = try std.fmt.bufPrint(&buf, "x={d}, y={d}", .{ 10, 20 });
_ = result; // слайс из buf
// Спецификаторы формата
// {d} — decimal integer
// {x} — hex
// {b} — binary
// {s} — string ([]const u8)
// {any} — любой тип (debug format)
// {e} — float exponential
// {d:.2} — float с 2 знаками
}Файловый I/O
const std = @import("std");
pub fn main() !void {
var gpa_state: std.heap.GeneralPurposeAllocator(.{}) = .init;
defer _ = gpa_state.deinit();
const allocator = gpa_state.allocator();
// Чтение файла целиком
const content = try std.fs.cwd().readFileAlloc(allocator, "data.txt", 1024 * 1024);
defer allocator.free(content);
// Запись файла
try std.fs.cwd().writeFile(.{
.sub_path = "output.txt",
.data = "Hello, file!\n",
});
// Построчное чтение
const file = try std.fs.cwd().openFile("data.txt", .{});
defer file.close();
var buf: [4096]u8 = undefined;
var reader_buf: [4096]u8 = undefined;
var reader = file.reader(&reader_buf);
while (try reader.interface.readUntilDelimiterOrEof(&buf, '\n')) |line| {
_ = line;
}
// Работа с директориями
var dir = try std.fs.cwd().openDir("src", .{ .iterate = true });
defer dir.close();
var dir_it = dir.iterate();
while (try dir_it.next()) |entry| {
std.debug.print("{s} ({s})\n", .{ entry.name, @tagName(entry.kind) });
}
}JSON
const std = @import("std");
const Config = struct {
host: []const u8,
port: u16,
debug: bool = false,
};
pub fn main() !void {
var gpa_state: std.heap.GeneralPurposeAllocator(.{}) = .init;
defer _ = gpa_state.deinit();
const allocator = gpa_state.allocator();
// Парсинг JSON в структуру
const json_str =
\\{"host": "localhost", "port": 8080, "debug": true}
;
const parsed = try std.json.parseFromSlice(Config, allocator, json_str, .{});
defer parsed.deinit();
const config = parsed.value;
std.debug.print("host={s} port={}\n", .{ config.host, config.port });
// Сериализация структуры в JSON
var buf: [1024]u8 = undefined;
var stream = std.io.fixedBufferStream(&buf);
try std.json.stringify(config, .{}, stream.writer());
const json_output = stream.getWritten();
_ = json_output;
}Потоки (Threads)
const std = @import("std");
fn worker(id: usize) void {
std.debug.print("Worker {} started\n", .{id});
std.time.sleep(100 * std.time.ns_per_ms);
std.debug.print("Worker {} done\n", .{id});
}
pub fn main() !void {
// Запуск потоков
var threads: [4]std.Thread = undefined;
for (&threads, 0..) |*t, i| {
t.* = try std.Thread.spawn(.{}, worker, .{i});
}
for (threads) |t| {
t.join();
}
// Mutex
var mutex: std.Thread.Mutex = .{};
var shared_counter: u64 = 0;
var workers: [4]std.Thread = undefined;
for (&workers) |*t| {
t.* = try std.Thread.spawn(.{}, struct {
fn run(m: *std.Thread.Mutex, counter: *u64) void {
for (0..1000) |_| {
m.lock();
defer m.unlock();
counter.* += 1;
}
}
}.run, .{ &mutex, &shared_counter });
}
for (workers) |t| {
t.join();
}
std.debug.print("counter = {}\n", .{shared_counter}); // 4000
}Сетевое программирование
const std = @import("std");
// TCP сервер
pub fn main() !void {
const address = std.net.Address.initIp4(.{ 127, 0, 0, 1 }, 8080);
var server = try address.listen(.{
.reuse_address = true,
});
defer server.deinit();
std.debug.print("Listening on :8080\n", .{});
while (true) {
const conn = try server.accept();
defer conn.stream.close();
var read_buf: [4096]u8 = undefined;
var reader_buf: [4096]u8 = undefined;
var reader = conn.stream.reader(&reader_buf);
const request = try reader.interface.readUntilDelimiterOrEof(&read_buf, '\n');
_ = request;
var write_buf: [4096]u8 = undefined;
var writer = conn.stream.writer(&write_buf);
try writer.interface.print("HTTP/1.1 200 OK\r\nContent-Length: 5\r\n\r\nHello", .{});
try writer.interface.flush();
}
}Система сборки (build.zig)
Zig использует декларативно-императивную систему сборки, написанную на самом Zig. Граф шагов (DAG) выполняется параллельно.
Базовый build.zig (0.15)
const std = @import("std");
pub fn build(b: *std.Build) void {
const target = b.standardTargetOptions(.{});
const optimize = b.standardOptimizeOption(.{});
// 0.15: createModule + root_module
const exe = b.addExecutable(.{
.name = "myapp",
.root_module = b.createModule(.{
.root_source_file = b.path("src/main.zig"),
.target = target,
.optimize = optimize,
}),
});
b.installArtifact(exe);
// Шаг запуска
const run_cmd = b.addRunArtifact(exe);
run_cmd.step.dependOn(b.getInstallStep());
if (b.args) |args| {
run_cmd.addArgs(args);
}
const run_step = b.step("run", "Run the application");
run_step.dependOn(&run_cmd.step);
// Шаг тестирования
const unit_tests = b.addTest(.{
.root_module = b.createModule(.{
.root_source_file = b.path("src/main.zig"),
.target = target,
.optimize = optimize,
}),
});
const run_unit_tests = b.addRunArtifact(unit_tests);
const test_step = b.step("test", "Run unit tests");
test_step.dependOn(&run_unit_tests.step);
}Библиотеки (0.15)
// Статическая/динамическая библиотека
const lib = b.addLibrary(.{
.name = "mylib",
.linkage = .static, // или .dynamic
.root_module = b.createModule(.{
.root_source_file = b.path("src/lib.zig"),
.target = target,
.optimize = optimize,
}),
});File system watching и incremental compilation
# Непрерывная пересборка при изменении файлов (с 0.14)
zig build --watch
# С debounce
zig build --watch --debounce 200
# Инкрементальная компиляция (экспериментально)
zig build --watch -fincrementalМенеджер пакетов (build.zig.zon)
Децентрализованный, content-addressed. Зависимости указываются по URL, без центрального реестра.
Манифест
// build.zig.zon
.{
.name = .myproject,
.version = "0.1.0",
.minimum_zig_version = "0.15.0",
.dependencies = .{
// Удалённая зависимость
.zap = .{
.url = "https://github.com/zigzap/zap/archive/refs/tags/v0.2.0.tar.gz",
.hash = "1220aababababababababababababababababababababababababababababababababab",
},
// Локальная зависимость
.mylib = .{
.path = "../mylib",
},
// Ленивая — скачивается только при использовании
.optional_dep = .{
.url = "https://example.com/dep.tar.gz",
.hash = "1220...",
.lazy = true,
},
},
.paths = .{
"build.zig",
"build.zig.zon",
"src",
},
}Подключение зависимостей в build.zig
pub fn build(b: *std.Build) void {
const target = b.standardTargetOptions(.{});
const optimize = b.standardOptimizeOption(.{});
const zap_dep = b.dependency("zap", .{
.target = target,
.optimize = optimize,
});
const exe = b.addExecutable(.{
.name = "myapp",
.root_module = b.createModule(.{
.root_source_file = b.path("src/main.zig"),
.target = target,
.optimize = optimize,
.imports = &.{
.{ .name = "zap", .module = zap_dep.module("zap") },
},
}),
});
b.installArtifact(exe);
}Работа с хешами
# Добавить зависимость (автоматически вычислит хеш)
zig fetch --save https://github.com/user/repo/archive/v1.0.tar.gz
# Или поставить placeholder-хеш, zig build покажет правильный
zig build
# error: hash mismatch... expected 1220abc..., found 1220def...
# Скопировать found-хеш в build.zig.zonРеестры пакетов
- Zigistry, community-реестр
- Зависимости URL-based (git archives, tarballs), не привязаны к реестру
C interop
Zig обеспечивает first-class совместимость с C: может компилировать C/C++ код, линковать C-библиотеки, транслировать C-заголовки.
@cImport (текущий способ, запланирован перенос в build system)
const c = @cImport({
@cDefine("_GNU_SOURCE", {});
@cInclude("stdio.h");
@cInclude("mylib.h");
});
pub fn main() void {
_ = c.printf("Hello from C\n");
}C interop через build system (рекомендуемый)
// build.zig
pub fn build(b: *std.Build) void {
const target = b.standardTargetOptions(.{});
const optimize = b.standardOptimizeOption(.{});
const exe = b.addExecutable(.{
.name = "myapp",
.root_module = b.createModule(.{
.root_source_file = b.path("src/main.zig"),
.target = target,
.optimize = optimize,
}),
});
// Линковка libc
exe.linkLibC();
// Линковка системной библиотеки
exe.linkSystemLibrary("sqlite3");
// Компиляция C-файлов
exe.addCSourceFiles(.{
.files = &.{ "src/legacy.c", "src/wrapper.c" },
.flags = &.{ "-Wall", "-O2" },
});
// Include-пути
exe.addIncludePath(b.path("include"));
// Трансляция C-заголовка в Zig-модуль
const translate = b.addTranslateC(.{
.root_source_file = b.path("include/myheader.h"),
.target = target,
.optimize = optimize,
});
exe.root_module.addImport("myheader", translate.createModule());
b.installArtifact(exe);
}Calling conventions и ABI
// Extern-функция с C calling convention
extern "c" fn printf(fmt: [*:0]const u8, ...) c_int;
// Zig-функция, вызываемая из C
export fn zig_add(a: c_int, b: c_int) c_int {
return a + b;
}
// Явный calling convention (0.15 синтаксис)
fn sigHandler(_: c_int) callconv(.c) void {
// обработчик сигнала с C ABI
}Zig как C/C++ компилятор
# Drop-in замена C-компилятора (с кросс-компиляцией из коробки)
zig cc -o hello hello.c
zig c++ -o hello hello.cpp
# Кросс-компиляция C-кода
zig cc -target aarch64-linux-gnu hello.c
zig cc -target x86_64-windows-gnu hello.cТестирование
Тестирование встроено в язык и тулчейн. Тестовые блоки являются частью исходного кода, а не отдельным фреймворком.
Тестовые блоки
const std = @import("std");
const expect = std.testing.expect;
fn add(a: i32, b: i32) i32 {
return a + b;
}
test "addition works" {
const result = add(2, 3);
try expect(result == 5);
}
test "string equality" {
const input = "Hello, World!";
try std.testing.expectEqualStrings("Hello", input[0..5]);
}
test "error conditions" {
const result = validate("");
try std.testing.expectError(error.EmptyInput, result);
}
test "no memory leaks" {
const allocator = std.testing.allocator; // ловит утечки
var list: std.ArrayListUnmanaged(u8) = .{};
defer list.deinit(allocator);
try list.append(allocator, 42);
try expect(list.items.len == 1);
}
fn validate(input: []const u8) !void {
if (input.len == 0) return error.EmptyInput;
}Утилиты std.testing
// Точное равенство
try std.testing.expectEqual(@as(u32, 42), computeValue());
// Строки (с diff при ошибке)
try std.testing.expectEqualStrings("expected", actual);
// Слайсы
try std.testing.expectEqualSlices(u8, expected_bytes, actual_bytes);
// Приближённое сравнение float
try std.testing.expectApproxEqAbs(@as(f64, 3.14), computePi(), 0.01);
// Проверка формата
try std.testing.expectFmt("hello 42", "{s} {}", .{ "hello", @as(u32, 42) });Запуск тестов
# Все тесты в файле
zig test src/main.zig
# Через build system
zig build test
# Фильтр по имени
zig test src/main.zig --test-filter "string"
# С подробным выводом
zig test src/main.zig --verboseAsync I/O (0.16-dev, экспериментально)
История
Pre-0.11 — языковые async/await на stackless coroutines
0.11 (2023) — async/await удалены, идёт редизайн
0.14–0.15 — нет async/await, используйте std.Thread
0.16-dev — новый async I/O framework, fibersНовый дизайн (0.16)
// std.Io — интерфейс, передаётся как параметр (как аллокатор)
// Две реализации:
// std.Io.Threaded — синхронные операции с потоками
// std.Io.Evented — event loop (io_uring / kqueue), fibers
// Функция принимает Io-параметр — работает с обеими реализациями
fn fetchData(io: std.Io, url: []const u8) ![]u8 {
_ = url;
_ = io;
// ...
}
// Ключевое различие:
// async — разделение вызова и возврата (infallible)
// concurrent — параллельное выполнение операцийДизайн “Writergate” в 0.15 является подготовкой к async: явные буферы и flush позволят переключаться между синхронным и асинхронным I/O без изменения пользовательского кода.
Паттерны
RAII через defer/errdefer
fn openAndProcess(allocator: std.mem.Allocator, path: []const u8) !Result {
const file = try std.fs.cwd().openFile(path, .{});
defer file.close();
const data = try allocator.alloc(u8, 4096);
errdefer allocator.free(data); // только при ошибке
const result = try parse(data, file);
return result; // data передаётся вызывающему
}Sentinel values вместо null-указателей
// Вместо NULL-указателя — optional
fn findUser(id: u64) ?*User {
// ...
return null; // явно, а не NULL-crash
}Comptime interfaces (duck typing в compile-time)
fn serialize(writer: anytype, value: anytype) !void {
const T = @TypeOf(value);
if (@hasDecl(T, "serialize")) {
try value.serialize(writer);
} else {
try writer.print("{any}", .{value});
}
}Builder pattern через struct init
const ServerConfig = struct {
host: []const u8 = "0.0.0.0",
port: u16 = 8080,
max_connections: u32 = 1024,
read_timeout_ms: u64 = 30_000,
};
fn startServer(config: ServerConfig) !void {
_ = config;
// ...
}
// Использование — лаконично через defaults
try startServer(.{}); // все defaults
try startServer(.{ .port = 3000 }); // только порт
try startServer(.{ .host = "127.0.0.1", .port = 9090 });Comptime dispatch для полиморфизма
fn Animal(comptime T: type) type {
return struct {
inner: T,
pub fn speak(self: @This()) []const u8 {
return self.inner.speak();
}
};
}
const Dog = struct {
pub fn speak(_: Dog) []const u8 { return "Woof!"; }
};
const Cat = struct {
pub fn speak(_: Cat) []const u8 { return "Meow!"; }
};
const my_dog = Animal(Dog){ .inner = .{} };
const sound = my_dog.speak(); // "Woof!" — без vtable, без heap
_ = sound;Branch hints (0.15)
// Заменяет старый @setCold()
fn handleRareCase() void {
@branchHint(.cold);
// редко выполняемый код
}
fn processItem(x: u32) void {
if (x > threshold) {
@branchHint(.likely);
// горячий путь
} else {
@branchHint(.unlikely);
// холодный путь
}
}Ключевые изменения по версиям
0.14.0 (март 2025)
- Labeled switch:
continue :swдля state machines (13% ускорение tokenizer) - @splat расширен на массивы
- File system watching:
zig build --watch - Incremental compilation:
-fincremental - 251 контрибьютор, 3467 коммитов
0.15.x (август–октябрь 2025), ломающий релиз
Компилятор:
- x86 self-hosted backend по умолчанию для debug (~5x быстрее LLVM)
- aarch64 backend активно развивается
Writergate (I/O overhaul):
- Явные буферы для readers/writers
- Обязательный
.flush()перед выходом std.io.getStdOut()больше не работает
Стандартная библиотека:
ArrayList→ArrayListUnmanaged(аллокатор в каждый вызов)usingnamespaceудалён- Теги типов в нижнем регистре (
.@"struct"вместо.Struct) @branchHint(.cold)вместо@setCold()std.rand→std.Randomstd.TailQueue→std.DoublyLinkedListstd.zig.CrossTarget→std.Target.Query
Build system:
b.createModule()+.root_moduleb.addLibrary(.{ .linkage = .static })вместоaddStaticLibrary()
0.16.0-dev (ожидается 2026)
std.Ioинтерфейс для всего I/Ostd.Io.Threadedиstd.Io.Evented- Fibers / stackful coroutines
- io_uring (Linux) / kqueue (macOS) backends
Частые проблемы
Забыли flush stdout
// ПРОБЛЕМА: вывод не появляется
var buf: [4096]u8 = undefined;
var writer = std.fs.File.stdout().writer(&buf);
try writer.interface.print("Hello\n", .{});
// программа завершается — вывод потерян
// РЕШЕНИЕ: всегда flush перед выходом
try writer.interface.flush();Утечка памяти: забыли defer
// ПРОБЛЕМА: утечка
fn bad() ![]u8 {
const allocator = std.heap.page_allocator;
const buf = try allocator.alloc(u8, 1024);
try riskyOperation(buf); // если ошибка — buf утёк
return buf;
}
// РЕШЕНИЕ: errdefer
fn good() ![]u8 {
const allocator = std.heap.page_allocator;
const buf = try allocator.alloc(u8, 1024);
errdefer allocator.free(buf);
try riskyOperation(buf);
return buf;
}Dangling pointer из слайса
// ПРОБЛЕМА: слайс указывает на стековый массив, который уже уничтожен
fn bad() []const u8 {
var buf: [64]u8 = undefined;
const result = std.fmt.bufPrint(&buf, "hello {}", .{42}) catch unreachable;
return result; // dangling pointer!
}
// РЕШЕНИЕ: аллоцировать или копировать
fn good(allocator: std.mem.Allocator) ![]u8 {
return try std.fmt.allocPrint(allocator, "hello {}", .{42});
}Capture по значению в цикле
// ПРОБЛЕМА: capture в for — копия, не ссылка
var items = [_]u32{ 1, 2, 3 };
for (items) |item| {
_ = item + 1; // item — копия, items не изменился
}
// РЕШЕНИЕ: capture по указателю
for (&items) |*item| {
item.* += 1; // мутирует оригинал
}Integer overflow в release
// ПРОБЛЕМА: в Debug overflow = паника, в Release = undefined behavior
const a: u8 = 255;
const b = a + 1; // Debug: паника. ReleaseFast: UB (0 или что угодно)
// РЕШЕНИЕ: явные wrapping/saturating операции
const c = a +% 1; // wrapping: 0
const d = a +| 1; // saturating: 255
_ = b;
_ = c;
_ = d;comptime vs runtime
// ПРОБЛЕМА: runtime-значение в comptime-контексте
fn bad(slice: []const u8) void {
// const arr: [slice.len]u8 = undefined; // ОШИБКА: slice.len не comptime
_ = slice;
}
// РЕШЕНИЕ: использовать слайсы или передать размер как comptime
fn good(comptime N: usize) [N]u8 {
return @splat(0);
}Сравнение слайсов
// ПРОБЛЕМА: == не работает для слайсов
const a: []const u8 = "hello";
const b: []const u8 = "hello";
// if (a == b) {} // ОШИБКА компиляции: operator == not allowed for []const u8
// РЕШЕНИЕ: std.mem.eql
if (std.mem.eql(u8, a, b)) {
// равны
}usingnamespace удалён (0.15)
// ПРОБЛЕМА: старый код с usingnamespace не компилируется
// usingnamespace @import("other.zig"); // ошибка в 0.15
// РЕШЕНИЕ: явный импорт
const other = @import("other.zig");
// используйте other.Foo, other.bar()ArrayList → ArrayListUnmanaged (0.15)
// ПРОБЛЕМА: старый код с ArrayList не компилируется
// var list = std.ArrayList(i32).init(allocator); // ошибка в 0.15
// РЕШЕНИЕ: ArrayListUnmanaged, аллокатор в каждый вызов
var list: std.ArrayListUnmanaged(i32) = .{};
defer list.deinit(allocator);
try list.append(allocator, 42);Type reflection tags (0.15)
// ПРОБЛЕМА: старые теги в верхнем регистре
// switch (@typeInfo(T)) { .Struct => ... } // ошибка в 0.15
// РЕШЕНИЕ: нижний регистр, ключевые слова через @""
switch (@typeInfo(T)) {
.@"struct" => {}, // struct — ключевое слово
.int => {}, // int — не ключевое слово
.float => {},
else => {},
}Задачи для прокачки до Middle
Задачи расположены от простых к сложным. Каждая прокачивает конкретные навыки. Все задачи прикладные: после выполнения у тебя будет работающий инструмент или библиотека.
Уровень 1. Основы языка
1.1. CLI-утилита: подсчёт строк (wc -l)
Навыки: файловый I/O, аргументы командной строки, error handling, форматированный вывод
Требования:
- Принимает список файлов через argv
- Для каждого файла выводит: количество строк, слов, байт, имя файла
- Поддерживает stdin (если файлов нет — читает из stdin)
- Итоговая строка с суммой (если файлов > 1)
- Корректно обрабатывает ошибки: файл не найден, нет прав, бинарный файл
Пример:
$ ./zwc src/main.zig src/lib.zig
142 387 4521 src/main.zig
89 201 2103 src/lib.zig
231 588 6624 total1.2. Парсер CSV
Навыки: строковая обработка, аллокаторы, слайсы, итераторы, тестирование
Требования:
- Парсит CSV с произвольным разделителем (по умолчанию запятая)
- Поддерживает кавычки: "field with, comma" → одно поле
- Поддерживает escape кавычек: "he said ""hello""" → he said "hello"
- Итератор по строкам, итератор по полям в строке
- Работает с любым аллокатором
- Тесты на edge cases: пустые поля, переводы строк в кавычках, trailing CRLF
API:
var parser = csv.parse(allocator, data, .{ .delimiter = ';' });
defer parser.deinit();
while (parser.next()) |row| {
for (row.fields()) |field| { ... }
}1.3. Ring buffer (кольцевой буфер)
Навыки: generics через comptime, указатели, управление памятью, тестирование
Требования:
- Generic: RingBuffer(comptime T: type, comptime capacity: usize)
- Операции: push, pop, peek, isFull, isEmpty, len
- Фиксированный размер (без аллокаций в runtime)
- Поддержка итерации (for-compatible iterator)
- Overwrite-режим: при push в полный буфер — затирает старейший элемент
Тесты:
- push/pop последовательности
- wrap-around (переход через конец массива)
- overwrite-режим
- итерация в правильном порядке
- пустой и полный буфер1.4. Генератор случайных паролей
Навыки: std.Random, работа со строками, компоновка буферов, CLI
Требования:
- Настраиваемая длина (по умолчанию 16)
- Флаги: --uppercase, --lowercase, --digits, --symbols (по умолчанию все)
- Флаг --count N (генерировать N паролей)
- Флаг --exclude "chars" (исключить символы)
- Криптографически стойкий: std.crypto.random
- Проверка: сгенерированный пароль содержит хотя бы по одному символу из каждой
включённой категории
Пример:
$ ./zpasswd --length 24 --count 5 --exclude "0OlI1"Уровень 2. Структуры данных и алгоритмы
2.1. HashMap с открытой адресацией
Навыки: хеширование, управление памятью, generics, resize, тестирование под нагрузкой
Требования:
- Generic: HashMap(comptime K: type, comptime V: type)
- Robin Hood hashing (при коллизии — вставка ближе к идеальной позиции)
- Автоматический resize при load factor > 0.75
- Операции: put, get, remove, contains, count, iterator
- Принимает аллокатор как параметр
- Поддержка пользовательских hash/eql функций
- Бенчмарк: сравнить производительность с std.HashMapUnmanaged
Тесты:
- 100_000 вставок/поисков
- коллизии (намеренно плохая хеш-функция)
- удаление без tombstone-утечек
- корректный resize2.2. Arena allocator с диагностикой
Навыки: аллокаторы (std.mem.Allocator interface), страницы памяти, introspection
Требования:
- Реализует std.mem.Allocator interface
- Аллоцирует блоками (chunks) из parent allocator
- reset() — освободить всё без возврата памяти ОС (переиспользование)
- deinit() — вернуть всё parent allocator
- Диагностика: total_allocated, peak_usage, num_allocations, num_chunks
- Опциональный лимит (maxBytes) — возвращает error.OutOfMemory при превышении
- Alignment-aware: корректно выравнивает все аллокации
Тесты:
- Использование с std.ArrayListUnmanaged и std.HashMapUnmanaged
- Проверка что reset реально переиспользует память
- Проверка alignment для разных типов
- Утечки через std.testing.allocator как parent2.3. Thread pool
Навыки: многопоточность, синхронизация, Mutex, Condition, очереди задач
Требования:
- Пул из N потоков (настраиваемо)
- submit(fn, args) -> Future — отправить задачу
- Future.wait() -> результат — дождаться завершения
- Graceful shutdown: finish() — дождаться всех задач и остановить потоки
- Очередь задач на основе lock-free или mutex-protected ring buffer
- Обработка паник в worker-потоках (не убивать весь пул)
Тесты:
- 1000 задач на пул из 4 потоков
- Все результаты корректны
- Нет data races (проверить через GPA + ThreadSanitizer)
- Graceful shutdown не теряет задачиУровень 3. Сетевое программирование
3.1. HTTP/1.1 сервер
Навыки: TCP сокеты, парсинг протокола, потоки, буферизованный I/O, keep-alive
Требования:
- Парсинг HTTP/1.1 запросов (метод, путь, заголовки, тело)
- Поддержка методов: GET, POST, PUT, DELETE, HEAD
- Content-Length и Transfer-Encoding: chunked (чтение тела)
- Keep-alive (Connection: keep-alive / close)
- Роутинг: регистрация обработчиков по пути и методу
- Статические файлы: отдача из директории с правильным Content-Type
- Middleware: logging (метод, путь, статус, время обработки)
- Многопоточность: по потоку на соединение (или через thread pool из задачи 2.3)
API:
var server = try HttpServer.init(allocator, .{ .port = 8080 });
server.get("/", handleIndex);
server.get("/api/users", handleUsers);
server.post("/api/users", createUser);
server.static("/static", "./public");
try server.listen();3.2. TCP чат (клиент + сервер)
Навыки: сетевое программирование, многопользовательский ввод/вывод, протокол сообщений
Требования:
Сервер:
- Принимает множество TCP-соединений
- Каждое соединение = участник чата
- Broadcast: сообщение от одного уходит всем остальным
- Команды: /nick <name>, /list, /quit, /whisper <nick> <msg>
- Уведомления: "User X joined", "User X left"
- Обработка обрыва соединения (не ронять сервер)
Клиент:
- Подключается к серверу по IP:port
- Неблокирующий ввод: одновременно читает stdin и сокет
- Отображение сообщений в реальном времени
Протокол (бинарный или текстовый — на выбор):
[2 bytes: msg_type][4 bytes: length][payload]
или текстовый: "MSG nick message\n", "CMD /nick newname\n"3.3. DNS resolver
Навыки: бинарные протоколы, UDP сокеты, bit-level парсинг, сериализация
Требования:
- Формирование DNS-запроса (Query) по RFC 1035
- Отправка через UDP на 8.8.8.8:53 (или настраиваемый сервер)
- Парсинг ответа: заголовок, question section, answer section
- Поддержка типов записей: A, AAAA, CNAME, MX, TXT, NS
- Обработка сжатия имён (pointer compression в DNS)
- Таймаут на ответ (3 секунды)
- CLI: ./zdns example.com A
Пример:
$ ./zdns google.com A
google.com IN A 142.250.74.206 (TTL: 300)
$ ./zdns google.com MX
google.com IN MX 10 smtp.google.com (TTL: 600)Уровень 4. Системное программирование
4.1. Key-Value хранилище с persistence
Навыки: файловый I/O, сериализация, буферизация, crash recovery, бенчмаркинг
Требования:
- API: put(key, value), get(key) -> ?value, delete(key), scan(prefix) -> iterator
- Ключи и значения: []const u8 (произвольные байты)
- Write-Ahead Log (WAL): каждая операция сначала пишется в лог
- Периодический snapshot: дамп всех данных на диск
- Recovery: при запуске — прочитать snapshot + replay WAL
- fsync после записи в WAL (гарантия durability)
- Бенчмарк: ops/sec для put и get (цель: >100K ops/sec на SSD)
- Ограничение по памяти: если данные не влезают — вытеснение (LRU)
Формат WAL:
[4B: crc32][1B: op_type][4B: key_len][4B: val_len][key][value]4.2. Memory-mapped файловый поиск (grep)
Навыки: mmap, SIMD-подсказки через @Vector, многопоточность, рекурсивный обход FS
Требования:
- Поиск строки/regex в файлах
- Memory-mapped I/O (std.posix.mmap или std.fs)
- Рекурсивный обход директорий
- Параллельный поиск: по потоку на файл (через thread pool)
- Вывод: файл:строка:содержимое (как grep -n)
- Фильтрация по расширению: --include "*.zig" --exclude "zig-cache"
- Подсветка совпадения в выводе (ANSI escape codes)
- Бенчмарк: сравнить с системным grep на большом дереве исходников
Бонус:
- Boyer-Moore или SIMD-ускоренный поиск подстроки4.3. Логгер с ротацией
Навыки: потокобезопасность, файловый I/O, форматирование, конфигурация
Требования:
- Уровни: debug, info, warn, err, fatal
- Потокобезопасный (множество потоков пишут одновременно)
- Вывод: stderr, файл, или оба
- Формат: [2025-03-15T10:23:45.123Z] [INFO] [thread:3] message
- Ротация по размеру: при превышении max_size → переименовать в .1, .2, ...
- Ротация по количеству: хранить не более N файлов
- Structured logging: log.info("request", .{ .method = "GET", .path = "/api", .ms = 42 })
- Реализует std.log interface (scoped logger)
- Lazy formatting: аргументы форматируются только если уровень >= текущего
API:
const log = try Logger.init(allocator, .{
.level = .info,
.file_path = "/var/log/myapp.log",
.max_size = 10 * 1024 * 1024, // 10MB
.max_files = 5,
});
defer log.deinit();
log.info("server started on port {}", .{8080});Уровень 5. Comptime и метапрограммирование
5.1. Comptime ORM / query builder
Навыки: comptime type generation, @typeInfo, inline for, строковые операции в comptime
Требования:
- Определение модели через обычную struct:
const User = struct {
id: u64,
name: []const u8,
email: []const u8,
age: ?u32,
created_at: i64,
};
- Comptime-генерация:
- CREATE TABLE SQL из struct (типы Zig → SQL типы)
- INSERT SQL с placeholder-ами
- SELECT с маппингом результата обратно в struct
- WHERE builder с типобезопасными условиями
- Валидация в compile-time:
- Несуществующее поле → ошибка компиляции
- Неправильный тип в условии → ошибка компиляции
API:
const db = try Database.init(allocator, "sqlite3.db");
const users = db.table(User);
// Генерирует: SELECT id, name, email, age, created_at FROM users WHERE age > ?
const results = try users.where(.{ .age = .{ .gt = 18 } }).select();
// Генерирует: INSERT INTO users (name, email, age) VALUES (?, ?, ?)
try users.insert(.{ .name = "Alice", .email = "a@b.com", .age = 25, .created_at = now });5.2. Компилируемый маршрутизатор (router)
Навыки: comptime строковой парсинг, type generation, tagged unions
Требования:
- Маршруты определяются в comptime:
const router = comptime Router.init(.{
.{ .GET, "/", handleIndex },
.{ .GET, "/users/:id", handleUser },
.{ .POST, "/users", createUser },
.{ .GET, "/users/:id/posts/:pid", handlePost },
.{ .GET, "/files/*path", serveFile },
});
- Comptime-генерация:
- Trie или radix tree строится в compile-time
- Для каждого маршрута — struct с типизированными параметрами
- Параметры :id автоматически парсятся в нужный тип
- *path — wildcard (остаток пути)
- В runtime: только lookup по дереву, без аллокаций
- Обработчик получает типизированные параметры:
fn handleUser(params: struct { id: u64 }, req: *Request) !Response { ... }
fn handlePost(params: struct { id: u64, pid: u64 }, req: *Request) !Response { ... }5.3. Сериализатор/десериализатор (MessagePack или CBOR)
Навыки: comptime type reflection, бинарные форматы, generics, рекурсивные типы
Требования:
- Автоматическая сериализация любой Zig struct в MessagePack (или CBOR)
- Автоматическая десериализация обратно в struct
- Поддержка типов: int, float, bool, []const u8, optional, slice, struct, enum, tagged union
- Вложенные структуры (рекурсивно)
- Comptime-проверка: если тип не поддерживается — ошибка компиляции с понятным сообщением
- Streaming API: сериализация в writer, десериализация из reader (без промежуточных аллокаций)
API:
const msgpack = @import("msgpack");
const Point = struct { x: f32, y: f32 };
const Line = struct { start: Point, end: Point, color: ?[]const u8 };
var buf: [1024]u8 = undefined;
const encoded = try msgpack.encode(&buf, Line{
.start = .{ .x = 0, .y = 0 },
.end = .{ .x = 10, .y = 20 },
.color = "red",
});
const decoded = try msgpack.decode(Line, encoded);Уровень 6. Интеграция и production-ready проекты
6.1. SQLite wrapper с comptime маппингом
Навыки: C interop, build.zig, error mapping, comptime, resource management
Требования:
- Линковка SQLite через build.zig (addCSourceFiles или linkSystemLibrary)
- Обёртка над C API: open, close, prepare, step, finalize
- Zig-идиоматичные ошибки (sqlite3 error codes → Zig error set)
- Comptime mapping: результат запроса → Zig struct
- Prepared statements с bind-параметрами
- Транзакции: begin/commit/rollback с defer-паттерном
- Пул соединений (опционально)
API:
const db = try Sqlite.open(allocator, "app.db");
defer db.close();
try db.exec("CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY, name TEXT, age INTEGER)");
// Компилятор проверяет: количество ? = количество аргументов
try db.exec("INSERT INTO users (name, age) VALUES (?, ?)", .{ "Alice", 30 });
// Результат автоматически маппится в struct
const User = struct { id: i64, name: []const u8, age: i32 };
var rows = try db.query(User, "SELECT id, name, age FROM users WHERE age > ?", .{18});
defer rows.deinit();
while (rows.next()) |user| {
std.debug.print("{s} ({})\n", .{ user.name, user.age });
}6.2. CLI framework
Навыки: comptime, build system, модульная архитектура, man pages, completion
Требования:
- Определение команд и флагов через comptime struct:
const cli = Cli.init(.{
.name = "mytool",
.version = "1.0.0",
.description = "My awesome tool",
.commands = &.{
.{ .name = "init", .handler = cmdInit, .args = struct {
path: []const u8 = ".",
@"--template": []const u8 = "default",
@"--force": bool = false,
}},
.{ .name = "build", .handler = cmdBuild, .args = struct {
@"--release": bool = false,
@"--target": ?[]const u8 = null,
@"--jobs": u32 = 4,
}},
},
});
- Автогенерация:
- --help для каждой команды (из struct field names и defaults)
- bash/zsh/fish completion скрипт
- Валидация типов аргументов (число, путь, enum)
- Обработчик получает типизированную struct:
fn cmdInit(args: struct { path: []const u8, ... }) !void { ... }
- Цветной вывод ошибок (ANSI)
- Подкоманды: mytool remote add <name> <url>6.3. Сборщик статического сайта
Навыки: файловый I/O, рекурсивный обход, шаблонизация, Markdown→HTML, build.zig интеграция
Требования:
- Читает .md файлы из content/
- Парсит Markdown → HTML (headings, paragraphs, code blocks, links, images, lists)
- Front matter (YAML-like метаданные в начале файла):
---
title: My Post
date: 2025-03-15
tags: zig, programming
---
- Шаблоны: layouts/ с placeholder-ами {{ content }}, {{ title }}, {{ date }}
- Генерирует output/ со статическими HTML-файлами
- Копирует static/ (CSS, JS, изображения) без изменений
- Индексная страница со списком постов (сортировка по дате)
- RSS/Atom feed генерация
- Инкрементальная сборка: пересобирать только изменённые файлы (по mtime)
- Встроенный dev-сервер: localhost:3000 с live-reload (через WebSocket или polling)
CLI:
$ ./zssg build # полная сборка
$ ./zssg serve # dev-сервер с live-reload
$ ./zssg new "Post Title" # создать новый .md с front matterКарта прогресса
Уровень 1 — Основы (1-2 недели на задачу)
├── 1.1 wc clone → файлы, ошибки, CLI args, форматирование
├── 1.2 CSV parser → строки, аллокаторы, итераторы, тесты
├── 1.3 Ring buffer → generics, comptime, указатели
└── 1.4 Password gen → std.crypto.random, CLI, строки
Уровень 2 — Структуры данных (1-2 недели)
├── 2.1 HashMap → хеширование, resize, generics, бенчмарки
├── 2.2 Arena allocator → Allocator interface, страницы, alignment
└── 2.3 Thread pool → Mutex, Condition, очереди, потоки
Уровень 3 — Сеть (2-3 недели)
├── 3.1 HTTP сервер → TCP, парсинг протокола, роутинг, потоки
├── 3.2 TCP чат → многопользовательский I/O, протокол сообщений
└── 3.3 DNS resolver → бинарный протокол, UDP, bit-level парсинг
Уровень 4 — Системное (2-3 недели)
├── 4.1 KV store → WAL, persistence, crash recovery, fsync
├── 4.2 grep clone → mmap, параллельный поиск, рекурсия FS
└── 4.3 Logger → потокобезопасность, ротация, structured logging
Уровень 5 — Comptime (2-3 недели)
├── 5.1 Query builder → @typeInfo, comptime codegen, SQL
├── 5.2 Router → comptime trie, type generation, tagged unions
└── 5.3 Serializer → reflection, бинарный формат, streaming
Уровень 6 — Production (3-4 недели)
├── 6.1 SQLite wrapper → C interop, build.zig, error mapping
├── 6.2 CLI framework → comptime, модульность, автогенерация
└── 6.3 Static site gen → всё вместе: I/O, парсинг, шаблоны, сеть
Итого: ~3-4 месяца интенсивной работы → уверенный Middle Zig разработчикЧеклист навыков Middle Zig
После выполнения всех задач ты должен уметь:
[Язык]
✓ Свободно работать с error unions, optional, tagged unions
✓ Понимать ownership: кто владеет памятью, defer vs errdefer
✓ Писать generic-код через comptime
✓ Использовать @typeInfo, @field, inline for для метапрограммирования
✓ Понимать разницу comptime vs runtime
[Память]
✓ Выбирать правильный аллокатор для задачи
✓ Реализовывать собственный аллокатор (Allocator interface)
✓ Избегать утечек, dangling pointers, use-after-free
✓ Использовать arena для request-scoped работы
✓ Профилировать потребление памяти
[I/O и сеть]
✓ Работать с файлами, директориями, mmap
✓ Реализовывать TCP клиент/сервер
✓ Парсить бинарные и текстовые протоколы
✓ Буферизованный I/O (0.15 Writergate)
[Конкурентность]
✓ Создавать и управлять потоками
✓ Синхронизация: Mutex, Condition, атомарные операции
✓ Реализовывать thread pool
✓ Избегать data races
[Tooling]
✓ Писать build.zig для сложных проектов
✓ Подключать C-библиотеки (linkLibC, addCSourceFiles)
✓ Управлять зависимостями (build.zig.zon)
✓ Кросс-компиляция
✓ Писать тесты с std.testing
[Паттерны]
✓ RAII через defer/errdefer
✓ Builder pattern через struct defaults
✓ Comptime interfaces (duck typing)
✓ Iterator pattern
✓ Allocator injection