# Примечания к выпуску 0.16.0

В 
Опубликовано 2026-04-15

Zig — это язык программирования общего назначения и набор инструментов для поддержки надёжного, оптимального и повторно используемого программного обеспечения.

Разработка Zig финансируется через Zig Software Foundation — некоммерческую организацию 501(c)(3). Пожалуйста, рассмотрите возможность регулярного пожертвования, чтобы мы могли выделять больше оплачиваемых часов основным участникам команды. Это самый прямой способ ускорить развитие проекта по Дорожной карте к версии 1.0. Если вам нужны подтверждения пожертвований или вы хотите перейти с GitHub Sponsors, рекомендуем жертвовать через Every.org.

В этом релизе представлены результаты 8 месяцев работы: изменения от 244 различных участников в 1183 коммитах.

Пожалуй, самое заметное нововведение — Интерфейс ввода-вывода, но не стоит упускать из виду и изменения в языке, а также улучшения компилятора, системы сборки, линкера, фаззера и инструментария.

Zig поддерживает широкий спектр архитектур и операционных систем. В разделах Таблица поддержки и Дополнительные платформы перечислены цели, для которых Zig может собирать программы, а в README zig-bootstrap — цели, на которые легко кросскомпилируется сам компилятор Zig.

# Основные изменения

  • aarch64-freebsd, aarch64-netbsd, loongarch64-linux, powerpc64le-linux, s390x-linux, x86_64-freebsd, x86_64-netbsd и x86_64-openbsd теперь тестируются нативно в CI Zig, что гарантирует высокое качество поддержки в будущем. Спасибо OSUOSL за предоставление оборудования AArch64 и Power ISA, а также IBM за оборудование z/Architecture.
  • Добавлена поддержка кросскомпиляции для aarch64-maccatalyst и x86_64-maccatalyst. Это стало возможным «бесплатно», так как поставляемый с Zig файл libSystem.tbd уже содержит необходимые символы для этих целей.
  • Добавлена начальная поддержка loongarch32-linux. Обратите внимание, что libc пока не поддерживается для этой цели, а LLVM всё ещё считает ABI нестабильным, но можно собирать программы, использующие только системные вызовы через std.os.linux.
  • Добавлена базовая поддержка архитектур Alpha, KVX, MicroBlaze, OpenRISC, PA-RISC и SuperH. На данный момент для этих целей требуется использовать C-бэкенд Zig с GCC или внешний форк LLVM/Clang.
  • Удалена поддержка Oracle Solaris, IBM AIX и z/OS. В целом, проект Zig не может поддерживать проприетарные операционные системы, которые делают чрезмерно сложным получение системных заголовков и аудит вклада. Обратите внимание, что это не затрагивает illumos — открытый форк OpenSolaris, который по-прежнему поддерживается.
  • Поддержка трассировки стека значительно улучшена по всем направлениям; теперь почти все основные цели предоставляют трассировку стека при сбоях.
  • Исправлены различные ошибки в стандартной библиотеке, в основном затрагивавшие архитектуры со слабым упорядочиванием памяти и цели с необычными размерами страниц. Это заметно повысило надёжность на AArch64 (особенно без LSE), LoongArch и Power ISA.
  • Исправлены различные ошибки в стандартной библиотеке и компиляторе, мешавшие работе Zig на big-endian хостах.
  • Исправлена генерация объектных файлов для big-endian ARM целей: теперь для ARMv6+ используется формат BE8 вместо устаревшего BE32.

# Система уровней поддержки

Уровень поддержки Zig для различных целей делится на четыре уровня (Tier), где Tier 1 — самый высокий. Цель — чтобы для Tier 1 целей не было отключённых тестов; после релиза 1.0.0 это станет обязательным требованием.

# Tier 1

  • Все неэкспериментальные языковые возможности гарантированно работают корректно.
  • Компилятор может генерировать машинный код для этих целей без использования LLVM.

# Tier 2

  • Кроссплатформенные абстракции стандартной библиотеки учитывают эти цели.
  • Для этих целей доступны возможности отладки (debug info), поэтому при сбоях и ошибках выдаётся трассировка стека.
  • При кросскомпиляции для этих целей доступна libc.
  • Машины непрерывной интеграции запускают модульные тесты для этих целей при каждом коммите.

# Tier 3

  • Компилятор может генерировать машинный код для этих целей через LLVM.
  • Линкер может создавать объектные файлы, библиотеки и исполняемые файлы для этих целей.
  • Эти цели не считаются экспериментальными в LLVM.

# Tier 4

  • Компилятор может генерировать исходный код на ассемблере для этих целей через LLVM.

# Таблица поддержки

В следующей таблице означает полную поддержку, — отсутствие поддержки, ⚠️ — частичную поддержку (например, только для некоторых подцелей или с известными проблемами). означает, что статус в основном неизвестен, обычно потому что цель редко тестируется. Наведите курсор на другие значки для получения подробностей.

Цель Уровень Языковые возможности Стандартная библиотека Генерация кода Компоновщик Debug Info libc CI
x86_64-linux 1 🖥️
* * *
aarch64-freebsd 2 🖥️🛠️
aarch64(_be)-linux 2 🖥️🛠️
aarch64-maccatalyst 2 🖥️🛠️ ⚠️
aarch64-macos 2 🖥️🛠️
aarch64(_be)-netbsd 2 🖥️🛠️
aarch64-openbsd 2 🖥️🛠️ ⚠️
aarch64-windows 2 🖥️🛠️ ⚠️
arm-freebsd 2 🖥️ ⚠️
arm(eb)-linux 2 🖥️
arm(eb)-netbsd 2 🖥️ ⚠️
arm-openbsd 2 🖥️ ⚠️
hexagon-linux 2 🖥️
loongarch64-linux 2 🖥️🛠️
mips(el)-linux 2 🖥️
mips(el)-netbsd 2 🖥️ ⚠️
mips64(el)-linux 2 🖥️
mips64(el)-openbsd 2 🖥️ ⚠️
powerpc-linux 2 🖥️ ⚠️
powerpc-netbsd 2 🖥️ ⚠️
powerpc-openbsd 2 🖥️ ⚠️
powerpc64(le)-freebsd 2 🖥️ ⚠️
powerpc64(le)-linux 2 🖥️ ⚠️ ⚠️
powerpc64-openbsd 2 🖥️ ⚠️
riscv32-linux 2 🖥️
riscv64-freebsd 2 🖥️🛠️ ⚠️
riscv64-linux 2 🖥️🛠️
riscv64-openbsd 2 🖥️🛠️ ⚠️
s390x-linux 2 🖥️
thumb(eb)-linux 2 🖥️
thumb-windows 2 🖥️ ⚠️
wasm32-wasi 2 🖥️🛠️ ⚠️
x86-linux 2 🖥️
x86-netbsd 2 🖥️ ⚠️
x86-openbsd 2 🖥️ ⚠️
x86-windows 2 🖥️ ⚠️
x86_64-freebsd 2 🖥️🛠️
x86_64-maccatalyst 2 🖥️ ⚠️
x86_64-macos 2 🖥️ ⚠️
x86_64-netbsd 2 🖥️🛠️
x86_64-openbsd 2 🖥️🛠️
x86_64-windows 2 🖥️🛠️
* * *
aarch64-haiku 3 ⚠️ 🖥️🛠️
aarch64-ios 3 🖥️🛠️
aarch64-serenity 3 ⚠️ 🖥️🛠️
aarch64-tvos 3 🖥️🛠️
aarch64-visionos 3 🖥️🛠️
aarch64-watchos 3 🖥️🛠️
arm-haiku 3 ⚠️ 🖥️
loongarch32-linux 3 ⚠️ 🖥️
mips64(el)-netbsd 3 🖥️
riscv64-haiku 3 ⚠️ 🖥️🛠️
riscv64-serenity 3 ⚠️ 🖥️🛠️
wasm64-wasi 3 🖥️🛠️ ⚠️
x86-haiku 3 ⚠️ 🖥️
x86-illumos 3 ⚠️ 🖥️
x86_64-dragonfly 3 🖥️🛠️
x86_64-haiku 3 ⚠️ 🖥️
x86_64-illumos 3 ⚠️ 🖥️🛠️
x86_64-serenity 3 ⚠️ 🖥️
* * *
alpha-linux 4 ⚠️ 📄
alpha-netbsd 4 📄
alpha-openbsd 4 📄
arc(eb)-linux 4 ⚠️ 📄
csky-linux 4 ⚠️ 📄
hppa-linux 4 ⚠️ 📄
hppa-netbsd 4 📄
hppa-openbsd 4 📄
hppa64-linux 4 📄
m68k-haiku 4 ⚠️ 🖥️
m68k-linux 4 🖥️
m68k-netbsd 4 🖥️
m88k-openbsd 4 📄
microblaze(el)-linux 4 ⚠️ 📄
or1k-linux 4 📄
sh(eb)-linux 4 ⚠️ 📄
sh(eb)-netbsd 4 📄
sh-openbsd 4 📄
sparc-linux 4 ⚠️ 🖥️
sparc-netbsd 4 🖥️
sparc64-haiku 4 ⚠️ 🖥️🛠️ ⚠️
sparc64-linux 4 🖥️🛠️ ⚠️
sparc64-netbsd 4 🖥️🛠️ ⚠️
sparc64-openbsd 4 🖥️🛠️ ⚠️
xtensa(eb)-linux 4 📄

# Требования к версиям ОС

В стандартной библиотеке Zig есть минимальные требования к версиям некоторых поддерживаемых операционных систем, что влияет и на сам компилятор.

Операционная система Минимальная версия
DragonFly BSD 6.0
FreeBSD 14.0
Linux 5.10
NetBSD 10.1
OpenBSD 7.8
macOS 13.0
Windows 10

# Дополнительные платформы

Zig также поддерживает различные дополнительные цели, для которых не применяется система уровней:

  • aarch64-driverkit
  • aarch64(_be)-freestanding
  • aarch64-uefi
  • alpha-freestanding
  • amdgcn-amdhsa
  • amdgcn-amdpal
  • amdgcn-mesa3d
  • arc(eb)-freestanding
  • arm(eb)-freestanding
  • arm-3ds
  • arm-uefi
  • arm-vita
  • avr-freestanding
  • bpf(eb,el)-freestanding
  • csky-freestanding
  • hexagon-freestanding
  • hppa(64)-freestanding
  • kalimba-freestanding
  • kvx-freestanding
  • lanai-freestanding
  • loongarch(32,64)-freestanding
  • loongarch(32,64)-uefi
  • m68k-freestanding
  • microblaze(el)-freestanding
  • mips(64)(el)-freestanding
  • mipsel-psp
  • msp430-freestanding
  • nvptx(64)-cuda
  • nvptx(64)-nvcl
  • or1k-freestanding
  • powerpc(64)(le)-freestanding
  • powerpc64-ps3
  • propeller-freestanding
  • riscv(32,64)(be)-freestanding
  • riscv(32,64)-uefi
  • s390x-freestanding
  • sh(eb)-freestanding
  • sparc(64)-freestanding
  • spirv(32,64)-opencl
  • spirv(32,64)-opengl
  • spirv(32,64)-vulkan
  • thumb(eb)-freestanding
  • ve-freestanding
  • wasm(32,64)-emscripten
  • wasm(32,64)-freestanding
  • x86(_16,_64)-freestanding
  • x86(_64)-uefi
  • x86_64-driverkit
  • x86_64-ps4
  • x86_64-ps5
  • xcore-freestanding
  • xtensa(eb)-freestanding

# Изменения в языке

# switch

Теперь в качестве вариантов switch можно использовать packed struct и packed union. Они сравниваются исключительно по своему базовому целочисленному значению, как и при сравнении на равенство:

const U = packed union(u2) {
    a: i2,
    b: u2,
};

const u: U = .{ .a = -1 };
switch (u) {
    .{ .b = 3 } => {},
    else => unreachable,
}

Другие реализованные возможности:

  • литералы объявлений (decl literals) и всё, что требует результирующего типа (например, @enumFromInt), теперь можно использовать как варианты switch;
  • захват тегов объединений теперь разрешён для всех вариантов, а не только для inline;
  • варианты switch могут содержать ошибки, не входящие в обрабатываемый набор ошибок, если эти варианты содержат => comptime unreachable;
  • захват переменных в вариантах switch больше нельзя полностью игнорировать.

Исправления ошибок:

  • множество проблем с switch по типам с одним возможным значением теперь исправлено;
  • правила для недостижимых (unreachable) вариантов else при switch по ошибкам теперь применяются к любому switch по ошибке, а не только к switch_block_err_union, и применяются корректно на основе AST;
  • switch по void больше не требует обязательного варианта else;
  • ленивые значения теперь корректно вычисляются до любых сравнений с вариантами;
  • порядок вычислений для всех видов операторов switch теперь одинаков, с меткой или без.

# Сравнения на равенство для упакованных объединений

Раньше это было возможно только при оборачивании packed union в packed struct. Теперь это возможно и без этого.

# Перевод @cImport в систему сборки

В будущем перевод C будет выполняться через систему сборки, а не через встроенную функцию языка @cImport, которая теперь считается устаревшей.

Руководство по переходу:

c.zig
pub const c = @cImport({
    @cInclude("stdio.h");
    @cInclude("math.h");
    @cInclude("time.h");
    @cInclude("stdlib.h");
    @cInclude("epoxy/gl.h");
    @cInclude("GLFW/glfw3.h");
});
const c = @import("c.zig").c;

⬇️

c.h
#include <stdio.h>
#include <math.h>
#include <time.h>
#include <stdlib.h>
#include <epoxy/gl.h>
#include <GLFW/glfw3.h>
build.zig
const translate_c = b.addTranslateC(.{
    .root_source_file = b.path("src/c.h"),
    .target = target,
    .optimize = optimize,
});
translate_c.linkSystemLibrary("glfw", .{});
translate_c.linkSystemLibrary("epoxy", .{});

const exe = b.addExecutable(.{
    .name = "tetris",
    .root_module = b.createModule(.{
        .root_source_file = b.path("src/main.zig"),
        .optimize = optimize,
        .target = target,
        .imports = &.{
            .{
                .name = "c",
                .module = translate_c.createModule(),
            },
        },
    }),
});
const c = @import("c");

Таким образом, переведённый C-код будет идентичен тому, что был при использовании @cImport.

Альтернативно, можно добавить официальный пакет translate-c как явную зависимость и получить доступ к большему количеству опций настройки перевода.

# Замена @Type на отдельные встроенные функции для создания типов

В Zig 0.16.0 реализовано давно обсуждаемое предложение #10710: встроенная функция @Type удалена из языка и заменена отдельными встроенными функциями, такими как @Int и @Struct. Хотя @Type был простым аналогом @typeInfo, на практике им было неудобно пользоваться для типовых задач, из-за чего пользователи прибегали к вспомогательным средствам вроде std.meta.Int. За исключением @Vector, который уже существовал, @Type заменён на 8 новых встроенных функций:

@EnumLiteral() type

@Int(comptime signedness: std.builtin.Signedness, comptime bits: u16) type

@Tuple(comptime field_types: []const type) type

@Pointer(
    comptime size: std.builtin.Type.Pointer.Size,
    comptime attrs: std.builtin.Type.Pointer.Attributes,
    comptime Element: type,
    comptime sentinel: ?Element,
) type

@Fn(
    comptime param_types: []const type,
    comptime param_attrs: *const [param_types.len]std.builtin.Type.Fn.Param.Attributes,
    comptime ReturnType: type,
    comptime attrs: std.builtin.Type.Fn.Attributes,
) type

@Struct(
    comptime layout: std.builtin.Type.ContainerLayout,
    comptime BackingInt: ?type,
    comptime field_names: []const []const u8,
    comptime field_types: *const [field_names.len]type,
    comptime field_attrs: *const [field_names.len]std.builtin.Type.StructField.Attributes,
) type

@Union(
    comptime layout: std.builtin.Type.ContainerLayout,
    /// Либо целочисленный тип тега, либо целочисленный базовый тип, в зависимости от `layout`.
    comptime ArgType: ?type,
    comptime field_names: []const []const u8,
    comptime field_types: *const [field_names.len]type,
    comptime field_attrs: *const [field_names.len]std.builtin.Type.UnionField.Attributes,
) type

@Enum(
    comptime TagInt: type,
    comptime mode: std.builtin.Type.Enum.Mode,
    comptime field_names: []const []const u8,
    comptime field_values: *const [field_names.len]TagInt,
) type

# Enum Literal

