Вышел Zig 0.16 — DI в main, I/O как интерфейс и async без раскраски функций

Zig 0.16 меняет точку входа: main получает DI-параметр с аллокатором и I/O, а async работает без раскраски функций. Разбираем, что сломается в коде с 0.15 и ради чего.

Обложка: Вышел Zig 0.16 — DI в main, I/O как интерфейс и async без раскраски функций

Zig 0.16 закрыл проблему раскраски функций: sync и async теперь имеют одинаковую сигнатуру, отличается только переданный I/O-интерфейс. Если вы писали асинхронный Rust и упирались в async fn-заражение — посмотрите, как это решено здесь. 14 апреля 2026 года команда Zig выпустила 0.16.0 после 8 месяцев работы 244 контрибьюторов. Главные изменения: I/O как интерфейс, DI в точке входа («Juicy Main»), новый ELF-линкер и окружение, перестающее быть глобальным.

Zig — системный язык с С-подобным синтаксисом и упором на явное управление памятью. 0.16.0 — не LTS, но в терминах API это один из самых ломающих релизов: многие библиотеки придётся переписывать под новый std.Io. Разбираем, что именно изменилось и ради чего.

Ключевые выводы

Что: Zig 0.16.0 — крупный релиз за 8 месяцев. I/O как интерфейс, DI в main, новый ELF-линкер.

Когда: 14 апреля 2026 года.

Где: ziglang.org/download, тарболы под все Tier-1 и Tier-2 платформы.

Масштаб: 244 контрибьютора, 1183 коммита, ломающие изменения в стандартной библиотеке.

Ключевое для кода: pub fn main(init: std.process.Init) — точка входа получает аллокаторы, Io, env и preopens.

Juicy Main — DI на уровне точки входа

«Juicy Main» — это внутреннее название proposal Эндрю Келли: идея в том, что точка входа должна получать «сочный» набор готовых зависимостей, а не создавать их внутри. DI здесь значит, что аллокатор, I/O, env и preopens передаются в main снаружи, а не конструируются каждым приложением заново. Раньше каждая программа на Zig начиналась с одного и того же бойлерплейта: создать GeneralPurposeAllocator, получить arg-итератор через std.process.argsAlloc, отдельно дёрнуть std.process.getEnvMap. В 0.16 всё это подаётся параметром в main:

			const std = @import("std");

pub fn main(init: std.process.Init) !void {
    const gpa = init.gpa;
    const io = init.io;
    const ptr = try gpa.create(i32);
    defer gpa.destroy(ptr);

    try std.Io.File.stdout().writeStreamingAll(io, "Hello, world!\n");

    const args = try init.minimal.args.toSlice(init.arena.allocator());
    for (args, 0..) |arg, i| {
        std.log.info("arg[{d}] = {s}", .{ i, arg });
    }
}
		

std.process.Init — структура с пре-инициализированными значениями: процессная arena с автоматической очисткой при выходе, дефолтный GPA (General Purpose Allocator) с leak-checker в debug, подходящая под таргет реализация Io, карта переменных окружения, preopens (наследие WASI-модели: файловые дескрипторы, переданные родительским процессом как capability). Это классический DI: main собирает зависимости, библиотеки внутри принимают Allocator и Io через параметры.

Подпись main теперь может быть одной из трёх:

  • Без параметров — нельзя получить CLI-аргументы и окружение. Подходит для чистых вычислительных утилит.
  • process.Init.Minimal — только сырые argv и environ, без аллокаторов и Io.
  • process.Init — полный набор: аллокаторы, Io, env-map, preopens.

I/O как интерфейс — async без раскраски функций

Второе крупное изменение: std.Io стал абстрактным интерфейсом, а не набором конкретных функций. Реализации:

  • Io.Threaded — текущая дефолтная, работает поверх ОС-потоков, поддерживает cancelation.
  • Io.Evented — экспериментальная, на userspace-стеках с work-stealing (M:N, «зелёные потоки»).
  • Io.Uring — proof-of-concept на Linux io_uring, без сети и обработки ошибок.
  • Io.Kqueue — proof-of-concept на macOS/BSD.
  • Io.Dispatch — поверх Grand Central Dispatch (macOS).
  • Io.failing — возвращает ошибку на любую операцию, для тестов.

Философия — ровно та же, что у Allocator: функции принимают интерфейс, а не привязываются к конкретной реализации. Это даёт Zig async без раскраски функций — то, на чём подрывались Rust, JavaScript и C#. Асинхронная функция ничем не отличается по сигнатуре от синхронной — меняется только переданный Io.

На уровне API появились новые абстракции:

  • Future(T) — таск, создаётся через io.async, ожидается через future.await. Может быть реализован синхронно, если Io не умеет concurrency.
  • Group — набор независимых тасков с общим awaiting и cancelation.
  • Queue(T) — многопроизводительная/многопотребительная очередь с блокировкой.
  • Select и Batch — выполнение нескольких операций одновременно с ожиданием первого завершения.
  • Clock, Duration, Timestamp, Timeout — type-safe единицы времени.

Есть цена: весь код стандартной библиотеки, который делал I/O, пришлось переписать под интерфейс. Старые типы GenericReader, AnyReader, FixedBufferStream удалены — в крейтах, обновлявшихся из 0.15, почти гарантированно сломается чтение/запись.

Environment перестаёт быть глобальным

