Zig вместо C: пишем высокопроизводительный CLI-инструмент и заменяем 3000 строк C-кода
Всё началось на второй паре по системному программированию. Нам дали задачу: написать CLI-утилиту для анализа логов - парсить файл, фильтровать записи по уровню ошибок, считать статистику, выводить красиво в консоль. "Ну понятно", - открыл я vim и началось мое долгое приключение...
Неделя. Две. Утилита называлась logz, она умела читать логи nginx и apache, фильтровать по уровню (DEBUG, INFO, WARN, ERROR), по дате, по IP, выводила топ адресов с наибольшим числом ошибок, рисовала простенький bar-chart прямо в терминале через unicode-символы. Только вот я сидел как-то вечером, запустил wc -l main.c - 3147 строк. И смотрел на это число минуты три с таким лицом - O_O.
Сама утилита работала. Но открывая её осознаешь что - это месиво. Одна функция process_file на 400 строк. Сегфолты раз в неделю. Valgrind как лучший друг. И каждый раз когда надо добавить фичу - сначала полчаса вспоминаешь что вообще происходит в коде.
Потом я случайно прочитал пост про Zig на lobste.rs. Заинтересовался и попробовал. Через месяц у меня была та же утилита, но теперь на 1089 строках, которая работала быстрее и не падала.
Тут я понял что вот золотая жила и расскажу о том - зачем Zig, как переписывал, где облажался и что вышло в итоге.
Постановка проблемы: что конкретно бесило в C
Я не скажу что "C - это плохой язык", потому что это чушь. Но конкретно мои боли в этом проекте:
Память вручную. Каждый malloc - это обязательство. Написал функцию, она вернула строку - не забудь освободить. Добавил ранний return по ошибке - не забудь освободить до него. Я честно раза три ловил утечки памяти в logz. Valgrind спасал, но ты не запускаешь valgrind - ты просто пушишь.
Сразу скажу что сегфолты - это когда прога пытается залезть в память к которой ей нельзя.
Сегфолты. Это отдельная история. Утилита читала файлы гигабайтами, и где-то в середине - Segmentation fault (core dumped). Дебажишь час, находишь: буфер на 256 байт, а строка лога оказалась 300. Классика. Починил - появился другой: off-by-one в парсере дат. Я реально начал видеть сегфолты во сне.
Bil-система. Мой Makefile работал, пока я его помнил. Через месяц открываешь - что это вообще такое? Плюс make иногда говорит «up to date» хотя ты только что изменил хедер. Да, знаю про -MMD, но это ещё один уровень вещей которые надо держать в голове.
Объём кода. Элементарный парсер строки вида 2026-01-17 ERROR [nginx] 192.168.1.100 500 на C - это функция строк на 50. Потому что: strtok или ручной парсинг, проверки NULL на каждом шагу, strncpy с ограничением длины, освобождение временных буферов... Это не сложно, но это много кода ради простой вещи.
Обработка ошибок. Функции возвращают -1 или NULL при ошибке, смысл ошибки - в errno или нигде. Пишешь цепочку if (result < 0) { perror("..."); return -1; } и либо ты теряешь контекст, либо таскаешь его через все уровни вручную. 3000 строк такого кода - это уже нечитабельно.
Почему Zig, а не Rust?
Rust читал месяц. Крутой, но borrow checker для утилиты которая читает файл и пишет в stdout - избыточен. Хотел C по духу, но без боли.
Zig зашёл за:
- Нет hidden control flow - defer file.close() прямо в коде, ничего за кулисами
- Ошибки через типы - !T значит "либо значение, либо ошибка", компилятор не даст проигнорировать
- Кросс-компиляция из коробки - zig build -Dtarget=x86_64-windows без тулчейнов
- Маленький проект собирается за секунду
Кратко: C - это контроль ценой боли, Rust - это безопасность ценой сложности, Zig - это C, только с нормальными инструментами.
Архитектура
Pipeline из пяти модулей, данные текут строго вниз:
В C у меня было иначе: одна функция process_file делала всё сразу. Добавить новый формат лога = переписывать её всю
1.Парсинг аргументов
С +- 80 строк было
Zig-версия - стало +- 40 строк
orelse return error.MissingValue - одна строка вместо трёх проверок. Ошибка явная, не -1.
2.Парсинг строки лога
С-версия +-65 строк
Zig-версия +- 35-40 строк
Чем лучше: нет malloc, нет free, нет strtok с его глобальным состоянием. Функция либо возвращает валидный LogEntry, либо ошибку - и это написано прямо в сигнатуре !LogEntry. Вызывающий не может случайно забыть освободить память.
3.Работа с файлами и defer
В C мне надо было писать fclose(file) перед каждым return, а их у меня было четыре разных в этой функции. Каждый раз думать: "я закрыл файл?". С defer эта задача исчезла.
Бенчмарки
Тестовый файл: 5 млн строк, +-800 МБ.
Zig быстрее на треть - просто потому что я сразу написал нормальный bufferedReader, а в C у меня был fgets без буферизации. Строк кода в три раза меньше - это главный результат.
"Грабли"
Allocators непривычны. Поначалу лепил везде page_allocator. Потом открыл ArenaAllocator - для CLI-утилиты идеально: выделяешь сколько хочешь, в конце arena.deinit() и всё сразу освобождается.
Строки - слайсы, не char*. []const u8 не имеет нуля в конце. Передаёшь в C-функцию напрямую - UB. Надо .ptr и убедиться что строка null-terminated.
Ошибки компилятора длинные. Сообщение на 20 строк - читай только первую, остальное трассировка. Уже привыкаешь.
Язык меняется. Zig пока не 1, я на 0.13. Примеры из интернета 2022 - 23 года часто не компилируются - всегда смотри официальную документацию.
Выводы по Zig
Стоит ли переходить на Zig в 2026?
Естественно да, если: пишешь CLI-утилиты, системные инструменты, что-то встраиваемое. Нужен контроль над памятью, но без сегфолтов каждую неделю. Хочешь кросс-компиляцию без боли. Устал от Makefile.
Лучше остаться на C, если: работаешь с большой существующей C-кодовой базой и интеграция не нужна. Или нужна максимальная стабильность языка - пока не 1, API может меняться между версиями.
Лучше взять Rust, если: проект большой и долгоживущий, важна экосистема (cargo + crates.io несравнимо богаче), работаешь в команде где Rust уже знают.
Для меня Zig занял нишу: "хочу писать низкоуровневый код без страданий". Три месяца назад я смотрел на 3000+ строк C и думал - ну это всё. Сейчас у меня 1089 строк Zig, утилита работает быстрее, и я понимаю каждую строку кода.
Если хочешь попробовать - ziglang.org. Начни с zig init, напиши hello world, потом попробуй открыть и построчно прочитать файл. Язык небольшой, стандартная библиотека читается за вечер.
Вот и все, напишите в комментах если было интересно.Буду рад если кому-нибудь это поможет и он будет пользоваться напостоянке.