@EnumLiteral() возвращает «тип литерала перечисления», то есть тип некорректируемых литералов перечисления вроде .foo. Хотя он эквивалентен @TypeOf(.something), новый вариант с @EnumLiteral() предпочтительнее для согласованности.

@Type(.enum_literal)

⬇️

@EnumLiteral()

# Integer

@Int — пожалуй, самая полезная новая встроенная функция для простого метапрограммирования. Её использование эквивалентно ныне устаревшему помощнику std.meta.Int: по заданной знаковости и количеству бит она возвращает целочисленный тип с этими свойствами. Новый вариант делает код значительно более лаконичным и читаемым.

@Type(.{ .int = .{ .signedness = .unsigned, .bits = 10 } })

⬇️

@Int(.unsigned, 10)

# Tuple

@Tuple эквивалентен ныне устаревшему помощнику std.meta.Tuple. Он принимает срез типов и возвращает тип кортежа, поля которого имеют эти типы.

@Type(.{ .@"struct" = .{
    .layout = .auto,
    .fields = &.{.{
        .name = "0",
        .type = u32,
        .default_value_ptr = null,
        .is_comptime = false,
        .alignment = @alignOf(u32),
    }, .{
        .name = "1",
        .type = [2]f64,
        .default_value_ptr = null,
        .is_comptime = false,
        .alignment = @alignOf([2]f64),
    }},
    .decls = &.{},
    .is_tuple = true,
} })

⬇️

@Tuple(&.{ u32, [2]f64 })

Для упрощения языка больше невозможно материализовать типы кортежей с полями comptime.

# Pointer

@Pointer возвращает тип указателя, эквивалентный @Type(.{ .pointer = ... }). Примечательно, что он использует новый тип std.builtin.Type.Pointer.Attributes, где значения полей по умолчанию делают использование более лаконичным и более близким к синтаксису литералов указателей.

@Type(.{ .pointer = .{
    .size = .one,
    .is_const = true,
    .is_volatile = false,
    .alignment = @alignOf(u32),
    .address_space = .generic,
    .child = u32,
    .is_allowzero = false,
    .sentinel_ptr = null,
} })

⬇️

@Pointer(.one, .{ .@"const" = true }, u32, null)
@Type(.{ .pointer = .{
    .size = .many,
    .is_const = false,
    .is_volatile = false,
    .alignment = 1,
    .address_space = .generic,
    .child = u64,
    .is_allowzero = false,
    .sentinel_ptr = &@as(u64, 0),
} })

⬇️

@Pointer(.many, .{ ."align" = 1 }, u64, 0)

# Функция

@Fn возвращает тип функции, эквивалентный @Type(.{ .@"fn" = ... }). Как и для указателей, были введены новые вспомогательные типы, чтобы сделать использование этой встроенной функции проще. Параметры указываются двумя отдельными аргументами: первый задаёт все типы параметров, второй — «атрибуты» (в настоящее время это только флаг noalias).

@Type(.{ .@"fn" = .{
    .calling_convention = .c,
    .is_generic = false,
    .is_var_args = true,
    .return_type = u32,
    .params = &.{.{
        .is_generic = false,
        .is_noalias = false,
        .type = f64,
    }, .{
        .is_generic = false,
        .is_noalias = true,
        .type = *const anyopaque,
    }},
} })

⬇️

@Fn(
    &.{ f64, *const anyopaque },
    &.{ .{}, .{ ."noalias" = true } },
    u32,
    .{ ."callconv" = .c, .varargs = true },
)

Это одна из нескольких новых встроенных функций, принимающих аргументы в стиле «структура массивов» (struct of arrays). Преимущество такого стиля — простота задания фиксированного значения для всех элементов. Например, чтобы использовать «по умолчанию» атрибуты .{} для всех параметров, используйте &@splat(.{}):

@Fn(param_types, &@splat(.{}), ReturnType, .{ ."callconv" = .c })

# Структура

@Struct возвращает тип struct, эквивалентный @Type(.{ .@"struct" = ... }). Как и @Fn, она использует стратегию «структура массивов» для передачи информации о полях. Поля передаются тремя отдельными массивами — имена полей, типы полей и атрибуты полей, где последние включают выравнивание, флаг comptime и значение по умолчанию (если есть).

@Type(.{ .@"struct" = .{
    .layout = ."extern",
    .fields = &.{.{
        .name = "foo",
        .type = [2]f64,
        .default_value_ptr = null,
        .is_comptime = false,
        .alignment = 1,
    }, .{
        .name = "bar",
        .type = u32,
        .default_value_ptr = &@as(u32, 123),
        .is_comptime = true,
        .alignment = @alignOf(u32),
    }},
    .decls = &.{},
    .is_tuple = false,
} })

⬇️

@Struct(
    ."extern",
    null,
    &.{ "foo", "bar" },
    &.{ [2]f64, u32 },
    &.{
        .{ ."align" = 1 },
        .{ ."comptime" = true, .default_value_ptr = &@as(u32, 123) },
    },
)

Опять же, &@splat(.{}) полезно для задания «по умолчанию» атрибутов полей. В некоторых случаях даже полезно использовать @splat для типов полей. Например, чтобы создать структуру с однородными типами полей FieldType, где имена полей совпадают с именами типа перечисления MyEnum:

const MyStruct = @Struct(.auto, null, std.meta.fieldNames(MyEnum), &@splat(FieldType), &@splat(.{}));

# Объединение

@Union возвращает тип union, эквивалентный @Type(.{ ."union" = ... }). Использование очень похоже на @Struct.

@Type(.{ ."union" = .{
    .layout = .auto,
    .tag_type = MyEnum,
    .fields = &.{.{
        .name = "foo",
        .type = i64,
        .alignment = @alignOf(i64),
    }, .{
        .name = "bar",
        .type = f64,
        .alignment = @alignOf(f64),
    }},
    .decls = &.{},
} })

⬇️

@Union(
    .auto,
    MyEnum,
    &.{ "foo", "bar" },
    &.{ i64, f64 },
    &@splat(.{}),
)

# Перечисление

@Enum возвращает тип enum, эквивалентный @Type(.{ ."enum" = ... }). Синтаксис несколько похож на @Struct, но вместо типов полей принимает массив значений тегов (field values), а не типов.

@Type(.{ ."enum" = .{
    .tag_type = u32,
    .fields = &.{.{
        .name = "foo",
        .value = 0,
    }, .{
        .name = "bar",
        .value = 1,
    }},
    .decls = &.{},
    .is_exhaustive = true,
} })

⬇️

@Enum(
    u32,
    ."exhaustive",
    &.{ "foo", "bar" },
    &.{ 0, 1 },
)

# Число с плавающей точкой

Встроенной функции @Float нет, потому что существует только 5 типов с плавающей точкой времени выполнения, поэтому эта функциональность тривиально реализуется в пользовательском коде. Если требуется создавать типы с плавающей точкой по количеству бит, можно использовать функцию std.meta.Float.

# Массив

Встроенной функции @Array нет, потому что эта функциональность тривиально реализуется обычным синтаксисом массивов. Общая функция Array выглядела бы так:

fn Array(comptime len: usize, comptime Elem: type, comptime sentinel: ?Elem) type {
    return if (sentinel) |s| [len:s]Elem else [len]Elem;
}

На практике такая универсальность обычно не требуется, и места использования можно просто заменить на [len]Elem или [len:s]Elem.

# Непрозрачный тип

Встроенной функции @Opaque нет. Вместо этого пишите opaque {}.

# Необязательный тип

Встроенной функции @Optional нет. Вместо этого пишите ?T.

# Объединение ошибок

Встроенной функции @ErrorUnion нет. Для упрощения языка больше невозможно материализовать объединения ошибок. Вместо этого объявляйте свои множества ошибок явно с помощью синтаксиса error{ ... }.

# Разрешение малых целочисленных типов приводить к типам с плавающей точкой

Если все возможные значения целочисленного типа могут поместиться в тип с плавающей точкой без округления, целое число может быть приведено к типу с плавающей точкой без явного преобразования. Это определяется сравнением количества бит точности в целочисленном типе и мантиссы в типе с плавающей точкой. Для больших целочисленных типов по-прежнему требуется @floatFromInt.

var foo_int: u24 = 123;
var foo_float: f32 = @floatFromInt(foo_int);

var bar_int: u25 = 123;
var bar_float: f32 = @floatFromInt(bar_int);

⬇️

var foo_int: u24 = 123;
var foo_float: f32 = foo_int; // Безопасное приведение

var bar_int: u25 = 123;
var bar_float: f32 = @floatFromInt(bar_int); // Явное преобразование всё ещё требуется

Это часть более масштабной работы по улучшению эргономики для создания видеоигр на Zig.

# Запрет использования векторных индексов во время выполнения

Руководство по переходу:

for (0..vector_len) |i| {
   _ = vector[i];
}

⬇️

// приводим вектор к массиву
const vector_type = @typeInfo(@TypeOf(vector)).vector;
const array: [vector_type.len]vector_type.child = vector;
for (&array) |elem| {
    _ = elem;
}

Это изменение было внесено в рамках переработки синтаксиса передачи по значению.

# Векторы и массивы больше не поддерживают приведение в памяти

Если вы использовали @ptrCast для преобразования между памятью массива и памятью вектора, используйте приведение (coercion) вместо этого.

Если вы приводили тип anyerror![4]i32 к anyerror!@Vector(4, i32) или подобным, сначала необходимо развернуть ошибку.

# Запрет возврата тривиального локального адреса из функций

Одна из сложностей, с которой сталкиваются новички в Zig — особенно те, кто не знаком с ручным управлением памятью — это возврат указателей на локальные переменные из функций.

Эту проблему сложно решить, потому что возвращать недействительный указатель — легально:

fn foo() *i32 {
    return undefined;
}

Это совершенно валидная функция — недопустимая операция происходит только при разыменовании возвращённого указателя. Более того, даже функция, которая безусловно вызывает недопустимое поведение, тоже легальна:

fn bar() noreturn {
    unreachable; // эквивалентно foo().*
}

Для такой функции выражение bar() эквивалентно выражению unreachable.

Так как же сделать ошибкой компиляции возврат недействительного указателя из функции? Синтаксическая педантичность. Мы запрещаем все выражения, которые тривиально (то есть без проверки типов) сводятся к return undefined, с обоснованием, что такие выражения должны быть записаны канонически как return undefined.

Так появилась следующая ошибка компиляции:

fn foo() *i32 {
    var x: i32 = 1234;
    return &x;
}
test.zig:3:13: error: returning address of истекшей локальной переменной 'x'
    return &x;
            ^
test.zig:2:9: note: объявлена здесь во время выполнения
    var x: i32 = 1234;
        ^

Планируется больше подобных ошибок компиляции.

# Унарные встроенные функции для чисел с плавающей запятой теперь выводят тип результата

Раньше Zig не выводил тип результата для следующих встроенных функций:

@sqrt
@sin
@cos
@tan
@exp
@exp2
@log
@log2
@log10
@floor
@ceil
@trunc
@round

Теперь это изменено. Раньше вы не могли написать:

const x: f64 = @sqrt(@floatFromInt(N));

поскольку @sqrt не выводил тип результата f64 для @floatFromInt, теперь это возможно.

Это часть более масштабной работы по улучшению эргономики для разработки видеоигр на Zig.

# @floor, @ceil, @round, @trunc — преобразование в целые числа

Теперь функции @floor, @ceil, @round и @trunc можно использовать для преобразования значения с плавающей запятой в целое число:

float-conversion.zig
const std = @import("std");
const expectEqual = std.testing.expectEqual;

test "round to int" {
    try example(12, 12.34);
    try example(13, 12.50);
}

fn example(expected: u8, value: f32) !void {
    const actual: u8 = @round(value);
    try expectEqual(expected, actual);
}

Shell

$ zig test float-conversion.zig
1/1 float-conversion.test.round to int...OK
All 1 tests passed.

@intFromFloat теперь избыточен вместе с @trunc и поэтому считается устаревшим.

Это часть более масштабной работы по улучшению эргономики для разработки видеоигр на Zig.

# Запрет неиспользуемых битов в упакованных объединениях

Не было единственно возможного способа отображения представления упакованного объединения на биты — в отличие от других упакованных типов. Например, enum (u5) { ... } очевидно представляет собой 5 бит и разрешён в упакованных контекстах, но ?u8 имеет два разумных способа отображения на 9 бит и поэтому не разрешён в упакованных контекстах.

Эта неоднозначность устранена требованием, чтобы все поля упакованного объединения имели одинаковый размер в битах (@bitSizeOf), как у базового целочисленного типа.

Руководство по переходу:

const U = packed union {
    x: u8,
    y: u16,
};

⬇️

const U = packed union(u16) {
    x: packed struct(u16) {
        data: u8,
        padding: u8 = 0,
    },
    y: u16,
};

# Запрет указателей в упакованных структурах и объединениях

Поля типов packed struct и packed union больше не могут быть указателями — реализовано предложение #24657.

Основная причина этого изменения — невозможность представления константных значений с невыровненными по байтам указателями в большинстве бинарных форматов. Кроме того, на некоторых платформах указатели нельзя представить просто как их адресные биты — у них есть дополнительные биты метаданных; в этом случае не имеет смысла упаковывать указатели в целое число, как это делают упакованные типы.

Если вы полагались на указатели в упакованных типах, можно использовать поле типа usize и преобразовывать его к указателю и обратно с помощью @ptrFromInt и @intFromPtr.

# Разрешение явного задания базового целого числа для упакованных объединений

Хотя предыдущие версии Zig позволяли типам packed struct явно указывать свой базовый целочисленный тип с помощью синтаксиса packed struct(T), для типов packed union это ранее не разрешалось. В Zig 0.16.0 это теперь разрешено.

packed_union_explicit_backing_int.zig

// Обычное объявление типа упакованного объединения
const Split16 = packed union(u16) {
    raw: MaybeSigned16,
    split: packed struct { low: u8, high: u8 },
};

// Создание типа упакованного объединения с помощью @Union
const MaybeSigned16 = @Union(
    ."packed",
    u16, // базовый целочисленный тип
    &.{ "unsigned", "signed" },
    &.{ u16, i16 },
    &@splat(.{}),
);

test "use packed union type with explicit backing integer" {
    const u: Split16 = .{ .raw = .{ .unsigned = 0xFFFE } };
    try testing.expectEqual(-2, u.raw.signed);
    try testing.expectEqual(0xFE, u.split.low);
    try testing.expectEqual(0xFF, u.split.high);
}

const testing = @import("std").testing;

Shell

$ zig test packed_union_explicit_backing_int.zig
1/1 packed_union_explicit_backing_int.test.use packed union type with explicit backing integer...OK
All 1 tests passed.

Обратите внимание, что из-за запрета перечислений и упакованных типов с неявными базовыми типами во внешнем контексте иногда требуется явно указывать базовый тип.

# Запрет перечислений и упакованных типов с неявными базовыми типами во внешнем контексте

Типы перечислений (enum) с выводимыми целочисленными тегами и типы packed struct/packed union с выводимым базовым целым числом больше не считаются валидными внешними (extern) типами. Это реализует предложение #24714.

Это изменение ломает обратную совместимость и сделано для того, чтобы ABI типа не определялся полностью неявно только по его полям. В частности, это важно потому, что у u8 и i8 могут быть разные ABI в некоторых контекстах, и неясно, какой из них используется при неявном выборе.

Если это вызвало ошибку компиляции в вашем коде, устраните её добавлением явного типа тега или базового типа. (См. также Разрешение явного задания базового целого числа для упакованных объединений.)

extern_implicit_backing_type.zig

const Enum = enum { a, b, c, d };
const PackedStruct = packed struct { a: u4, b: u4 };
const PackedUnion = packed union { a: u8, b: i8 };

export var some_enum: Enum = .a;
export var some_packed_struct: PackedStruct = .{ .a = 1, .b = 2 };
export var some_packed_union: PackedUnion = .{ .a = 123 };

Shell

$ zig test extern_implicit_backing_type.zig
/home/ci/.cache/act/ade7a1d6abc67e0a/hostexecutor/src/download/0.16.0/release-notes/extern_implicit_backing_type.zig:5:1: error: unable to export type 'extern_implicit_backing_type.Enum'
export var some_enum: Enum = .a;
^~~~~~
/home/ci/.cache/act/ade7a1d6abc67e0a/hostexecutor/src/download/0.16.0/release-notes/extern_implicit_backing_type.zig:1:14: note: целочисленный тип тега перечисления выводится автоматически (inferred)
const Enum = enum { a, b, c, d };
             ^~~~~~~~~~~~~~~~~~~~
...

⬇️

extern_explicit_backing_type.zig