В C setenv в многопоточной программе — UB: глобальная environ читается без блокировок. Zig до 0.16 наследовал эту проблему через std.os.environ, который ещё и нельзя было заполнить без линковки libc.

Теперь окружение доступно только из main через init.environ_map. Если библиотеке нужен env — она принимает его параметром, как аллокатор. Это ломает код, дёргавший std.process.getEnvVarOwned из произвольного места, но убирает целый класс thread-safety-багов.

Параллельно переименованы функции в std.mem: indexOffind, добавлены cut / cutScalar для разбиения слайсов по первому/последнему вхождению.

Новый ELF-линкер — без LLVM для Linux

В 0.16 появился собственный ELF-линкер — включается флагом -fnew-linker, а при -fincremental на self-hosted ELF-сборке используется автоматически. Он пока не feature-complete (например, не пишет DWARF), поэтому по умолчанию release-сборки идут через LLVM + LLD. Выгода — инкрементальная линковка (194мс → 65мс на тестовом проекте) и меньше зависимостей для debug-сборок.

Плюс доработки под Windows: сетевой стек теперь работает без ws2_32.dll (напрямую через NtDll), завершена миграция с Win32 API на NtDll для остальных системных вызовов, появился inter-process progress reporting для параллельных сборок.

Что ещё важного в 0.16

  • x86-бэкенд компилятора стал самодостаточным — дебажные сборки на x86_64 собираются без LLVM и идут значительно быстрее. aarch64-бэкенд ещё work-in-progress: в 0.16 он падает на behavior-тестах.
  • Инкрементальная компиляция переработана, меньше false-rebuilds, стабильнее на больших проектах.
  • Fuzzer (zig test --fuzz) получил multi-process режим, infinite mode и crash dumps с AST-дампом.
  • Build-система: локальный override пакетов, --error-style и --multiline-errors, таймауты юнит-тестов, temporary files API.
  • Крипто: добавлены AES-SIV, AES-GCM-SIV, Ascon-AEAD, Ascon-Hash, Ascon-CHash.
  • Heap: ArenaAllocator стал thread-safe и lock-free, обёртка ThreadSafeAllocator удалена.
  • Тулчейн: LLVM 21 (с отключённой loop vectorization из-за регрессии), musl 1.2.5, glibc 2.43, Linux 6.19 headers, macOS 26.4 headers, MinGW-w64, FreeBSD 15.0 libc.

Как мигрировать с 0.15

  1. Ставьте 0.16 из тарбола или zigup параллельно со старой версией. Одновременная установка нескольких версий — штатный сценарий.
  2. Соберите проект и смотрите на ошибки. Чаще всего сломается: вызовы std.os.environ, std.process.getEnvVarOwned, использование GenericReader/AnyReader, FixedBufferStream.
  3. Перепишите main на новую сигнатуру. Даже если просто хотите старое поведение — объявите pub fn main(init: std.process.Init.Minimal), так вы получите хотя бы args/environ.
  4. Если нужен Io в библиотеке, но вы не контролируете main — временно используйте Io.Threaded в режиме single-threaded и берите threaded.io(). Это не рекомендуемый путь, но он работает как page_allocator для аллокатора.
  5. Проверьте зависимости через zig fetch. Старые версии пакетов под 0.15 скорее всего не соберутся, ждите апдейтов или фиксируйте у себя fork.
  6. Пересоберите и прогоните тесты на std.testing.io — это Io-бэкенд для тестов, аналог std.testing.allocator.
Часто задаваемые вопросы
1
Готов ли Zig для production?

Zig ещё до 1.0, LTS не объявлен. При этом на Zig написан Bun (JavaScript-рантайм, сотни тысяч пользователей) и TigerBeetle (финансовая БД). Для системного кода с высокими требованиями к производительности и предсказуемости — Zig используется в продакшене, но готовьтесь к ломающим изменениям API в каждом мажоре.

2
Сломается ли мой код с 0.15?

С высокой вероятностью — да. Удалены старые Reader/Writer-типы, изменена сигнатура main, убран глобальный environ. Типичная миграция — несколько часов на средний проект.

3
Что с производительностью новых бэкендов?

Дебажные сборки на x86_64 без LLVM идут значительно быстрее. Качество машкода хуже, чем у LLVM, поэтому для release-сборок LLVM остаётся дефолтом. aarch64-бэкенд self-hosted ещё не готов — используйте LLVM.

4
Когда Io.Uring станет дефолтом на Linux?

Команда пишет «не в этом релизе». На 0.16 это proof-of-concept без сети и полного покрытия ошибок. Работа идёт, но сроков нет.

5
Где взять примеры нового стиля?

В самих release notes и в исходниках stdlib. Большинство модулей stdlib переписаны под новый Io — это живой учебник.

Что дальше

После 0.16 любая Zig-функция, принимающая Io, автоматически работает и в sync-, и в async-режиме без смены сигнатуры. В Rust это требует async fn + executor-specific runtime — в Zig теперь параметр.

0.16.0 — это не про новые фичи, а про фундамент: зафиксированы I/O-интерфейс и контракт точки входа. Если у вас есть Zig-код на 0.15 — закладывайте день на миграцию по чек-листу выше. Если смотрели на Zig со стороны и интересует async без раскраски — ставьте 0.16 и загляните в исходники stdlib: это самая чистая реализация «нераскрашенного» async среди мейнстримных языков на 2026 год.

Полные release notes — на ziglang.org. Обсуждение и отчёты о багах — в GitHub.