const Enum = enum(u8) { a, b, c, d };
const PackedStruct = packed struct(u8) { a: u4, b: u4 };
const PackedUnion = packed union(u8) { a: u8, b: i8 };

export var some_enum: Enum = .a;
export var some_packed_struct: PackedStruct = .{ .a = 1, .b = 2 };
export var some_packed_union: PackedUnion = .{ .a = 123 };

Shell

$ zig test extern_explicit_backing_type.zig
All 0 tests passed.

# Ленивый анализ полей

С момента введения интерфейса ввода-вывода была замечена проблема: если тип используется как пространство имён, его поля всё равно анализируются. Например, использование std.Io.Writer в любом виде приводит к включению vtable для std.Io. В некоторых случаях это могло приводить к ненужной генерации кода, что раздувало размер бинарных файлов.

Теперь struct (напомним, что файлы — это struct), union, enum и opaque анализируются только тогда, когда требуется их размер или тип одного из полей. Это означает, что вы можете использовать типы как пространства имён без обращения к ним, а также использовать недереференсированные указатели *T без необходимости разрешения типа T.

Это изменение было внесено в рамках переработки разрешения типов.

# Указатели на типы, доступные только во время компиляции, больше не являются таковыми

Например, хотя comptime_int — это тип, доступный только во время компиляции, *comptime_int таковым не является, как и []comptime_int. На первый взгляд это может показаться запутанным — проще всего понять это на примере указателей на функции. Тип *const fn () void — это тип времени выполнения. Однако вы не можете разыменовать его во время выполнения, потому что тип элемента (тип тела функции fn () void) доступен только во время компиляции. Таким образом, такие указатели могут существовать во время выполнения, но могут быть разыменованы только во время компиляции. Это делает их практически бесполезными во время выполнения — но есть исключение! Предположим, у вас есть []const std.builtin.Type.StructField, и вы хотите передать имя каждого поля в какой-то код времени выполнения. Раньше для этого приходилось создавать отдельный []const []const u8. Теперь же вы можете напрямую передать []const std.builtin.Type.StructField в функцию времени выполнения. Естественно, эта функция не может загрузить сам StructField из этого среза во время выполнения. Однако она может загрузить поле name, потому что оно имеет тип времени выполнения!

Это изменение было внесено в рамках переработки разрешения типов.

# Явно выровненные типы указателей теперь отличаются от естественно выровненных

Раньше Zig считал, что типы *u8 и *align(1) u8 буквально одинаковы; они сравнивались как равные, и *u8 считался канонической формой (именно так компилятор выводил тип). Теперь эти два типа больше не считаются эквивалентными.

Важно: эти два типа по-прежнему можно использовать взаимозаменяемо. Они приводятся друг к другу даже через указатели (то, что компилятор называет «приведениями в памяти»), и почти во всех случаях нет необходимости заботиться о том, какой из них у вас есть. Эту разницу можно сравнить с разницей между u32 и c_uint: технически это разные типы, но (если ваша платформа имеет 32-битный int) они ведут себя одинаково для всех практических целей, и технически не имеет значения, какой из них выбрать.

Это изменение было внесено в рамках переработки разрешения типов.

# Упрощённые правила для циклов зависимостей

Появились новые случаи, которые теперь считаются циклами зависимостей, хотя раньше таковыми не являлись.

Однако теперь стало более очевидно, почему существует цикл зависимостей — благодаря упрощённым правилам проверки типов и улучшенным ошибкам компиляции. Это также снижает сложность формального описания языка Zig.

Это изменение было внесено в рамках переработки разрешения типов.

# Поля кортежей с нулевым размером больше не являются неявно comptime

В версии 0.14.0 было случайно введено правило: поля кортежей с типами нулевого размера неявно становились полями comptime:

comptime {
    const S = struct { void };
    @compileLog(@typeInfo(S).@"struct".fields[0].is_comptime); // @as(bool, true)
}

Zig 0.16.0 отменяет это изменение: теперь такое поле кортежа больше не считается полем comptime. Однако это не мешает значению поля всегда быть известным во время компиляции:

test "zero-bit tuple field is comptime-known" {
    const S = struct { u32, void };
    var runtime_known: S = undefined;
    runtime_known = .{ 123, {} };
    // Даже если кортеж известен во время выполнения, поле с нулевым размером известно во время компиляции:
    comptime assert(runtime_known[1] == {});
}
const assert = @import("std").debug.assert;

Иными словами, это изменение практически не нарушает совместимость. Единственный случай, когда оно может повлиять на старый код — если вы напрямую полагались на std.builtin.StructField.is_comptime из @typeInfo, или на эквивалентность кортежей с явно объявленными и не объявленными полями comptime:

//! Эти тесты проходили в Zig 0.15.x, но не проходят в Zig 0.16.x.
test "zero-bit tuple field is comptime" {
    const S = struct { void };
    try expect(@typeInfo(S).@"struct".fields[0].is_comptime);
}
test "comptime annotation on zero-bit field is irrelevant to type equivalence" {
    const A = struct { void };
    const B = struct { comptime void = {} };
    try expect(A == B);
}
const expect = @import("std").testing.expect;

# Стандартная библиотека

Добавлено:

  • Io.Dir.renamePreserve: операция переименования без замены целевого файла
  • Io.net.Socket.createPair

Удалено:

  • SegmentedList
  • meta.declList
  • Io.GenericWriter
  • Io.AnyWriter
  • Io.null_writer
  • Io.CountingReader
  • Thread.Mutex.Recursive

Изменения в наборах ошибок:

  • error.RenameAcrossMountPoints ➡️ error.CrossDevice
  • error.NotSameFileSystem ➡️ error.CrossDevice
  • error.SharingViolation ➡️ error.FileBusy
  • error.EnvironmentVariableNotFound ➡️ error.EnvironmentVariableMissing
  • std.Io.Dir.rename возвращает error.DirNotEmpty вместо error.PathAlreadyExists

Прочие изменения:

  • fmt: Formatter ➡️ Alt
  • fmt: format ➡️ std.Io.Writer.print
  • fmt: FormatOptions ➡️ Options
  • fmt: bufPrintZ ➡️ bufPrintSentinel
  • compress: lzma, lzma2 и xz обновлены до Io.Reader / Io.Writer
  • DynLib: удалена поддержка Windows. Теперь пользователи должны использовать LoadLibraryExW и GetProcAddress напрямую.
  • math.sign: возвращает наименьший целочисленный тип, вмещающий возможные значения
  • Автоматическое получение корневых сертификатов на Windows при запуске
  • tar.extract: защита от обхода путей
  • BitSet, EnumSet: initEmpty, initFull заменены на литералы объявлений

# Интерфейс ввода-вывода

Начиная с Zig 0.16.0, вся функциональность ввода-вывода требует передачи экземпляра Io. Как правило, всё, что потенциально блокирует поток управления или вносит недетерминизм, должно принадлежать интерфейсу ввода-вывода.

Вместе с интерфейсом этот релиз включает следующие реализации:

  • Io.Threaded — основан на потоках. С этой реализацией операции ввода-вывода просты. Например, операции с файловой системой напрямую вызывают read, write, open, close и т.д. При обновлении кода с Zig 0.15.x эта реализация обеспечивает эквивалентное поведение. Эта реализация полностью функциональна и хорошо протестирована, включая отмену. Это реализация по умолчанию для «Juicy Main».
    • -fno-single-threaded — поддерживает параллелизм задач и отмену.
    • -fsingle-threaded — не поддерживает параллелизм задач и отмену.
  • Io.Eventedв разработке, экспериментальная реализация для развития интерфейса. Основана на переключении стека в пользовательском пространстве с «кражей» работы (work stealing), также известной как M:N потоки («зелёные потоки», стековые корутины).
    • Io.Uring — хотя не было в центре внимания этого цикла разработки, уже есть доказательство концепции на базе Linux io_uring. Этот бэкенд обладает отличными свойствами, но ещё не завершён: отсутствуют сетевые функции, обработка ошибок, покрытие тестами и минимальные выделения стека задач.
    • Io.Kqueue — только доказательство концепции; достаточно для исправления распространённой ошибки в других асинхронных рантаймах.
    • Io.Dispatch — основан на Grand Central Dispatch (macOS).
  • Io.failing — симулирует систему без поддерживаемых операций.

Обзор:

  • Future — абстракция уровня задач на основе функций. Позволяет вводить независимость операций (асинхронность) среди любого набора вызовов функций.
  • Group — эффективно управляет множеством независимых задач. Поддерживает ожидание и отмену всех задач группы одновременно.
  • Queue(T) — много производителей/много потребителей (MPMC), потокобезопасная очередь с настраиваемым размером буфера во время выполнения. Когда буфер пуст, потребители приостанавливаются и возобновляются производителями; когда буфер полон — наоборот.
  • Select — выполняет задачи совместно; позволяет ждать завершения одной или нескольких задач. Похоже на Batch, но работает на более высоком уровне абстракции задач.
  • Batch — низкоуровневая абстракция для введения независимости среди любого набора операций.
  • Clock, Duration, Timestamp, Timeout — типобезопасность для единиц измерения времени

Пример HTTP-запроса к домену:

http-get.zig
const std = @import("std");
const Io = std.Io;

pub fn main(init: std.process.Init) !void {
    const gpa = init.gpa;
    const io = init.io;

    const args = try init.minimal.args.toSlice(init.arena.allocator());

    const host_name: Io.net.HostName = try .init(args[1]);

    var http_client: std.http.Client = .{ .allocator = gpa, .io = io };
    defer http_client.deinit();

    var request = try http_client.request(.HEAD, .{
        .scheme = "http",
        .host = .{ .percent_encoded = host_name.bytes },
        .port = 80,
        .path = .{ .percent_encoded = "/" },
    }, .{});
    defer request.deinit();

    try request.sendBodiless();

    var redirect_buffer: [1024]u8 = undefined;
    const response = try request.receiveHead(&redirect_buffer);
    std.log.info("received {d} {s}", .{ response.head.status, response.head.reason });
}

Shell

$ zig build-exe http-get.zig
$ ./http-get example.com
info: received 200 OK

Благодаря тому, что сетевые функции теперь используют новый интерфейс std.Io, этот код обладает следующими свойствами:

  • Асинхронно отправляет DNS-запросы к каждому настроенному серверу имён.
  • По мере поступления каждого ответа сразу же асинхронно пытается установить TCP-соединение с возвращённым IP-адресом.
  • При первом успешном TCP-соединении все остальные попытки соединения (включая DNS-запросы) отменяются.
  • Код работает даже при компиляции с -fsingle-threaded (операции выполняются последовательно).
  • В Windows всё это происходит без зависимости от ws2_32.dll.

Параметр init: std.process.Init реализован благодаря «Juicy Main».

При обновлении кода, если у вас нет доступа к экземпляру Io, вы можете получить его так:

var threaded: Io.Threaded = .init_single_threaded;
const io = threaded.io();

Это работает до тех пор, пока вам не нужен параллелизм задач; однако это неидеальное решение — как использование std.heap.page_allocator вместо Allocator. Лучше принимать параметр Io (или хранить его в структуре контекста). Главное — за создание экземпляра Io обычно отвечает функция main приложения.

При тестировании рекомендуется использовать std.testing.io (по аналогии со std.testing.allocator).

# Future

Future — это абстракция уровня задач, основанная на функциях.

io.async создаёт Future(T), где T — возвращаемый тип вызываемой функции. async выражает асинхронность: вызов функции независим от другой логики. Создание такой задачи всегда успешно и переносимо между ограниченными реализациями Io, включая те, где нет механизма параллелизма. Реализациям Io разрешено выполнять вызовы async просто прямым вызовом функции до возврата.

io.concurrent аналогичен io.async, но явно указывает, что операция должна выполняться параллельно для корректности. Это обязательно требует выделения памяти, поскольку такова природа одновременного выполнения. Поэтому эта функция может завершиться ошибкой с error.ConcurrencyUnavailable.

В обоих случаях создаётся структура Future(T), у которой есть два метода:

  • await — логически блокирует поток управления до завершения задачи и возвращает результат функции.
  • cancel — аналогичен await, но также запрашивает у реализации Io прерывание операции и возврат error.Canceled. Большинство операций ввода-вывода теперь включают error.Canceled в свои наборы ошибок.

Используйте такой шаблон, чтобы избежать утечек ресурсов и корректно обрабатывать отмену:

var foo_future = io.async(foo, .{args});
defer if (foo_future.cancel(io)) |resource| resource.deinit() else |_| {}

var bar_future = io.async(bar, .{args});
defer if (bar_future.cancel(io)) |resource| resource.deinit() else |_| {}

const foo_result = try foo_future.await(io);
const bar_result = try bar_future.await(io);

Если функции foo или bar не возвращают ресурс, который нужно освобождать, конструкцию if можно упростить до _ = foo.cancel(io) catch {}, а если функция возвращает void, можно убрать и игнорирование результата. Однако вызов cancel необходим, чтобы освободить ресурсы асинхронной задачи при ошибках (включая error.Canceled).

# Group

Группы подходят, когда у многих задач общий жизненный цикл. Они обеспечивают накладные расходы O(1) на запуск N задач.

group.zig
const std = @import("std");
const Io = std.Io;

test "sleep sort" {
    const io = std.testing.io;

    // Инициализируем массив 10 случайными числами.
    const rng_impl: std.Random.IoSource = .{ .io = io };
    const rng = rng_impl.interface();

    var array: [10]i32 = undefined;
    for (&array) |*elem| elem.* = rng.uintLessThan(u16, 1000);

    var sorted: [10]i32 = undefined;
    var index: std.atomic.Value(usize) = .init(0);

    // Запускаем задачу для каждого элемента: она "спит" миллисекунд,
    // равных значению элемента, затем добавляет элемент.
    var group: Io.Group = .init;
    defer group.cancel(io);

    for (&array) |elem| group.async(io, sleepAppend, .{ io, &sorted, &index, elem });

    try group.await(io);

    // Проверяем сортировку.
    for (sorted[0 .. sorted.len - 1], sorted[1..]) |a, b| {
        try std.testing.expect(a <= b);
    }
}

fn sleepAppend(io: Io, result: []i32, i_ptr: *std.atomic.Value(usize), elem: i32) !void {
    try io.sleep(.fromMilliseconds(elem), .awake);
    result[i_ptr.fetchAdd(1, .monotonic)] = elem;
}

Shell

$ zig test group.zig
1/1 group.test.sleep sort...OK
All 1 tests passed.

# Отмена

Вот как правильно пишется слово «отмена» — с одной «л»! Не позволяйте безбожным лжецам сбивать вас с толку.

Как и при досрочном выходе из цикла for, при одновременном выполнении нескольких задач часто возникает ситуация: если одна задача завершилась (например, с ошибкой), нужно прервать другие, потому что их результаты или побочные эффекты уже не имеют значения — а иногда их даже нужно отменить.

API Future, Group и Batch поддерживают запрос отмены. Запрошенная отмена может быть как подтверждена реализацией, так и проигнорирована. Подтверждённые запросы отмены приводят к возврату из операций ввода-вывода ошибки error.Canceled. Даже реализация Io.Threaded поддерживает отмену: она посылает сигнал потоку, вызывая возврат из блокирующих системных вызовов с кодом EINTR, а затем реагирует на этот код проверкой запроса отмены перед повтором системного вызова.

Игнорировать error.Canceled безопасно только в той логике, которая инициировала отмену. В остальных случаях есть три способа обработки error.Canceled (в порядке убывания частоты):

  1. Пробросить ошибку дальше.
  2. После получения ошибки вызвать io.recancel() и не пробрасывать ошибку. Это снова выставляет запрос отмены, чтобы следующая проверка могла его обнаружить и подтвердить.
  3. Сделать ошибку недостижимой с помощью io.swapCancelProtection().

В целом отмена эквивалентна ожиданию (await), за исключением самого запроса отмены. Это значит, что вы всё равно можете получить результат задачи — которая могла успешно завершиться несмотря на запрос отмены. В этом случае нужно учитывать побочные эффекты (например, выделение ресурсов). Вот пример открытия файла с немедленной отменой задачи. Обратите внимание: нужно учитывать возможность успешного открытия файла.

cancel.zig
const std = @import("std");
const Io = std.Io;

test "trivial cancel demo" {
    const io = std.testing.io;

    var file_task = io.async(Io.Dir.openFile, .{ .cwd(), io, "hello.txt", .{} });
    defer if (file_task.cancel(io)) |file| file.close(io) else |_| {};
}

Shell

$ zig test cancel.zig
1/1 cancel.test.trivial cancel demo...OK
All 1 tests passed.

Обычно, поскольку и await, и cancel идемпотентны, самый полезный шаблон — это defer отмена после создания задачи. Это гарантирует освобождение ресурсов (включая параллельные задачи) до выхода из функции.

В целом программистам Zig не нужно явно добавлять поддержку отмены, потому что error.Canceled уже встроен в наборы ошибок всех отменяемых операций ввода-вывода. Однако дополнительные точки отмены можно добавить вызовом io.checkCancel. Вызывать эту функцию требуется редко; основной случай применения — долгие вычисления на CPU, которым нужно реагировать на отмену до завершения.

# Batch

Batch можно рассматривать как низкоуровневый механизм параллелизма: он обеспечивает параллелизм на уровне операции (Operation), что эффективно и переносимо, но сложнее для абстрагирования — особенно если между операциями нужно выполнять какую-то логику.

В перспективе большая часть функциональности Файловой системы и Сети будет переведена на операции (Operation), что сделает их совместимыми с Batch и с функцией operateTimeout, предоставляющей универсальный способ добавить таймаут к любой операции ввода-вывода.

Сейчас список совместимых операций:

  • FileReadStreaming
  • FileWriteStreaming
  • DeviceIoControl
  • NetReceive

В то же время Future — это эквивалент на уровне функций: гибко и удобно, но требует выделения памяти под задачу и может приводить к ошибкам error.ConcurrencyUnavailable (при использовании concurrent) или нежелательным блокировкам (при использовании async) чаще, чем низкоуровневые API Batch.

Поэтому если вы пишете оптимальное и переиспользуемое ПО и вам просто нужно выполнить несколько операций одновременно — используйте Batch. Если же это потребует переизобретения Future — всегда можно использовать API Future. Или начать с них и оптимизировать переходом на Batch позже, если потребуется снизить накладные расходы на задачи.

# Примитивы синхронизации

API синхронизации должны быть переведены на новые API std.Io, чтобы синхронизируемый код корректно интегрировался с выбранной реализацией ввода-вывода приложения. Это обеспечит, например, что при использовании std.Io.Threaded захват спорного мьютекса заблокирует поток, а при использовании std.Io.Evented произойдёт переключение стека.

Эти API также корректно интегрируются с отменой.

  • std.Thread.ResetEvent ➡️ std.Io.Event
  • std.Thread.WaitGroup ➡️ std.Io.Group
  • std.Thread.Futex ➡️ std.Io.Futex
  • std.Thread.Mutex ➡️ std.Io.Mutex
  • std.Thread.Condition ➡️ std.Io.Condition
  • std.Thread.Semaphore ➡️ std.Io.Semaphore
  • std.Thread.RwLock ➡️ std.Io.RwLock
  • std.once удалён; избегайте глобальных переменных или реализуйте логику вручную

Примитивы синхронизации без блокировок не требуют интеграции со std.Io.

# Энтропия

Руководство по переходу:

std.crypto.random.bytes

var buffer: [123]u8 = undefined;
std.crypto.random.bytes(&buffer);

⬇️

var buffer: [123]u8 = undefined;
io.random(&buffer);

std.crypto.random (интерфейс std.Random)

const rng = std.crypto.random;

⬇️

const rng_impl: std.Random.IoSource = .{ .io = io };
const rng = rng_impl.interface();

posix.getrandom

var buffer: [64]u8 = undefined;
posix.getrandom(&buffer);

⬇️

var buffer: [64]u8 = undefined;
io.random(&buffer);

std.Options.crypto_always_getrandom и std.Options.crypto_fork_safety

Вместо этих глобальных опций теперь два разных API std.Io:

/// Получает энтропию.
/// Реализация может хранить состояние CSPRNG в памяти процесса и использовать его для заполнения буфера.
/// Степень криптографической стойкости определяется реализацией Io.
/// Потокобезопасно.
/// См. также randomSecure.
pub fn random(io: Io, buffer: []u8) void {
    return io.vtable.random(io.userdata, buffer);
}
pub const RandomSecureError = error{EntropyUnavailable} || Cancelable;
/// Получает криптографически стойкую энтропию извне процесса.
/// Всегда выполняет системный вызов или иным образом избегает зависимости от состояния процесса для получения свежей случайности.
/// Не имеет механизмов отката; возвращает error.EntropyUnavailable при любых проблемах.
/// Потокобезопасно.
/// См. также random.
pub fn randomSecure(io: Io, buffer: []u8) RandomSecureError!void {
    return io.vtable.randomSecure(io.userdata, buffer);
}

Если вы хотите исключить хранение состояния криптографически стойкого CSPRNG в памяти процесса — используйте Io.randomSecure вместо Io.random.

# Время

В этом выпуске добавлена возможность получать разрешение часов, что может завершиться ошибкой. Это позволяет убрать ошибки error.Unexpected и error.ClockUnsupported из наборов ошибок таймаутов и чтения времени, поскольку теперь их можно рассматривать как имеющие бесконечное разрешение, что пользователь может проверить заранее, вызвав Clock.resolution.

Руководство по переходу:

  • std.time.Instant ➡️ std.Io.Timestamp
  • std.time.Timer ➡️ std.Io.Timestamp
  • std.time.timestamp ➡️ std.Io.Timestamp.now

# Файловая система

Все API fs переведены на Io.

Хотя изменений много и они ломают обратную совместимость (в отличие от "writergate"), ожидается, что переход будет простым для программистов Zig — он не требует глубокого осмысления. Например:

file.close();

⬇️

file.close(io);

Хотя ваш diff при обновлении может быть большим, понять необходимые изменения будет несложно.

Добавлено:

  • Io.Dir.hardLink
  • Io.Dir.Reader
  • Io.Dir.setFilePermissions
  • Io.Dir.setFileOwner
  • Io.File.NLink

Удалено без замены:

  • fs.realpathZ
  • fs.realpathW
  • fs.realpathW2
  • fs.makeDirAbsoluteZ
  • fs.deleteDirAbsoluteZ
  • fs.openDirAbsoluteZ
  • fs.renameAbsoluteZ
  • fs.renameZ
  • fs.deleteTreeAbsolute
  • fs.symLinkAbsoluteW
  • fs.Dir.realpathZ
  • fs.Dir.realpathW
  • fs.Dir.realpathW2
  • fs.Dir.deleteFileZ
  • fs.Dir.deleteFileW
  • fs.Dir.deleteDirZ
  • fs.Dir.deleteDirW
  • fs.Dir.renameZ
  • fs.Dir.renameW
  • fs.Dir.symLinkWasi
  • fs.Dir.symLinkZ
  • fs.Dir.symLinkW
  • fs.Dir.readLinkWasi
  • fs.Dir.readLinkZ
  • fs.Dir.readLinkW
  • fs.Dir.adaptToNewApi
  • fs.Dir.adaptFromNewApi
  • fs.File.isCygwinPty
  • fs.File.adaptToNewApi
  • fs.File.adaptFromNewApi

Изменено:

  • fs.copyFileAbsolute ➡️ std.Io.Dir.copyFileAbsolute
  • fs.makeDirAbsolute ➡️ std.Io.Dir.createDirAbsolute
  • fs.deleteDirAbsolute ➡️ std.Io.Dir.deleteDirAbsolute
  • fs.openDirAbsolute ➡️ std.Io.Dir.openDirAbsolute
  • fs.openFileAbsolute ➡️ std.Io.Dir.openFileAbsolute
  • fs.accessAbsolute ➡️ std.Io.Dir.accessAbsolute
  • fs.createFileAbsolute ➡️ std.Io.Dir.createFileAbsolute
  • fs.deleteFileAbsolute ➡️ std.Io.Dir.deleteFileAbsolute
  • fs.renameAbsolute ➡️ std.Io.Dir.renameAbsolute
  • fs.readLinkAbsolute ➡️ std.Io.Dir.readLinkAbsolute
  • fs.symLinkAbsolute ➡️ std.Io.Dir.symLinkAbsolute
  • fs.has_executable_bit ➡️ std.Io.File.Permissions.has_executable_bit
  • fs.realpath ➡️ std.Io.Dir.realPathFileAbsolute
  • fs.rename ➡️ std.Io.Dir.rename
  • fs.cwd ➡️ std.Io.Dir.cwd
  • fs.defaultWasiCwd ➡️ std.os.defaultWasiCwd
  • fs.realpathAlloc ➡️ std.Io.Dir.realPathFileAbsoluteAlloc
  • fs.openSelfExe ➡️ std.process.openExecutable
  • fs.selfExePathAlloc ➡️ std.process.executablePathAlloc
  • fs.selfExePath ➡️ std.process.executablePath
  • fs.selfExeDirPath ➡️ std.process.executableDirPath
  • fs.selfExeDirPathAlloc ➡️ std.process.executableDirPathAlloc
  • fs.Dir.setAsCwd ➡️ std.process.setCurrentDir
  • fs.Dir.realpath ➡️ std.Io.Dir.realPathFile
  • fs.Dir.realpathAlloc ➡️ std.Io.Dir.realPathFileAlloc
  • fs.Dir ➡️ std.Io.Dir
  • fs.File ➡️ std.Io.File
  • fs.Dir.makeDir ➡️ std.Io.Dir.createDir
  • fs.Dir.makePath ➡️ std.Io.Dir.createDirPath
  • fs.Dir.makeOpenDir ➡️ std.Io.Dir.createDirPathOpen
  • fs.Dir.rename: now accepts two Dir parameters (plus Io)
  • fs.Dir.atomicSymLink ➡️ std.Io.Dir.symLinkAtomic
  • fs.Dir.chmod ➡️ std.Io.Dir.setPermissions
  • fs.Dir.chown ➡️ std.Io.Dir.setOwner
  • fs.File.Mode ➡️ std.Io.File.Permissions
  • fs.File.PermissionsWindows ➡️ std.Io.File.Permissions
  • fs.File.PermissionsUnix ➡️ std.Io.File.Permissions
  • fs.File.default_mode ➡️ std.Io.File.Permissions.default_file
  • fs.File.getOrEnableAnsiEscapeSupport ➡️ std.Io.File.enableAnsiEscapeCodes
  • fs.File.setEndPos ➡️ std.Io.File.setLength
  • fs.File.getEndPos ➡️ std.Io.File.length
  • fs.File.seekTo, std.fs.File.seekBy, std.fs.File.seekFromEnd ➡️ std.Io.File.Reader.seekTo, std.Io.File.Reader.seekBy, std.Io.File.Writer.seekTo
  • fs.File.getPos ➡️ std.Io.File.Reader.logicalPos, std.Io.Writer.logicalPos
  • fs.File.mode ➡️ std.Io.File.stat().permissions.toMode
  • fs.File.chmod ➡️ std.Io.File.setPermissions
  • fs.File.chown ➡️ std.Io.File.setOwner
  • fs.File.updateTimes ➡️ std.Io.File.setTimestamps, std.Io.File.setTimestampsNow
  • fs.File.read ➡️ std.Io.File.readStreaming
  • fs.File.readv ➡️ std.Io.File.readStreaming
  • fs.File.pread ➡️ std.Io.File.readPositional
  • fs.File.preadv ➡️ std.Io.File.readPositional
  • fs.File.preadAll ➡️ std.Io.File.readPositionalAll
  • fs.File.write ➡️ std.Io.File.writeStreaming
  • fs.File.writev ➡️ std.Io.File.writeStreaming
  • fs.File.pwrite ➡️ std.Io.File.writePositional
  • fs.File.pwritev ➡️ std.Io.File.writePositional
  • fs.File.writeAll ➡️ std.Io.File.writeStreamingAll
  • fs.File.pwriteAll ➡️ std.Io.File.writePositionalAll
  • fs.File.copyRange, std.fs.File.copyRangeAll ➡️ std.Io.File.writer

Многие функции теперь принимают параметр Io.

Устарело:

  • fs.path ➡️ std.Io.Dir.path
  • fs.max_path_bytes ➡️ std.Io.Dir.max_path_bytes
  • fs.max_name_bytes ➡️ std.Io.Dir.max_name_bytes

# Сеть

Все API net переведены на Io.

Io.Evented пока не реализует сетевые функции. Io.net пока не поддерживает не-IP сети.

# Процесс

Запуск дочернего процесса:

var child = std.process.Child.init(argv, gpa);
    child.stdin_behavior = .Pipe;
    child.stdout_behavior = .Pipe;
    child.stderr_behavior = .Pipe;
    try child.spawn(io);

⬇️

var child = try std.process.spawn(io, .{
        .argv = argv,
        .stdin = .pipe,
        .stdout = .pipe,
        .stderr = .pipe,
    });

Запуск дочернего процесса и захват его вывода:

const result = std.process.Child.run(allocator, io, .{

⬇️

const result = std.process.run(allocator, io, .{

Замена образа текущего процесса:

const err = std.process.execv(arena, argv);

⬇️

const err = std.process.replace(io, .{ .argv = argv });

# File.MemoryMap

Содержимое указателя определено так, что оно синхронизируется только после явных точек синхронизации. Это позволяет реализовать fallback на основе файловых операций, при этом поддерживая ряд сценариев использования отображения в память.

Кроме того, это делает допустимым для событийных реализаций ввода-вывода использовать событийные файловые операции для точек синхронизации вместо отображения в память.

Технически это изменение ломает обратную совместимость, поскольку наборы ошибок для позиционного чтения и записи файлов стали более строгими. Также в WASI теперь корректно возвращается error.IsDir вместо error.NotOpenForReading.

# Удаление std.posix и os.windows

Большинство функций std.posix и std.os.windows находились на неудобном среднем уровне абстракции и были удалены. Поэтому, если вы использовали какие-либо из удалённых функций, теперь нужно выбрать направление:

  • Перейти выше: использовать std.Io
  • Перейти ниже: использовать std.posix.system напрямую

Планируются дальнейшие удаления.

# heap.ArenaAllocator становится потокобезопасным и без блокировок

Безблокировочная и потокобезопасная реализация лучше сочетается с интеграцией std.Io и интеграцией с libc. Избегая блокировок, мы избавляемся от необходимости в примитивах синхронизации, а значит — и от необходимости в экземпляре Io, а также позволяем использовать Allocator как базовый аллокатор для экземпляра Io.

Новая реализация обеспечивает производительность, сравнимую с предыдущей при доступе из одного потока, и небольшое ускорение по сравнению с предыдущей реализацией, обёрнутой в ThreadSafeAllocator, вплоть до ~7 потоков, выполняющих операции одновременно.

Подробнее

То же самое планируется для heap.DebugAllocator

# heap.ThreadSafe Allocator удалён

Единственный разумный способ реализовать ThreadSafeAllocator, который оборачивает базовый Allocator, — это с помощью мьютекса, что требует наличия экземпляра Io и обычно неэффективно. В то же время практически любой Allocator, где требуется потокобезопасность, можно сделать без блокировок и избежать медленных блокирующих мьютеков — или хотя бы на некоторых горячих путях! ThreadSafeAllocator — это антипаттерн. Здесь требуется более тесная интеграция.

# Добавлена Deflate-компрессия, упрощена декомпрессия

Добавлена компрессия Deflate, реализованная с нуля. В буфере писателя хранится скользящее окно для поиска совпадений, а для поиска совпадений используется хеш-таблица с цепочками. Токены накапливаются до достижения порога и затем выводятся в виде блока.

Дополнительно предоставляются два других writer'а Deflate:

  • Raw — пишет только несжатые блоки (store blocks). Использует векторы данных для эффективной передачи заголовков блоков и данных.
  • Huffman — выполняет только сжатие Хаффмана без поиска совпадений.

Описанные реализации также могут использовать семантику writer'ов, так как им не требуется хранить историю.

Параметры кодов литералов и расстояний в токене также переработаны. Теперь их параметры вычисляются математически, однако более дорогие по-прежнему берутся из таблицы поиска (кроме ReleaseSmall).

Чтение битов при декомпрессии значительно упрощено за счёт возможности заглядывать (peek) в нижележащий reader. Кроме того, исправлены некоторые ошибки с обработкой лимитов.

# Сравнение с Zlib

zlib обеспечивает коэффициент сжатия на 1.00% лучше на уровне сжатия по умолчанию и на 0.77% лучше на максимальном уровне. Похоже, zlib выбирает немного другие совпадения, однако общее количество совпавших байт меньше. В будущем было бы хорошо разобраться в этом и достичь паритета с zlib.

Вот бенчмарк производительности по сравнению с zlib при эквивалентных параметрах (т.е. уровнях).

С уровнем сжатия по умолчанию:

Benchmark 1 (20 runs): sh -c ./zpipe<sample
  measurement          mean ± σ            min … max           outliers         delta
  wall_time           252ms ± 1.07ms     250ms …  255ms          1 ( 5%)        0%
  peak_rss           5.46MB ± 97.4KB    5.32MB … 5.64MB          0 ( 0%)        0%
  cpu_cycles         1.19G  ± 4.44M     1.19G  … 1.21G           2 (10%)        0%
  instructions       1.83G  ±  665      1.83G  … 1.83G           3 (15%)        0%
  cache_references    117M  ±  904K      116M  …  120M           1 ( 5%)        0%
  cache_misses       1.66M  ±  931K      942K  … 5.00M           1 ( 5%)        0%
  branch_misses      13.6M  ± 9.84K     13.6M  … 13.7M           1 ( 5%)        0%
Benchmark 2 (22 runs): sh -c ./std-deflate<sample
  measurement          mean ± σ            min … max           outliers         delta
  wall_time           228ms ±  841us     226ms …  229ms          0 ( 0%)        ⚡-  9.7% ±  0.2%
  peak_rss           5.45MB ±  116KB    5.24MB … 5.61MB          0 ( 0%)          -  0.2% ±  1.2%
  cpu_cycles         1.07G  ± 1.33M     1.07G  … 1.08G           1 ( 5%)        ⚡-  9.8% ±  0.2%
  instructions       2.18G  ±  825      2.18G  … 2.18G           0 ( 0%)        💩+ 18.9% ±  0.0%
  cache_references   95.0M  ±  435K     94.1M  … 96.1M           1 ( 5%)        ⚡- 18.7% ±  0.4%
  cache_misses        874K  ±  326K      499K  … 1.94M           1 ( 5%)        ⚡- 47.3% ± 25.7%
  branch_misses      6.30M  ± 18.3K     6.24M  … 6.32M           2 ( 9%)        ⚡- 53.7% ±  0.1%

С максимальным уровнем сжатия:

Benchmark 1 (7 runs): sh -c ./zpipe<sample
  measurement          mean ± σ            min … max           outliers         delta
  wall_time           803ms ± 5.75ms     798ms …  815ms          0 ( 0%)        0%
  peak_rss           5.48MB ±  120KB    5.24MB … 5.61MB          0 ( 0%)        0%
  cpu_cycles         3.85G  ± 30.5M     3.83G  … 3.92G           0 ( 0%)        0%
  instructions       5.32G  ± 1.11K     5.32G  … 5.32G           0 ( 0%)        0%
  cache_references    414M  ± 1.47M      412M  …  416M           0 ( 0%)        0%
  cache_misses       7.91M  ± 1.12M     6.15M  … 9.30M           0 ( 0%)        0%
  branch_misses      28.6M  ± 15.2K     28.6M  … 28.7M           0 ( 0%)        0%
Benchmark 2 (7 runs): sh -c ./std-deflate<sample
  measurement          mean ± σ            min … max           outliers         delta
  wall_time           797ms ± 1.19ms     795ms …  798ms          0 ( 0%)          -  0.8% ±  0.6%
  peak_rss           5.50MB ± 82.3KB    5.35MB … 5.60MB          0 ( 0%)          +  0.3% ±  2.2%
  cpu_cycles         3.82G  ± 2.11M     3.82G  … 3.82G           0 ( 0%)          -  0.7% ±  0.7%
  instructions       8.19G  ±  508      8.19G  … 8.19G           0 ( 0%)        💩+ 54.1% ±  0.0%
  cache_references    345M  ± 1.02M      344M  …  346M           0 ( 0%)        ⚡- 16.8% ±  0.4%
  cache_misses       4.63M  ±  393K     4.20M  … 5.44M           0 ( 0%)        ⚡- 41.5% ± 12.4%
  branch_misses      6.98M  ± 41.8K     6.93M  … 7.02M           0 ( 0%)        ⚡- 75.6% ±  0.1%

Бенчмарк декомпрессии по сравнению с предыдущей версией:

Benchmark 1 (113 runs): sh -c ./std-inflate-old<sample.gz
  measurement          mean ± σ            min … max           outliers         delta
  wall_time          44.1ms ±  474us    43.3ms … 46.0ms         12 (11%)        0%
  peak_rss           5.48MB ±  112KB    5.23MB … 5.70MB          0 ( 0%)        0%
  cpu_cycles          194M  ±  487K      193M  …  197M           5 ( 4%)        0%
  instructions        459M  ±  524       459M  …  459M           7 ( 6%)        0%
  cache_references   1.90M  ± 46.2K     1.80M  … 2.18M           7 ( 6%)        0%
  cache_misses       38.1K  ± 3.95K     33.8K  … 65.1K           7 ( 6%)        0%
  branch_misses      3.16M  ± 3.87K     3.15M  … 3.18M           4 ( 4%)        0%
Benchmark 2 (126 runs): sh -c ./std-inflate-new<sample.gz
  measurement          mean ± σ            min … max           outliers         delta
  wall_time          39.9ms ±  662us    38.2ms … 42.3ms          4 ( 3%)        ⚡-  9.5% ±  0.3%
  peak_rss           5.47MB ±  104KB    5.18MB … 5.65MB          0 ( 0%)          -  0.1% ±  0.5%
  cpu_cycles          173M  ±  241K      173M  …  175M           4 ( 3%)        ⚡- 10.6% ±  0.0%
  instructions        410M  ±  321       410M  …  410M           2 ( 2%)        ⚡- 10.7% ±  0.0%
  cache_references   1.84M  ± 38.7K     1.71M  … 2.09M           3 ( 2%)        ⚡-  2.9% ±  0.6%
  cache_misses       36.2K  ± 1.61K     33.1K  … 40.8K           1 ( 1%)        ⚡-  4.9% ±  2.0%
  branch_misses      2.58M  ± 3.36K     2.58M  … 2.59M           0 ( 0%)        ⚡- 18.3% ±  0.0%

Источник

# Расширенная поддержка обработки сбоев/размотки стека

На всех целевых платформах, реально используемых с Zig (и, вероятно, даже на нескольких, которые не используются), теперь реализованы рабочие трассировки стека при сбоях и при использовании DebugAllocator.

Кроме того, на Windows при выводе трассировки стека теперь разрешаются inline-вызывающие из отладочной информации. Если отладочная информация неоднозначна, выводятся все возможные кандидаты. Планируется поддержка разрешения inline-трасс из DWARF. Windows была приоритетной, так как изначально PDB ассоциирует адреса возврата с самым внешним inline-вызывающим, что приводит к особенно плохому опыту отладки, если другие вызывающие не разрешаются. Теперь в трассировках возврата ошибок на всех платформах также отображаются inline-вызывающие.

Это часть более масштабной работы по улучшению сценариев разработки видеоигр на Zig.

# Удаление ucontext_t и связанных типов/функций

Этот тип был полезен для двух вещей:

  • Выполнение нелокального управления потоком с помощью функций ucontext.h.
  • Инспекция состояния машины в обработчике сигнала.

Первый сценарий использования не поддерживается; мы больше не предоставляем привязки к этим функциям в стандартной библиотеке. Они также устарели в POSIX и, как следствие, недоступны в musl.

Второй сценарий использования валиден, но очень плохо обслуживается стандартной библиотекой. Как показывают изменения в std.debug.cpu_context.signal_context_t в этом выпуске, пользователям будет лучше реализовать собственные типы ucontext_t и особенно mcontext_t, которые подходят под их конкретную ситуацию. Кроме того, эти типы часто меняются по мере развития архитектур, а стандартная библиотека не успевала за изменениями и даже не предоставляла их для всех поддерживаемых целей.

# Переработка отладочной информации

Zig 0.16.0 перерабатывает многие API стандартной библиотеки, связанные с отладочной информацией, и в частности — с трассировкой стека. Мотивация изменений заключалась в том, чтобы обеспечить быструю трассировку стека (без необходимости проверять каждый фрейм стека на недопустимые адреса памяти) без риска аварийного завершения в случаях, когда указатели фреймов недоступны (например, если libc скомпилирована с -fomit-frame-pointer).

Это удивительно сложная задача. Для её решения требуется «информация для размотки» (unwind information), которая кодируется по-разному на разных платформах. Стандартная библиотека Zig уже поддерживала использование информации для размотки, но эта поддержка была глючной, неполной и часто страдала от низкой производительности. В Zig 0.16.0 стандартная библиотека всегда будет использовать «безопасную» размотку стека по умолчанию, если она доступна, а влияние на производительность (по сравнению с наивной размоткой по указателю фрейма) обычно приемлемо.

Интерфейс для вывода std.builtin.StackTrace — это std.debug.writeStackTrace:

/// Записывает ранее захваченную трассировку стека в `t`, с аннотацией исходных позиций.
pub fn writeStackTrace(st: *const StackTrace, t: Io.Terminal) Writer.Error!void { ... }

Для отладочных целей также есть std.debug.dumpStackTrace, который пишет в stderr, а не принимает std.Io.Terminal.

Чтобы захватить текущий стек вызовов в значение std.builtin.StackTrace, используйте std.debug.captureCurrentStackTrace, которая также принимает опции для управления поведением сбора трассировки:

pub const StackUnwindOptions = struct {
    /// Если не `null`, мы будем игнорировать все фреймы до этого адреса возврата. Обычно
    /// используется для исключения промежуточного кода обработки (например, обработчика паники и его механизмов)
    /// из трассировок стека.
    first_address: ?usize = null,
    /// Если не `null`, размотка будет выполняться из этого `cpu_context.Native` вместо текущего верха стека.
    /// Основной сценарий использования — вывод трассировок стека из обработчиков сигналов, где ядро предоставляет
    /// `*const cpu_context.Native` состояния до сигнала.
    context: ?CpuContextPtr = null,
    /// Если `true`, используются стратегии размотки стека, которые могут привести к сбоям, как последнее средство.
    /// Если `false`, будут предприняты только заведомо безопасные механизмы.
    allow_unsafe_unwind: bool = false,
};

/// Захватывает и возвращает текущий стек вызовов. Возвращаемый `StackTrace` хранит свои адреса в переданном буфере,
/// поэтому время жизни `addr_buf` должно быть не меньше времени жизни `StackTrace`.
///
/// См. также writeCurrentStackTrace для немедленного вывода трассировки вместо захвата.
pub noinline fn captureCurrentStackTrace(options: StackUnwindOptions, addr_buf: []usize) StackTrace { ... }

Наконец, для печати текущего стека вызовов есть аналоги writeStackTrace и dumpStackTrace:

/// Записывает текущий стек вызовов в `t`, с аннотацией исходных позиций.
///
/// См. также captureCurrentStackTrace для захвата адресов трассировки в буфер вместо вывода.
pub noinline fn writeCurrentStackTrace(options: StackUnwindOptions, t: Io.Terminal) Writer.Error!void { ... }
/// Тонкая обёртка вокруг writeCurrentStackTrace, которая пишет в stderr и игнорирует ошибки записи.
pub fn dumpCurrentStackTrace(options: StackUnwindOptions) void { ... }

Большинство этих функций уже существовало в предыдущих версиях Zig (хотя и с другими сигнатурами), но раньше было также несколько дополнительных функций, которые теперь консолидированы в перечисленные выше. Вот API, который вам нужен, если вы использовали одну из удалённых функций:

  • captureStackTrace ➡️ captureCurrentStackTrace
  • dumpStackTraceFromBase ➡️ dumpCurrentStackTrace
  • walkStackWindows ➡️ captureCurrentStackTrace
  • writeStackTraceWindows ➡️ writeCurrentStackTrace

std.debug.StackIterator теперь считается внутренним API и больше не является pub. Если вы ранее использовали его, подумайте, подходит ли вам captureCurrentStackTrace. Если по какой-то причине это не так, обратите внимание на API, предоставляемый std.debug.SelfInfo — это абстракция стандартной библиотеки над отладочной информацией платформы.

Реализация std.debug.SelfInfo может быть переопределена путём экспорта @import("root").debug.SelfInfo. Это позволяет сделать трассировки стека функциональными даже на платформах, которые не поддерживаются стандартной библиотекой Zig, включая freestanding-цели!

# Отчёт о ходе выполнения между процессами для Windows

Теперь std.Progress поддерживает передачу информации от дочерних процессов на Windows.

Максимальная длина узла увеличена с 40 до 120.

# Работа с сетью в Windows без ws2_32.dll

Весь сетевой API на Windows теперь реализован через прямой доступ к AFD.

Это исправляет ряд ошибок, обеспечивает корректную работу отмены и группировки для сетевых операций, а также позволяет избежать проблем с производительностью, существующих в реализации ws2_32.dll, например, ненужного хранения хеш-таблицы для дополнительных данных, прикреплённых к дескрипторам сокетов, что требует выделения памяти и синхронизации, вместо простой передачи режима сокета и протокола в функцию accept.

# Завершён переход на NtDll

В Windows вся функциональность стандартной библиотеки теперь реализована на основе вызовов самого низкого стабильного системного API. Оставшиеся внешние функции в стандартной библиотеке, которые вызывают функции из DLL Windows:

extern "kernel32" fn CreateProcessW(
extern "crypt32" fn CertOpenStore(
extern "crypt32" fn CertCloseStore(
extern "crypt32" fn CertEnumCertificatesInStore(
extern "crypt32" fn CertFreeCertificateContext(
extern "crypt32" fn CertAddEncodedCertificateToStore(
extern "crypt32" fn CertOpenSystemStoreW(
extern "crypt32" fn CertGetCertificateChain(
extern "crypt32" fn CertFreeCertificateChain(
extern "crypt32" fn CertVerifyCertificateChainPolicy(

Это позволяет избежать ошибок, проблем с производительностью и недостающей функциональности на Windows, делая программы на Zig более надёжными, компактными и быстрыми по сравнению с другими языками программирования для этой платформы.

Особенно стоит отметить, что API группировки и отмены теперь полностью поддерживаются в Windows с эффективными реализациями благодаря этим изменениям.

Пользователям, желающим поддерживать старые версии Windows, такие как XP, или предпочитающим, чтобы их приложения использовали высокоуровневые DLL, такие как kernel32, рекомендуется сотрудничать над сторонней реализацией ввода-вывода, которая не использует NtDll.

Планы по отказу от использования перечисленных выше функций отсутствуют.

# «Juicy Main»

Начиная с Zig 0.16.0, добавляя параметр process.Init в функцию main, вы получаете доступ к следующим значениям:

/// Стандартный набор предварительно инициализированных полезных API, которые
/// программы могут использовать. Это тип первого параметра функции main.
/// Приложения, желающие большей гибкости, могут принимать Init.Minimal.
pub const Init = struct {
    /// Init — это надмножество Minimal; последнее включено сюда.
    minimal: Minimal,
    /// Постоянное хранилище для всего процесса, автоматически очищаемое при
    /// выходе. Потокобезопасно.
    arena: *std.heap.ArenaAllocator,
    /// Общий аллокатор по умолчанию для временных выделений в куче. В режиме
    /// отладки будет настроена проверка утечек, если возможно.
    /// Потокобезопасно.
    gpa: Allocator,
    /// Подходящая реализация Io по умолчанию, основанная на конфигурации
    /// целевой платформы. В режиме отладки будет настроена проверка утечек,
    /// если возможно.
    io: Io,
    /// Переменные окружения, инициализированные с помощью gpa. Не потокобезопасно.
    environ_map: *Environ.Map,
    /// Именованные файлы, предоставленные родительским процессом. В основном
    /// полезно на WASI, но может использоваться и на других системах для
    /// имитации поведения относительно stdio.
    preopens: Preopens,

    /// Альтернатива Init в качестве первого параметра функции main.
    pub const Minimal = struct {
        /// Переменные окружения.
        environ: Environ,
        /// Аргументы командной строки.
        args: Args,
    };
};

Пример использования:

juice.zig
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.log.info("{d} env vars", .{init.environ_map.count()});
}

Shell

$ zig build-exe juice.zig
$ ./juice i like cheese
Hello, world!
info: arg[0] = ./juice
info: arg[1] = i
info: arg[2] = like
info: arg[3] = cheese
info: 97 env vars

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

  • Отсутствует. Пустой список параметров main по-прежнему допустим, однако теперь это означает, что вы не сможете получить доступ к аргументам командной строки или переменным окружения.
  • process.Init.Minimal. Доступны только argv и environ в сыром виде.
  • process.Init. Предоставляет множество предварительно инициализированных удобств.

Рассматривается дополнительное улучшение — добавление разбора аргументов командной строки в качестве второго параметра, однако есть несколько конкурирующих идей относительно наилучшего способа реализации этого.

# Переменные окружения и аргументы процесса больше не являются глобальными

«Окружение» (набор сопоставлений «ключ-значение» строк, наследуемых дочерними процессами), будучи глобальным состоянием, хоть и является очень распространённой абстракцией, создаёт проблемы. В C небезопасно вызывать функции модификации окружения, такие как setenv, в многопоточном контексте, потому что environ может (и часто так и происходит) напрямую использоваться без какой-либо блокировки. Кроме того, в стандартной библиотеке Zig была крупная «ловушка»: std.os.environ должен был быть эквивалентен C-шному environ, но его невозможно было заполнить в библиотеке, которая не линкует libc.

Теперь переменные окружения доступны только в главной функции приложения. Поэтому функции, которым нужен доступ к переменным окружения, должны принимать параметры для нужных значений или параметр *const process.Environ.Map. Экземпляр этой карты переменных окружения можно удобно получить из «Juicy Main».

# Доступ к переменным окружения:

example.zig
const std = @import("std");

pub fn main(init: std.process.Init) !void {
    for (init.environ_map.keys(), init.environ_map.values()) |key, value| {
        std.log.info("env: {s}={s}", .{ key, value });
    }
}

# Доступ к переменным окружения (минимальный вариант):

example.zig
const std = @import("std");

pub fn main(init: std.process.Init.Minimal) !void {
    var arena_allocator: std.heap.ArenaAllocator = .init(std.heap.page_allocator);
    defer arena_allocator.deinit();
    const arena = arena_allocator.allocator();

    std.log.info("contains HOME: {any}", .{init.environ.contains(arena, "HOME")});
    std.log.info("contains HOME (unempty): {any}", .{init.environ.containsUnempty(arena, "HOME")});
    std.log.info("contains EDITOR: {any}", .{init.environ.containsConstant("EDITOR")});
    std.log.info("contains EDITOR (unempty): {any}", .{init.environ.containsConstant("EDITOR")});

    std.log.info("HOME: {?s}", .{init.environ.getPosix("HOME")});
    std.log.info("EDITOR: {s}", .{try init.environ.getAlloc(arena, "EDITOR")});

    const environ_map = try init.environ.createMap(arena);

    for (environ_map.keys(), environ_map.values()) |key, value| {
        std.log.info("env: {s}={s}", .{ key, value });
    }
}

# Доступ к аргументам командной строки (iterate):

example.zig
const std = @import("std");

pub fn main(init: std.process.Init.Minimal) void {
    var args = init.args.iterate();
    while (args.next()) |arg| {
        std.log.info("arg: {s}", .{arg});
    }
}

# Доступ к аргументам командной строки (toSlice):

example.zig
const std = @import("std");

pub fn main(init: std.process.Init) !void {
    const args = try init.minimal.args.toSlice(init.arena.allocator());
    for (args) |arg| {
        std.log.info("arg: {s}", .{arg});
    }
}

# mem: введены функции cut; переименование «index of» в «find»

  1. Введены функции cut: cut, cutPrefix, cutSuffix, cutScalar, cutLast, cutLastScalar.
  2. Движение в сторону соглашения об именовании функций: одно слово на одну концепцию и построение имён функций из конкатенации понятий.

В std.mem используются следующие концепции:

  • «find» — возвращает индекс подстроки.
  • «pos» — параметр начального индекса.
  • «last» — поиск с конца.
  • «linear» — простой цикл for вместо сложного алгоритма.
  • «scalar» — подстрока является одним элементом.

# Выборочный обход деревьев каталогов

std.Io.Dir.walk можно использовать для рекурсивного обхода дерева каталогов, но он не поддерживает пропуск определённых каталогов по пути. Для поддержки этого сценария добавлена функция std.Io.Dir.walkSelectively, которая требует явного согласия на рекурсивный переход в каждый встреченный каталог. Такой подход позволяет избежать избыточных системных вызовов открытия/закрытия для каталогов, которые пропускаются.

Руководство по переходу, если у вас есть сценарий, где полезен выборочный обход:

var walker = try dir.walk(gpa);
defer walker.deinit();

while (try walker.next(io)) |entry| {
    // ...
}

⬇️

var walker = try dir.walkSelectively(gpa);
defer walker.deinit();

while (try walker.next(io)) |entry| {
    // некая фильтрация
    if (failsFilter(entry)) continue;
    if (entry.kind == .directory) {
        try walker.enter(io, entry);
    }
    // ...
}

Кроме того, в Walker.Entry добавлена функция depth, а в Walker и SelectiveWalker — функции leave, чтобы можно было досрочно прекратить обход определённого каталога.

# fs.path: пути Windows

Все функции в std.fs.path теперь корректнее и согласованнее обрабатывают пути Windows, особенно в отношении UNC, «корневых» (rooted) и относительных к диску типов путей. Это включает изменения в поведении многих функций, подробности см. в #25993.

Изменения API:

  • windowsParsePath / diskDesignator / diskDesignatorWindows ➡️ parsePath, parsePathWindows, parsePathPosix
  • Добавлена getWin32PathType
  • componentIterator / ComponentIterator.init больше не могут завершаться ошибкой

# fs.path.relative стал чистой функцией

Функции relative, relativeWindows и relativePosix теперь являются чистыми и требуют передачи пути к текущему рабочему каталогу (CWD) и (опционально) карты окружения в качестве входных данных, вместо внутреннего запроса этой информации у ОС (карта окружения нужна для разрешения определённых типов путей на Windows).

Руководство по переходу:

const relative = try std.fs.path.relative(gpa, from, to);
defer gpa.free(relative);

⬇️

const cwd_path = try std.process.currentPathAlloc(io, gpa);
defer gpa.free(cwd_path);

const relative = try std.fs.path.relative(gpa, cwd_path, environ_map, from, to);
defer gpa.free(relative);

# File.Stat: сделать время доступа (atime) необязательным

Файловые системы обычно считают это значение проблематичным для постоянного обновления, поскольку оно превращает доступ только для чтения в изменение файловой системы. Некоторые системы сообщают устаревшие значения, а некоторые системы явно отказываются сообщать это значение. Последний случай теперь обрабатывается с помощью null.

Было замечено, что ZFS не сообщает atime из statx.

Также была использована возможность сделать API установки временных меток более гибким и привести его в соответствие с широко доступными API, которые имеют константы UTIME_OMIT и UTIME_NOW, которые можно независимо устанавливать для обоих полей.

Это необходимо для плавной обработки случая, когда atime равно null.

# Руководство по переходу:

try atomic_file.file_writer.file.setTimestamps(io, src_stat.atime, src_stat.mtime);

⬇️

try atomic_file.file_writer.file.setTimestamps(io, .{
    .access_timestamp = .init(src_stat.atime),
    .modify_timestamp = .init(src_stat.mtime),
});

Для доступа к Io.File.Stat.atime:

stat.atime

⬇️

stat.atime orelse return error.FileAccessTimeUnavailable

# «Preopens»

# Руководство по переходу:

const wasi_preopens: std.fs.wasi.Preopens = try .preopensAlloc(arena);

⬇️

const preopens: std.process.Preopens = try .init(arena);

Или просто получить их из «Juicy Main» через std.Process.Init.preopens.

Данные имеют тип void в системах, отличных от WASI; вы не платите за это, если не используете. Однако этот API рассчитан на будущее на случай, если другие операционные системы добавят эквивалентную функциональность.

# Атомарные/временные файлы

Основной мотивацией для этого изменения было перенести вызов std.crypto.random ниже std.Io.VTable. В частности, тот, что в std.Io.File.Atomic.init.

Одновременно я воспользовался возможностью интегрировать это с O_TMPFILE в Linux. Хотелось бы воспользоваться случаем и пожаловаться на этот API. Во-первых, он почти очень хорош. Он даёт возможность создать эфемерный, безымянный файловый дескриптор, с которым можно свободно работать до тех пор, пока не потребуется материализовать его в файловой системе. Если процесс завершается до того, как это произойдёт, ОС собирает файл как мусор, а не оставляет после себя временные небезопасные файлы. Блестяще! К сожалению, из-за множества ошибок и ограничивающего недостатка в дизайне этот API почти бесполезен.

Во-первых, O_TMPFILE разбит на 2 бита на некоторых архитектурах и отсутствует на других. Что за чушь? Впрочем, это не настоящая проблема, идём дальше.

При использовании O_TMPFILE, как бы вы догадались, что openat() сообщает о том, что файловая система не поддерживает эту операцию? Может быть, с помощью ENOSYS? Или, возможно, OPNOTSUPP? Универсальный EINVAL? Это были бы два очень разумных предположения и приемлемое третье, однако — неверно! Возвращается либо EISDIR, либо ENOENT. Напоминаю, речь идёт о openat(), поэтому нам очень важно знать, не существует ли путь или не работает механизм временных файлов.

Далее — отсутствует API. linkat() не поддерживает флаг AT_REPLACE, хотя был патч для этого, поданный почти 10 лет назад, который был вполне рабочим. Линус сказал, что всё в порядке, а потом его просто так и не смержили. Без этого флага O_TMPFILE нельзя использовать для атомарной перезаписи существующего файла. Это значит, что если вы хотите это сделать, придётся создать обычный непостоянный файл со случайными числами или чем-то подобным, а затем использовать renameat().

Таким образом, единственное время, когда трюк с O_TMPFILE действительно приносит пользу — если вам нужны семантики жёсткой ссылки, то есть вы хотите получить error.PathAlreadyExists, когда путь назначения уже существует. Подумайте: если вы удалите файл, чтобы освободить место, это уже не будет атомарно!

Ладно, хватит ворчать.

В любом случае, суть в том, что как только любая ОС исправит свои паршивые API в отношении временных файлов, мы сможем соответствующим образом изменить std.Io.Threaded, и весь код Zig, использующий std.Io, сможет оставаться неизменным и прозрачно получить эти преимущества.

Наконец, в этой ветке представлен API std.Io.File.hardLink, который работает только в Linux и необходим для материализации файлового дескриптора O_TMPFILE без семантики замены.

# Руководство по переходу:

var buffer: [1024]u8 = undefined;
var atomic_file = try dest_dir.atomicFile(io, dest_path, .{
    .permissions = actual_permissions,
    .write_buffer = &buffer,
});
defer atomic_file.deinit();

// do something with atomic_file.file_writer;

try atomic_file.flush();
try atomic_file.renameIntoPlace();

⬇️

var atomic_file = try dest_dir.createFileAtomic(io, dest_path, .{
    .permissions = actual_permissions,
    .make_path = true,
    .replace = true,
});
defer atomic_file.deinit(io);

var buffer: [1024]u8 = undefined; // Используется только когда прямой fd-to-fd недоступен.
var file_writer = atomic_file.file.writer(io, &buffer);

// do something with file_writer

try file_writer.flush();
try atomic_file.replace(io); // или установите .replace = false выше и вызовите link() вместо этого

# API блокировки и защиты памяти перенесён в process

Флаги mmap и mprotect теперь имеют типобезопасность:

std.posix.PROT.READ | std.posix.PROT.WRITE,

⬇️

.{ .READ = true, .WRITE = true },

mlock, mlock2, mlockall:

try std.posix.mlock();
try std.posix.mlock2(slice, std.posix.MLOCK_ONFAULT);
try std.posix.mlockall(slice, std.posix.MCL_CURRENT|std.posix.MCL_FUTURE);

⬇️

try std.process.lockMemory(slice, .{});
try std.process.lockMemory(slice, .{.on_fault = true});
try std.process.lockMemoryAll(.{ .current = true, .future = true });

# API текущего каталога переименован

В стандартной библиотеке Zig Dir означает открытый дескриптор каталога. path представляет собой строку-идентификатор файловой системы. Эта функция лучше названа как «текущий путь», а не «текущий каталог». Слова «get» и «working» избыточны.

# Руководство по переходу:

std.process.getCwd(buffer)
std.process.getCwdAlloc(allocator)

⬇️

std.process.currentPath(io, buffer)
std.process.currentPathAlloc(io, allocator)

# Миграция на «неуправляемые» контейнеры

В прошлом стандартная библиотека Zig предлагала два варианта динамически растущих структур данных: один с полем Allocator в структуре («управляемый»), другой, где Allocator должен передаваться в каждый метод, которому он нужен («неуправляемый»).

Со временем программисты Zig пришли к выводу, что вариант без поля аллокатора более универсален, а другой следует удалить. Теперь, когда остался только один вариант, больше нет необходимости в этом расплывчатом слове «управляемый» для их различения. В этом выпуске несколько API сделали шаги по миграции:

  • Добавлены heap.MemoryPoolUnmanaged, heap.MemoryPoolAlignedUnmanaged, heap.MemoryPoolExtraUnmanaged (#23234)
  • PriorityDequeue больше не имеет поля Allocator.
  • PriorityQueue больше не имеет поля Allocator.
  • Удалены ArrayHashMap, AutoArrayHashMap, StringArrayHashMap.
  • AutoArrayHashMapUnmanaged ➡️ array_hash_map.Auto
  • StringArrayHashMapUnmanaged ➡️ array_hash_map.String
  • ArrayHashMapUnmanaged ➡️ array_hash_map.Custom

# PriorityDequeue

Изменения следуют за Deque:

  • Методы, содержащие add, переименованы в push, а remove — в pop.
  • popMinOrNull и popMaxOrNull объединены в popMin и popMax соответственно (без потери функциональности).
  • Значения полей по умолчанию инициализируются с помощью константы .empty, а не метода init().

# Руководство по переходу:

  • init ➡️ .empty
  • add ➡️ push
  • addSlice ➡️ pushSlice
  • addUnchecked ➡️ pushUnchecked
  • removeMinOrNull ➡️ popMin
  • removeMin ➡️ popMin
  • removeMaxOrNull ➡️ popMax
  • removeMax ➡️ popMax
  • removeIndex ➡️ popIndex

# PriorityQueue

Очередь с приоритетом со значениями полей по умолчанию может быть инициализирована с помощью .empty.

Например, очередь с приоритетом может быть использована для инициализации min- и max-кучи с функцией сравнения так:

min_heap.zig
fn lessThan(context: void, a: u32, b: u32) Order {
    _ = context;
    return std.math.order(a, b);
}

const MinHeap = std.PriorityQueue(u32, void, lessThan);

var queue: MinHeap = .empty;
max_heap.zig
fn greaterThan(context: void, a: u32, b: u32) Order {
    _ = context;
    return std.math.order(a, b).invert();
}

const MaxHeap = std.PriorityQueue(u32, void, greaterThan);

var queue: MaxHeap = .empty;

# Руководство по переходу:

  • init ➡️ initContext
  • add ➡️ push
  • addUnchecked ➡️ pushUnchecked
  • addSlice ➡️ pushSlice
  • remove ➡️ pop
  • removeOrNull ➡️ pop
  • removeIndex ➡️ popIndex

# Thread.Pool удалён

Реализация пула потоков, ранее находившаяся в std.Thread.Pool, была удалена в Zig 0.16.0 в пользу примитивов многозадачности в новом интерфейсе std.Io.

Использования std.Thread.Pool.spawnWg следует заменить вызовами к std.Io.async или std.Io.Group.async, однако обратите внимание, что это предполагает, что задача не должна синхронизироваться с вызывающим кодом (то есть, предполагается, что новая задача асинхронна по отношению к вызывающему). Например, миграция может выглядеть так:

/// Выполняет много работы в пуле и возвращает результат после завершения всей работы.
fn doAllTheWork(pool: *std.Thread.Pool) void {
    var wg: std.Thread.WaitGroup = .{};
    pool.spawnWg(wg, doSomeWork, .{ pool, &wg, first_work_item });
    wg.wait();
}

/// Выполняет часть работы и потенциально добавляет одну или несколько новых задач в пул.
fn doSomeWork(pool: *std.Thread.Pool, wg: *std.Thread.WaitGroup, foo: Foo) void {
    foo.doTheThing();
    for (foo.new_work_items) |new| {
        pool.spawnWg(wg, doSomeWork, .{ pool, wg, new });
    }
}

⬇️

/// Выполняет много работы в группе и возвращает результат после завершения всей работы.
fn doAllTheWork(io: std.Io) void {
    var g: std.Io.Group = .init;

    // Хотя doAllTheWork не может завершиться ошибкой в этом случае,
    // всё же может быть хорошей идеей сделать так,
    // чтобы не допустить ошибку при изменении doAllTheWork:
    errdefer g.cancel(io);

    g.async(io, doSomeWork, .{ io, &g, first_work_item });
    g.wait(io);
}

/// Выполняет одну единицу работы и потенциально добавляет одну или несколько новых задач в группу.
fn doSomeWork(io: std.Io, g: *std.Io.Group, foo: Foo) void {
    foo.doTheThing();
    for (foo.new_work_items) |new| {
        g.async(io, doSomeWork, .{ io, g, new });
    }
}

Обратите внимание, что при переходе с std.Thread.Pool на std.Io для корректности требуется преобразовать любые примитивы синхронизации потоков (Thread.Mutex, Thread.Condition, Thread.ResetEvent и т. д.) в их эквиваленты из Io (Io.Mutex, Io.Condition, Io.Event).

Для сложных сценариев использования пула потоков (где две или более задачи должны как-то синхронизироваться), async может не подойти: обратитесь к документации для std.Io.async и std.Io.concurrent для получения дополнительной информации.

# Удалить builtin.subsystem

Обнаружение подсистемы было ненадёжным и часто некорректным и фактически не требовалось компилятору или стандартной библиотеке. Фактическая подсистема не будет известна до времени линковки, поэтому нет смысла пытаться определить её во время компиляции.

Удаление std.builtin.subsystem — это ломающее изменение, но маловероятно, что многие пользователи вообще его использовали. Если вашему коду абсолютно необходимо знать подсистему — есть способы определить её во время выполнения.

# Переместить Target.SubSystem в zig.Subsystem и обновить имена полей

std.zig — это место для опций вроде SanitizeC или LtoMode, поэтому оно подходит. std.Target.SubSystem остаётся как устаревший псевдоним, а старые имена полей остаются как устаревшие объявления, чтобы не ломать скрипты build.zig, например exe.subsystem = .Windows.

# Io: удаление GenericReader, AnyReader, FixedBufferStream

# Руководство по переходу:

  • std.io ➡️ std.Io
  • std.Io.GenericReader ➡️ std.Io.Reader
  • std.Io.AnyReader ➡️ std.Io.Reader
  • std.leb.readUleb128 ➡️ std.Io.Reader.takeLeb128
  • std.leb.readIleb128 ➡️ std.Io.Reader.takeLeb128

# FixedBufferStream (чтение)

var fbs = std.io.fixedBufferStream(data);
const reader = fbs.reader();

⬇️

var reader: std.Io.Reader = .fixed(data);

# FixedBufferStream (запись)

var fbs = std.io.fixedBufferStream(buffer);
const writer = fbs.writer();

⬇️

var writer: std.Io.Writer = .fixed(buffer);

# fs.getAppDataDir удалён

Этот API был слишком субъективным для стандартной библиотеки Zig. Приложения должны содержать эту логику самостоятельно. Пользователи могут рассмотреть сторонний пакет known-folders в качестве альтернативы.

# Io.Writer.Allocating: поле Alignment

В этом API теперь есть новое поле:

alignment: std.mem.Alignment,

Это значение выравнивания, известное во время выполнения. API аллокатора поддерживает это, если вы используете варианты функций «raw».

# fs.Dir.readFileAlloc

const contents = try std.fs.cwd().readFileAlloc(allocator, file_name, 1234);

⬇️

const contents = try std.Io.Dir.cwd().readFileAlloc(io, file_name, allocator, .limited(1234));

Обратите внимание, что лимит теперь работает иначе: если он достигнут, также возвращается ошибка. Кроме того, ошибка изменена с FileTooBig на StreamTooLong.

# fs.File.readToEndAlloc

const contents = try file.readToEndAlloc(allocator, 1234);

⬇️

var file_reader = file.reader(&.{});
const contents = try file_reader.interface.allocRemaining(allocator, .limited(1234));

# std.crypto: добавлены AES-SIV и AES-GCM-SIV

В стандартной библиотеке Zig отсутствовали схемы, устойчивые к повторному использованию nonce.

AES-SIV и AES-GCM-SIV — это стандартные решения для этой задачи.

AES-GCM-SIV особенно полезен, когда Zig нацелен на встраиваемые системы, а AES-SIV особенно ценен для обёртывания ключей.

# std.crypto: добавлены Ascon-AEAD, Ascon-Hash, Ascon-CHash

Ascon — это семейство криптографических конструкций, стандартизированных NIST для облегчённой криптографии.

Стандартная библиотека Zig уже включала саму перестановку Ascon, но высокоуровневые конструкции на её основе были намеренно отложены до публикации финальной спецификации NIST.

Эта спецификация теперь опубликована как NIST SP 800-232.

С этой публикацией мы можем уверенно включить эти конструкции в стандартную библиотеку.

# Система сборки

# Некатегоризированные изменения:

  • std.Build.Step.ConfigHeader: обработка ведущих пробелов для cmake

# Возможность локального переопределения пакетов

Введён новый флаг zig build:

zig build --fork=[path]

Это опция переопределения проекта. Указанный путь содержит файл build.zig.zon с полями name и fingerprint. Каждый раз, когда дерево зависимостей должно разрешить пакет с совпадающими name и fingerprint, вместо этого разрешается переопределение по всему дереву, полностью игнорируя version. Это происходит до возможной загрузки пакета. Поэтому если у вас нет интернета, вы забыли выполнить fetch, но у вас есть репозиторий git, вы можете разблокировать сборку одной командой CLI.

Это простой способ временно использовать одну или несколько вилок (forks), находящихся в совершенно разных каталогах. Можно итерировать по всему дереву зависимостей до тех пор, пока всё не заработает, удобно используя среду разработки и систему контроля версий проектов-зависимостей.

Тот факт, что это флаг CLI, делает его подходящим для временного использования. Как только вы убираете флаги, вы возвращаетесь к использованию исходного дерева зависимостей.

Если проект не совпадает, происходит ошибка, предотвращая путаницу:

$ zig build --fork=/home/andy/dev/mime
error: fork /home/andy/dev/mime matched no mime packages
$

Если проект совпадает, вы получаете напоминание о том, что используете вилку (fork), предотвращая путаницу:

$ zig build --fork=/home/andy/dev/dvui
info: fork /home/andy/dev/dvui matched 1 (dvui) packages
...

Эта функциональность предназначена для улучшения рабочего процесса при сбоях в экосистеме.

Эта функция зависит от нового формата хеша; поэтому поддержка старого формата хеша удалена.

# Загрузка пакетов в локальный каталог проекта

Вместо того чтобы загружаться в $GLOBAL_ZIG_CACHE/p/$HASH, зависимости пакетов теперь загружаются в каталог «zig-pkg» относительно корня сборки (рядом с build.zig). Пользователям обычно рекомендуется не коммитить эти файлы в систему контроля версий, однако понятно, что некоторые будут делать это для удобства.

После загрузки пакета применяются фильтры (поле paths в build.zig.zon) для удаления файлов, не входящих в хеш, а затем пакет повторно архивируется в канонический $GLOBAL_ZIG_CACHE/p/$HASH.tar.gz, чтобы избежать повторного скачивания при необходимости того же пакета.

Мотивация этого изменения — облегчить эксперименты. Смело редактируйте эти файлы, смотрите, что получится. Замените каталог пакета на git-клон. Ищите по всем зависимостям сразу. Настройте свою IDE на автодополнение на основе каталога zig-pkgs. Запустите baobab на дереве зависимостей. Кроме того, наличие сжатых файлов в глобальном кэше облегчает обмен этими кэшированными данными между компьютерами.

zig build теперь будет завершаться с ошибкой при обнаружении зависимостей пакетов без поля fingerprint или с name в виде строки, а не литерала перечисления. Fingerprint необходим, чтобы определить, что два пакета с разными версиями предназначены для разных версий одного и того же проекта. Будет ошибкой иметь в дереве зависимостей одинаковый fingerprint, одинаковую версию, но разный хеш, потому что это означает, что кто-то забыл увеличить номер версии, или кто-то пытается сделать враждебную вилку пакета, и теперь вам придётся выбрать сторону.

Zig больше не учитывает переменную окружения ZIG_BTRFS_WORKAROUND. Эта ошибка была исправлена в апстриме Linux уже давно ( #17095).

# Таймауты юнит-тестов

Теперь можно указать таймаут, который будет применяться ко всем отдельным юнит-тестам Zig (то есть блокам test). С помощью флага --test-timeout к zig build можно указать значение таймаута, по истечении которого система сборки принудительно завершит текущий юнит-тест (путём завершения и перезапуска тестового процесса) и перейдёт к следующему.

Например, запуск zig build test --test-timeout 500ms выполнит шаг с именем test, за исключением случаев, когда отдельный юнит-тест Zig не завершится в течение 500 мс реального времени — в этом случае тест будет прерван и выведена ошибка:

$ zig build test --test-timeout 500ms
test
└─ run test 1 pass, 2 timeout (3 total)
error: 'main.test.first slow test' timed out after 499.491ms
error: 'main.test.second slow test' timed out after 499.609ms
failed command: ./.zig-cache/o/6d2da140357b7fa42c69cd4b151c14ff/test --cache-dir=./.zig-cache --seed=0xb6711f5 --listen=-

Build Summary: 1/3 steps succeeded (1 failed); 1/3 tests passed (2 timed out)
test transitive failure
└─ run test 1 pass, 2 timeout (3 total)

Это полезно для обнаружения медленных тестов или тестов, которые не завершаются. Однако имейте в виду, что таймауты указываются в реальном времени, а не в процессорном, поэтому при высокой нагрузке на систему стресс планировщика может вызвать неожиданные таймауты.

# Добавлен флаг --error-style

Новый CLI-флаг --error-style у zig build позволяет настраивать способ записи сообщений об ошибках от шагов сборки в stderr. Стиль по умолчанию, verbose, будет выводить полный контекст, включая релевантное дерево зависимостей шага, показывающее, почему этот шаг собирается, и неудачные команды, где это применимо. В качестве альтернативы можно указать стиль minimal, чтобы опустить эти части информации в пользу простого вывода имени неудачного шага и сообщения об ошибке.

Кроме того, доступны ещё два стиля ошибок: verbose_clear и minimal_clear. Они похожи на verbose и clear соответственно, но при использовании с --watch они будут очищать терминал при пересборке, вызванной изменением входного файла. Эти режимы особенно полезны, если вы используете инкрементальную компиляцию.

Если флаг --error-style не указан, система сборки также проверит переменную окружения ZIG_BUILD_ERROR_STYLE, и если она присутствует, будет использовать это значение. Это позволяет глобально указать предпочитаемый режим, установив переменную окружения в конфигурации вашей оболочки.

Этот флаг заменяет флаг --prominent-compile-errors, который был удалён. Если вы ранее использовали --prominent-compile-errors, эквивалент в Zig 0.16.x — это --error-style minimal.

# Добавлен флаг --multiline-errors

Новый CLI-флаг --multiline-errors у zig build управляет тем, как система сборки печатает ошибки, которые занимают несколько строк. Доступны варианты: indent (новый по умолчанию), newline, и none:

error: this is how the "indent" style looks when an error message
       spans multiple lines. every line other than the first is
       indented to align with the first line.

error:
this is how the "newline" style looks when an error message
spans multiple lines. an extra newline is added before the
start to align all of the lines at the first column.

error: this is how the "none" style looks when an error message
spans multiple lines. no special handling is applied, so
the first line is not aligned with the remaining lines.

Если флаг --multiline-errors не указан, система сборки также проверит переменную окружения ZIG_BUILD_MULTILINE_ERRORS, и если она присутствует, будет использовать это значение. Это позволяет глобально указать предпочитаемый режим путём установки переменной окружения в конфигурации вашей оболочки.

# API временных файлов

Шаг RemoveDir удалён без замены. Этот шаг не имел валидной цели. Мутация исходных файлов? Это должно делаться с помощью шага UpdateSourceFiles. Удаление временных каталогов? Это требовало создания временных каталогов на этапе конфигурации, что некорректно. Удаление кэшированных артефактов? Это приведёт к проблемам.

Аналогично, функция Build.makeTempPath удалена. Она использовалась для создания временного пути на этапе конфигурации, что опять же неверное место для этого.

Вместо этого шаг WriteFile был обновлён с добавлением функциональности:

tmp-режим: В этом режиме каталог будет размещён внутри «tmp», а не «o», и кэширование будет пропущено. На этапе make шаг всегда выполнит все операции с файловой системой, а по завершении успешной сборки каталог будет удалён вместе со всеми временными каталогами. Таким образом, каталог пригоден для использования для мутаций другими шагами. Build.addTempFiles вводится для инициализации шага WriteFile с этим режимом.

mutate-режим: Операции не будут выполняться против только что созданного каталога, а будут действовать против временного каталога. Build.addMutateFiles вводится для инициализации шага WriteFile с этим режимом.

Введена функция Build.tmpPath, которая является сокращением для Build.addTempFiles, за которым следует WriteFile.getDirectory.

# Руководство по переходу:

Если вы вызывали b.makeTempPath() с последующим addRemoveDirTree, теперь вы можете вызвать b.addTempFiles и использовать API std.Build.Step.WriteFile. Не нужно делать ничего другого — сборщик сам очистит временные файлы и поймёт, что их нельзя кэшировать.

# Компилятор

# Перевод C

Реализация translate-c в Zig теперь основана на arocc и translate-c, а не на libclang. Прощайте и скатертью дорога 5 940 строкам нашего оставшегося C++ кода в дереве исходников компилятора, осталось 3 763.

Реализация компилируется лениво из исходников при первом обнаружении @cImport. В будущем Zig откажется от встроенной функции языка @cImport, но пока она остаётся, поддерживаемая Aro вместо Clang.

Это шаг к переходу от зависимости библиотеки от LLVM к зависимости процесса от Clang.

Технически это изменение, не нарушающее совместимость. Хотя поломки возможны из-за замены одного C-компилятора на другой, если они произойдут — это баг, а не фича. Так что скрестите пальцы при обновлении и сообщайте о багах, если что-то сломается.

# Бэкенд LLVM

  • Экспериментальная поддержка инкрементальной компиляции
  • Уменьшение размера биткода LLVM на 3–7%
  • Немного более быстрая компиляция (~3%) в некоторых случаях
  • Исправлена отладочная информация для объединений с полезной нагрузкой нулевого размера
  • Отладочная информация теперь включает правильные имена для всех типов
  • Типы наборов ошибок теперь преобразуются в enum, чтобы имена ошибок были видны во время выполнения

Мэтью также рассмотрел изменение представления типов объединений с тегами и объединений ошибок в отладочной информации для использования вариантных типов, что позволило бы отладчикам понимать, какое поле «активно», и отображать только его. К сожалению, хотя GDB поддерживает эту функцию, LLDB — нет, и вообще не выводит поля типа при использовании вариантных типов. (Странно, но LLDB имеет частичную поддержку, но она включается только когда язык помечен как Rust). Он может вернуться к этому в будущем, если ситуация улучшится.

Мы внесли некоторые внутренние изменения, чтобы попытаться полностью распараллелить этот бэкенд, чтобы несколько потоков могли генерировать LLVM IR для разных функций, которые затем «склеиваются» потоком-«линкером». Ожидайте больше прогресса в этом направлении в будущем!

По сравнению с x86 Backend, бэкенд LLVM проходит 2004 из 2010 (100%) тестов поведения.

# Переработка понижения синтаксиса Byval

При написании самохостящегося компилятора был ранний эксперимент по попытке немного уменьшить количество промежуточных инструкций в конвейере за счёт понижения выражений с семантикой «byval». Эксперимент провалился, потому что это привело к следующим проблемам:

Теперь фронтенд понижает выражения «byref» до финальной загрузки, что исправляет все эти проблемы.

Подробнее

# Переработка разрешения типов

Zig 0.16.0 значительно перерабатывает внутреннюю логику разрешения типов в компиляторе Zig. Мотивация этого изменения заключалась в упрощении процесса написания спецификации языка Zig и устранении огромного количества багов компилятора, в частности связанных с инкрементальной компиляцией.

Новые семантики разрешения типов в целом более разрешительны, чем старое поведение. Это означает, что большинство кода, который раньше работал, продолжит работать, а некоторые примеры, которые раньше не работали (вероятно, с ошибкой «dependency loop»), теперь будут работать.

Однако новая система не строго более разрешительна. Есть определённые вещи, которые раньше принимались компилятором Zig, а теперь — нет, например:

struct_uses_own_alignment.zig
const S = struct {
    foo: [*]align(@alignOf(@This())) u8,
};

test "trigger dependency loop" {
    const val: S = .{ .foo = &.{} };
    _ = val;
}

Shell

$ zig test struct_uses_own_alignment.zig
/home/ci/.cache/act/ade7a1d6abc67e0a/hostexecutor/src/download/0.16.0/release-notes/struct_uses_own_alignment.zig:2:28: error: type 'struct_uses_own_alignment.S' depends on itself for alignment query here
    foo: [*]align(@alignOf(@This())) u8,
                           ^~~~~~~

Правила новой системы в целом более интуитивны — например, хотя приведённый выше фрагмент кода теоретически мог бы работать, также ясно, почему он может не работать. Другими словами, ошибки циклов зависимостей теперь не кажутся неразумными или совершенно неожиданными, как это часто бывало в предыдущих версиях Zig.

К сожалению, трудно дать общий совет, если вы столкнулись с ошибками циклов зависимостей, потому что подходящее решение сильно зависит от контекста. Однако Zig 0.16.0 также значительно улучшает сообщения об ошибках в ситуациях с циклами зависимостей, что должно облегчить понимание их происхождения:

complex_dependency_loop.zig
test "trigger dependency loop" {
    const val: S = .{};
    _ = val;
}

const S = struct { x: u32 = default_val };
const default_val = other_val;
const other_val = @typeInfo(S).@"struct".fields.len;

Shell

$ zig test complex_dependency_loop.zig
error: dependency loop with length 3
    /home/ci/.cache/act/ade7a1d6abc67e0a/hostexecutor/src/download/0.16.0/release-notes/complex_dependency_loop.zig:6:29: note: default field value of 'complex_dependency_loop.S' uses value of declaration 'complex_dependency_loop.default_val' here
    const S = struct { x: u32 = default_val };
                                ^~~~~~~~~~~
    /home/ci/.cache/act/ade7a1d6abc67e0a/hostexecutor/src/download/0.16.0/release-notes/complex_dependency_loop.zig:7:21: note: value of declaration 'complex_dependency_loop.default_val' uses value of declaration 'complex_dependency_loop.other_val' here
    const default_val = other_val;
                        ^~~~~~~~~
    /home/ci/.cache/act/ade7a1d6abc67e0a/hostexecutor/src/download/0.16.0/release-notes/complex_dependency_loop.zig:8:19: note: value of declaration 'complex_dependency_loop.other_val' uses default field values of 'complex_dependency_loop.S' here
    const other_val = @typeInfo(S).@"struct".fields.len;
                      ^~~~~~~~~~~~
    note: eliminate any one of these dependencies to break the loop

Если вам сложно разрешить цикл зависимостей, подумайте о присоединении к сообществу Zig для получения помощи от других пользователей Zig!

# Инкрементальная компиляция

Инкрементальная компиляция — это функция компилятора Zig, позволяющая компилировать только тот код, который был изменён с момента предыдущей сборки, благодаря чему небольшие изменения собираются за миллисекунды вместо секунд или минут. В Zig 0.16.0 поддержка этой функции была значительно улучшена.

Вот основные улучшения в этом цикле выпуска:

  • Инкрементальные обновления стали значительно быстрее за счёт устранения «избыточного анализа» (когда компилятор пересобирает больше кода, чем нужно) в подавляющем большинстве случаев. Например, при использовании инкрементальной компиляции на самом компиляторе Zig изменения, которые раньше приводили к пересборке почти всего компилятора, теперь завершаются за миллисекунды. Это стало возможным благодаря переработке разрешения типов, сделавшей внутренний граф зависимостей компилятора ацикличным (за исключением случаев циклов зависимостей).
  • Инкрементальная компиляция больше не вызывает ошибок «цикл зависимостей», которые не возникают при неинкрементальных сборках (и наоборот). Это была самая большая несогласованность между инкрементальными и неинкрементальными сборками в предыдущих релизах, и она была устранена в рамках переработки разрешения типов.
  • При использовании самохостируемого бэкенда с таргетом ELF новый ELF-линкер теперь включён по умолчанию, он быстрее и имеет гораздо более стабильную поддержку инкрементальной компиляции. Этот линкер пока не обладает полным набором функций — подробности см. в New ELF Linker.
  • Общая стабильность значительно повысилась — сбои и неправильные компиляции при инкрементальных обновлениях встречаются гораздо реже, чем в предыдущих версиях Zig.
  • Бэкенд LLVM теперь поддерживает инкрементальную компиляцию. Это не ускоряет этап «LLVM Emit Object» компиляции: этот шаг полностью зависит от LLVM, и мы мало что можем сделать для его ускорения. Однако это ускоряет построение биткода LLVM в компиляторе Zig. Это также означает, что в случаях, когда ваш код вызывает ошибки компиляции, вы можете получить практически мгновенную обратную связь даже с бэкендом LLVM (поскольку «LLVM Emit Object» пропускается при наличии ошибок компиляции).

Инкрементальная компиляция по-прежнему содержит известные баги, включая некоторые неправильные компиляции, и поэтому по умолчанию в 0.16.0 она отключена. Тем не менее, мы по-прежнему рекомендуем её включать. Пользователи часто удивляются тому, сколько времени можно сэкономить даже просто благодаря практически мгновенной обратной связи по ошибкам компиляции, не говоря уже о практически мгновенной компиляции!

Поскольку инкрементальная компиляция теперь может использоваться как с новым ELF-линкером, так и с бэкендом LLVM, её включение обычно так же просто, как запуск zig build -fincremental --watch. Эта команда запустит процесс сборки, который может обнаруживать изменения любых исходных файлов и автоматически выполнять инкрементальное обновление.

В будущих релизах продолжится работа над инкрементальной компиляцией: больше исправлений ошибок, улучшенная инфраструктура тестирования, повышение производительности и лучшая поддержка линкера.

# Бэкенд x86

  • Исправлено 11 багов.
  • Генерирует лучший код для memcpy констант ( #25353).

По сравнению с бэкендом LLVM, этот бэкенд проходит больше тестов поведения, имеет значительно более высокую скорость компиляции, превосходную отладочную информацию и худшее качество машинного кода. Он остаётся бэкендом по умолчанию при компиляции в режиме Debug.

# Бэкенд aarch64

Всё ещё в стадии разработки. Прогресс был приостановлен в этом цикле выпуска из-за изменений в I/O интерфейсе. В настоящее время падает при запуске тестов поведения. Ожидается, что прогресс возобновится по мере стабилизации изменений в стандартной библиотеке.

# WebAssembly-бэкенд

По сравнению с бэкендом LLVM, WebAssembly-бэкенд Zig проходит 1813 из 1970 (92%) тестов поведения.

# Генерация библиотек импорта из .def-файлов без LLVM

Устраняет зависимость от LLVM в отношении набора .def-файлов MinGW-w64, поставляемых с Zig. Эта реализация во многом основана на реализации LLVM (в частности, на COFFModuleDefinition.cpp и COFFImportFile.cpp).

Это шаг к переходу от зависимости библиотеки от LLVM к зависимости процесса от Clang.

# Улучшенная генерация кода для проверок безопасности циклов for

Циклы по срезам теперь генерируют примерно на 30% меньше кода.

# Линкер

# Новый ELF-линкер

Новый линкер можно использовать с помощью -fnew-linker в CLI или установив exe.use_new_linker = true в скрипте сборки. Теперь он используется по умолчанию при передаче -fincremental и таргете ELF.

Точка данных о производительности [ источник ]: сборка компилятора Zig, затем внесение однострочного изменения в функцию, а затем ещё одного:

  • Старый линкер: 14с, 194мс, 191мс
  • Новый линкер: 14с, 65мс, 64мс (на 66% быстрее)
  • Пропуск линковки вообще: 14с, 62мс, 62мс (на 68% быстрее)

Производительность достаточно высока, чтобы больше не было особого смысла в шаге сборки -Dno-bin. Вы можете просто всегда оставлять включёнными генерацию кода и линковку, потому что разница в скорости компиляции незначительна, и в итоге вы получаете исполняемый файл.

Однако этот новый линкер пока не обладает полным набором функций по сравнению со старым и с LLD. Например, исполняемые файлы, созданные таким образом, не содержат информации DWARF. Поэтому старый линкер и LLD всё ещё доступны. Когда новый линкер будет полностью функционален, старый линкер будет удалён, а LLD будет удалён как зависимость.

# Фаззер

# Smith

Параметр []const u8 фаззер-тестов заменён на *std.testing.Smith. Этот новый интерфейс используется для генерации значений фаззером. Он содержит следующие базовые методы:

  • value — для генерации любого типа.
  • eos — для генерации маркеров конца потока. Даёт дополнительную гарантию, что в итоге будет возвращено true.
  • bytes — для заполнения байтового массива.
  • slice — для заполнения части буфера и предоставления длины.

Значениям можно задать вероятность выбора с помощью []const Smith.Weight. Это полезно для:

  • более частого выбора интересных значений;
  • снижения вероятности выполнения лишней работы;
  • ограничения выбираемых значений.

В пустом срезе весов каждое значение имеет вес ноль и не будет выбрано. Веса можно использовать только с типами, помещающимися в 64 бита. Для каждого базового метода есть соответствующий, принимающий веса. Кроме того, предоставляются следующие функции:

  • baselineWeights — предоставляет набор весов, содержащий все возможные значения типа.
  • boolWeighted и eosSimpleWeighted — для удобного взвешивания true и false.
  • valueRangeAtMost и valueRangeLessThan — для генерации только диапазона значений.

У каждого метода также есть аналог, принимающий хеш, где значения с одинаковым хешем с большей вероятностью будут мутировать относительно друг друга. Обычные методы уже используют хеши на основе адреса возврата вызываемого, поэтому обычно избыточно напрямую вызывать эти функции, но они могут быть полезны при инлайнинге.

# Пример перехода:

fn fuzzTest(_: void, input: []const u8) !void {
    var sum: u64 = 0;
    for (input) |b| {
        sum += b;
    }
    try std.testing.expect(sum != 1234);
}

⬇️

fn fuzzTest(_: void, smith: *std.testing.Smith) !void {
    var sum: u64 = 0;
    while (!smith.eosWeightedSimple(7, 1)) {
        sum += smith.value(u8);
    }
    try std.testing.expect(sum != 1234);
}

# Мультипроцессное фаззинг-тестирование

Теперь фаззер способен использовать несколько ядер. Это управляется с помощью опции сборки -j. Ограниченное фаззинг-тестирование по-прежнему использует одно ядро.

# Фаззинг в бесконечном режиме

При наличии нескольких тестов фаззер теперь переключается между ними и отдаёт приоритет наиболее эффективным и интересным. Со временем уже исследованные тесты будут запускаться крайне редко по сравнению с тестами, дающими новые входные данные.

# Дамп аварий

Вводы, вызывающие сбой, теперь сохраняются в файл, указанный в сообщении о сбое. Рекомендуется использовать эти файлы для воспроизведения сбоя с помощью std.testing.FuzzInputOptions.corpus и @embedFile.

# Многочисленные баги найдены и исправлены с помощью AST smith

Новый интерфейс smith уже нашёл применение при тестировании инструментария с созданием AST Smith, который используется для генерации случайных валидных AST.

При запуске против zig fmt (в дополнение к некоторым более ранним простым случайным тестам исходного кода) было найдено и исправлено 20 уникальных багов, некоторые из которых были ранее сообщены, а многие — открыты впервые.

Также были обнаружены несколько несоответствий между указанной PEG и парсером: в частности, кортеж не мог содержать типы, начинающиеся с extern или inline; например, const T = struct { u64, extern struct { a: u64 }, u32 } приводил к ошибке. Подробный список изменений PEG и парсера можно найти в add an ast smith.

# Исправление багов

Полный список из 345 закрытых отчётов о багах в этом цикле выпуска:

Многие баги были как внесены, так и исправлены в этом цикле выпуска. Большинство исправлений багов опущены в этих примечаниях к выпуску ради краткости.

# В этом релизе есть баги

В Zig есть известные баги, неправильные компиляции и регрессии.

Даже с Zig 0.16.x работа над нетривиальным проектом на Zig может потребовать участия в процессе разработки.

Когда Zig достигнет версии 1.0.0, поддержка Tier 1 получит политику по багам как дополнительное требование.

# Инструментарий

# LLVM 21

В этом выпуске Zig обновлён до LLVM 21.1.0. Это обновление охватывает Clang ( zig cc), libc++, libc++abi, libunwind и libtsan.

# Отключена векторизация циклов для обхода регрессии

Регрессия серьёзна для Zig, поскольку приводит к неправильной компиляции самого компилятора в распространённых конфигурациях. Попытки обойти это путём отключения определённых возможностей CPU слишком ненадёжны, поэтому мы полностью отключили векторизацию циклов до обновления до версии LLVM, где этот баг исправлен. Это ухудшает генерацию кода в некоторых случаях, что, хотя и прискорбно, предпочтительнее неправильных компиляций.

Это было сообщено и исправлено в апстриме, однако на момент написания исправление ещё не было перенесено в ветку выпуска 22.x LLVM, поэтому мы ожидаем, что эта регрессия производительности затронет не только Zig 0.16.x, но и 0.17.x, и будет окончательно решена только в 0.18.x.

# musl 1.2.5

Zig 0.16.0 поставляет musl 1.2.5 плюс обратные порты (backported) исправлений безопасности. Тем временем, апстрим выпустил 1.2.6. В будущем выпуске Zig будет обновлён до musl 1.2.6.

При таргете на musl в статическом режиме многие функции теперь предоставляются zig libc, а не исходными файлами, скопированными из musl. В частности, теперь с Zig распространяется на 331 исходный файл C из musl меньше, осталось 1 206. Поэтому, если вы столкнётесь с багами в musl libc, предоставляемом Zig, пожалуйста, проявите уважение к апстриму и сообщайте о них в трекер проблем Zig, а не musl.

Обратите внимание, что Zig 0.16.0, как считается, не подвержен CVE-2026-40200 из-за того, что musl'ные qsort и qsort_r больше не используются.

# glibc 2.43

Версия glibc 2.43 теперь доступна при кросскомпиляции.

# Заголовки Linux 6.19

Этот выпуск включает заголовки ядра Linux версии 6.19.

# Заголовки macOS 26.4

Этот выпуск включает системные заголовки macOS версии 26.4.

# MinGW-w64

Zig 0.16.0 продолжает поставлять коммит MinGW-w64 38c8142f660b6ba11e7c408f2de1e9f8bfaf839e.

Однако многие функции теперь предоставляются zig libc, а не исходными файлами, скопированными из MinGW-w64. В частности, теперь с Zig распространяется на 99 исходных файлов C из MinGW-w64 меньше, осталось 398. Поэтому, если вы столкнётесь с багами в MinGW-w64 libc, предоставляемом Zig, пожалуйста, проявите уважение к апстриму и сообщайте о них в трекер проблем Zig, а не MinGW-w64.

# FreeBSD 15.0 libc

Версия libc FreeBSD 15.0 теперь доступна при кросскомпиляции.

# WASI libc

Zig 0.16.0 обновлён до коммита WASI libc c89896107d7b57aef69dcadede47409ee4f702ee.

Однако многие функции теперь предоставляются zig libc, а не исходными файлами, скопированными из WASI libc.

Несмотря на это, количество исходных файлов C WASI libc, распространяемых с Zig, увеличилось со 196 до 228 из-за того, что в более новой WASI libc добавлены pthread-обёртки и потому что большинство исходных файлов WASI libc являются общими с musl.

# zig libc

Реализация libc в Zig получила множество новых функций, что привело к удалению соответствующих исходных файлов C из musl и MinGW-w64. В этом выпуске количество распространяемых исходных файлов C уменьшилось с 2 270 до 1 873 (−17%).

В частности, это включает многие математические функции, а также malloc и его аналоги. Особая благодарность Szabolcs Nagy за libc-test.

# zig cc

zig cc и zig c++ теперь основаны на Clang 21.1.8.

Исправлено 9 багов: GitHub Codeberg

# Поддержка динамически связанного OpenBSD libc при кросскомпиляции

Zig теперь позволяет кросскомпилировать в OpenBSD 7.8+, предоставляя заглушечные библиотеки для динамического libc, аналогично тому, как обрабатывается кросскомпиляция для glibc. Кроме того, предоставляются все заголовки libc и большинство системных заголовков.

# Дорожная карта

Цикл выпуска 0.17.0 будет коротким и будет направлен в основном на обновление до LLVM 22 и завершение разделения процесса make (сборщик) от процесса configure (build.zig).

После этого основными инициативами будут:

  • Завершение и стабилизация языка.
  • Завершение aarch64 Backend, чтобы сделать его бэкендом по умолчанию для режима debug.
  • Усовершенствование реализаций Linker, устранение зависимости от LLD и поддержка инкрементальной компиляции.
  • Усовершенствование интегрированного Fuzzer для конкуренции с AFL и другими современными фаззерами.
  • Переход от зависимости библиотеки от LLVM к зависимости процесса от Clang.

Примечание переводчика: Далее в оригинале следует список контрибьюторов и благодарности. Оригинал этого Release Notes по ссылке