← Вернуться обратно на Изучение

Обзор

Ключевые особенности

Компактный и простой язык

Сфокусируйтесь на отладке вашего приложения, а не на отладке ваших знаний языка программирования.

Синтаксис Zig полностью описан в грамматике PEG, состоящей из 580 строк.

В языке нет скрытых потоков управления, скрытых выделений памяти, препроцессоров и макросов. Если код на Zig не выглядит так, будто он вызывает функцию, значит, он её не вызывает. Можно быть уверенным, что в приведённом ниже коде вызываются только foo(), а затем bar() – независимо от типов данных:

var a = b + c.d;
foo();
bar();

Примеры скрытых потоков управления:

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

Производительность или безопасность? И то, и другое

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

Parameter Debug ReleaseSafe ReleaseFast ReleaseSmall
Оптимизации — улучшают скорость, усложняют отладку, увеличивают время компиляции -O3 -O3 -Os
Проверки безопасности во время выполнения — снижают скорость, увеличивают размер, приводят к аварийному завершению вместо неопределённого поведения Включено Включено

Вот как переполнение целых чисел (Integer Overflow) выглядит во время компиляции, независимо от режима сборки:

1-integer-overflow.zig
test "integer overflow at compile time" {
    const x: u8 = 255;
    _ = x + 1;
}
Shell
$ zig test 1-integer-overflow.zig
assets/zig-code/features/1-integer-overflow.zig:3:11: error: overflow of integer type 'u8' with value '256'

А вот как это выглядит во время выполнения в сборках с проверкой безопасности:

2-integer-overflow-runtime.zig
test "integer overflow at runtime" {
    var x: u8 = 255;
    x += 1;
}
Shell
$ zig test 2-integer-overflow-runtime.zig
1/1 2-integer-overflow-runtime.test.integer overflow at runtime...thread 3030394 panic: integer overflow
/home/ci/actions-runner-website/_work/www.ziglang.org/www.ziglang.org/assets/zig-code/features/2-integer-overflow-runtime.zig:3:7: 0x103cc2e in test.integer overflow at runtime (test)
    x += 1;
      ^
/home/ci/deps/zig-linux-x86_64-0.13.0/lib/compiler/test_runner.zig:157:25: 0x1047f99 in mainTerminal (test)
        if (test_fn.func()) |_| {
                        ^
/home/ci/deps/zig-linux-x86_64-0.13.0/lib/compiler/test_runner.zig:37:28: 0x103e01b in main (test)
        return mainTerminal();
                           ^
/home/ci/deps/zig-linux-x86_64-0.13.0/lib/std/start.zig:514:22: 0x103d159 in posixCallMainAndExit (test)
            root.main();
                     ^
/home/ci/deps/zig-linux-x86_64-0.13.0/lib/std/start.zig:266:5: 0x103ccc1 in _start (test)
    asm volatile (switch (native_arch) {
    ^
???:?:?: 0x0 in ??? (???)
error: the following test command crashed:
/home/ci/actions-runner-website/_work/www.ziglang.org/www.ziglang.org/.zig-cache/o/9e3bfcb661c1ce3251fa44aa9886f390/test

Эта трассировка стека совместима со всеми целевыми платформами, включая независимые целевые платформы.

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

3-undefined-behavior.zig
test "actually undefined behavior" {
    @setRuntimeSafety(false);
    var x: u8 = 255;
    x += 1; // XXX undefined behavior!
}

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

Говоря о производительности, Zig работает быстрее, чем C.

Обратите внимание, что Zig не является полностью безопасным языком. Тем, кто интересуется развитием безопасности в Zig, рекомендуем подписаться на следующие GitHub Issues:

Zig конкурирует с C, а не зависит от него

Стандартная библиотека Zig интегрируется с libc, но не зависит от неё. Вот "Hello, World!":

4-hello.zig
const std = @import("std");

pub fn main() void {
    std.debug.print("Hello, world!\n", .{});
}
Shell
$ zig build-exe 4-hello.zig
$ ./4-hello
Hello, world!

При компиляции с флагом -O ReleaseSmall, без символов отладки и в однопоточном режиме, это создает статический исполняемый файл размером 9.8 КиБ для платформы x86_64-linux:

$ zig build-exe hello.zig -O ReleaseSmall -fstrip -fsingle-threaded
$ wc -c hello
9944 hello
$ ldd hello
  not a dynamic executable

Сборка для Windows выходит ещё меньше, всего 4096 байта:

$ zig build-exe hello.zig -O ReleaseSmall -fstrip -fsingle-threaded -target x86_64-windows
$ wc -c hello.exe
4096 hello.exe
$ file hello.exe
hello.exe: PE32+ executable (console) x86-64, for MS Windows

Порядок-независимые объявления на верхнем уровне

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

5-global-variables.zig
var y: i32 = add(10, x);
const x: i32 = add(12, 34);

test "global variables" {
    assert(x == 46);
    assert(y == 56);
}

fn add(a: i32, b: i32) i32 {
    return a + b;
}

const std = @import("std");
const assert = std.debug.assert;
Shell
$ zig test 5-global-variables.zig
1/1 5-global-variables.test.global variables...OK
All 1 tests passed.

Опциональный тип вместо нулевых указателей

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

В Zig указатели, не имеющие дополнительных модификаторов, не могут быть нулевыми:

6-null-to-ptr.zig
test "null @intToPtr" {
    const foo: *i32 = @ptrFromInt(0x0);
    _ = foo;
}
Shell
$ zig test 6-null-to-ptr.zig
assets/zig-code/features/6-null-to-ptr.zig:2:35: error: pointer type '*i32' does not allow address zero

Однако любой тип может быть превращен в опциональный тип с помощью префикса ?:

7-optional-syntax.zig
const std = @import("std");
const assert = std.debug.assert;

test "null @intToPtr" {
    const ptr: ?*i32 = @ptrFromInt(0x0);
    assert(ptr == null);
}
Shell
$ zig test 7-optional-syntax.zig
1/1 7-optional-syntax.test.null @intToPtr...OK
All 1 tests passed.

Чтобы получить значение из опционального типа, можно использовать orelse, задав значение по умолчанию:

8-optional-orelse.zig
// malloc prototype included for reference
extern fn malloc(size: size_t) ?*u8;

fn doAThing() ?*Foo {
    const ptr = malloc(1234) orelse return null;
    // ...
}

Другой вариант — использовать if:

9-optional-if.zig
fn doAThing(optional_foo: ?*Foo) void {
    // do some stuff

    if (optional_foo) |foo| {
        doSomethingWithFoo(foo);
    }

    // do some stuff
}

Такой же синтаксис используется и с while:

10-optional-while.zig
const std = @import("std");

pub fn main() void {
    const msg = "hello this is dog";
    var it = std.mem.tokenize(u8, msg, " ");
    while (it.next()) |item| {
        std.debug.print("{s}\n", .{item});
    }
}
Shell
$ zig build-exe 10-optional-while.zig
$ ./10-optional-while
hello
this
is
dog

Ручное управление памятью

Библиотека, написанная на Zig, может быть использована в любом месте:

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

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

Помимо нового подхода к обработке ошибок, Zig предоставляет конструкции defer и errdefer, чтобы сделать управление ресурсами — не только памятью — простым и легко проверяемым.

Пример использования defer можно найти в разделе Интеграция с библиотеками C без FFI. Вот пример применения errdefer:

11-errdefer.zig
const Device = struct {
    name: []u8,

    fn create(allocator: *Allocator, id: u32) !Device {
        const device = try allocator.create(Device);
        errdefer allocator.destroy(device);

        device.name = try std.fmt.allocPrint(allocator, "Device(id={d})", id);
        errdefer allocator.free(device.name);

        if (id == 0) return error.ReservedDeviceId;

        return device;
    }
};

Новый подход к обработке ошибок

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

12-errors-as-values.zig
const std = @import("std");

pub fn main() void {
    _ = std.fs.cwd().openFile("does_not_exist/foo.txt", .{});
}
Shell
$ zig build-exe 12-errors-as-values.zig
/home/ci/actions-runner-website/_work/www.ziglang.org/www.ziglang.org/assets/zig-code/features/12-errors-as-values.zig:4:30: error: error union is discarded
    _ = std.fs.cwd().openFile("does_not_exist/foo.txt", .{});
        ~~~~~~~~~~~~~~~~~~~~~^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
/home/ci/actions-runner-website/_work/www.ziglang.org/www.ziglang.org/assets/zig-code/features/12-errors-as-values.zig:4:30: note: consider using 'try', 'catch', or 'if'
referenced by:
    callMain: /home/ci/deps/zig-linux-x86_64-0.13.0/lib/std/start.zig:514:17
    callMainWithArgs: /home/ci/deps/zig-linux-x86_64-0.13.0/lib/std/start.zig:482:12
    remaining reference traces hidden; use '-freference-trace' to see all reference traces

Ошибки можно обработать с помощью catch:

13-errors-catch.zig
const std = @import("std");

pub fn main() void {
    const file = std.fs.cwd().openFile("does_not_exist/foo.txt", .{}) catch |err| label: {
        std.debug.print("unable to open file: {}\n", .{err});
        const stderr = std.io.getStdErr();
        break :label stderr;
    };
    file.writeAll("all your codebase are belong to us\n") catch return;
}
Shell
$ zig build-exe 13-errors-catch.zig
$ ./13-errors-catch
unable to open file: error.FileNotFound
all your codebase are belong to us

Ключевое слово try является сокращением для catch |err| return err:

14-errors-try.zig
const std = @import("std");

pub fn main() !void {
    const file = try std.fs.cwd().openFile("does_not_exist/foo.txt", .{});
    defer file.close();
    try file.writeAll("all your codebase are belong to us\n");
}
Shell
$ zig build-exe 14-errors-try.zig
$ ./14-errors-try
error: FileNotFound
/home/ci/deps/zig-linux-x86_64-0.13.0/lib/std/posix.zig:1768:23: 0x1066a70 in openatZ (14-errors-try)
            .NOENT => return error.FileNotFound,
                      ^
/home/ci/deps/zig-linux-x86_64-0.13.0/lib/std/fs/Dir.zig:880:16: 0x1038294 in openFileZ (14-errors-try)
    const fd = try posix.openatZ(self.fd, sub_path, os_flags, 0);
               ^
/home/ci/deps/zig-linux-x86_64-0.13.0/lib/std/fs/Dir.zig:827:5: 0x1034d9e in openFile (14-errors-try)
    return self.openFileZ(&path_c, flags);
    ^
/home/ci/actions-runner-website/_work/www.ziglang.org/www.ziglang.org/assets/zig-code/features/14-errors-try.zig:4:18: 0x1034be8 in main (14-errors-try)
    const file = try std.fs.cwd().openFile("does_not_exist/foo.txt", .{});
                 ^

Обратите внимание, что это трассировка возврата ошибки, а не трассировка стека. Для создания этой трассировки разворачивание стека не потребовалось, что позволило исключить дополнительные затраты на производительность.

Ключевое слово switch, применяемое к ошибке, гарантирует, что все возможные ошибки будут обработаны:

15-errors-switch.zig
const std = @import("std");

test "switch on error" {
    _ = parseInt("hi", 10) catch |err| switch (err) {};
}

fn parseInt(buf: []const u8, radix: u8) !u64 {
    var x: u64 = 0;

    for (buf) |c| {
        const digit = try charToDigit(c);

        if (digit >= radix) {
            return error.DigitExceedsRadix;
        }

        x = try std.math.mul(u64, x, radix);
        x = try std.math.add(u64, x, digit);
    }

    return x;
}

fn charToDigit(c: u8) !u8 {
    const value = switch (c) {
        '0'...'9' => c - '0',
        'A'...'Z' => c - 'A' + 10,
        'a'...'z' => c - 'a' + 10,
        else => return error.InvalidCharacter,
    };

    return value;
}
Shell
$ zig build-exe 15-errors-switch.zig
/home/ci/deps/zig-linux-x86_64-0.13.0/lib/std/start.zig:509:45: error: root struct of file '15-errors-switch' has no member named 'main'
    switch (@typeInfo(@typeInfo(@TypeOf(root.main)).Fn.return_type.?)) {
                                        ~~~~^~~~~
/home/ci/actions-runner-website/_work/www.ziglang.org/www.ziglang.org/assets/zig-code/features/15-errors-switch.zig:1:1: note: struct declared here
const std = @import("std");
^~~~~
/home/ci/deps/zig-linux-x86_64-0.13.0/lib/std/start.zig:482:20: note: called from here
    return callMain();
           ~~~~~~~~^~
/home/ci/deps/zig-linux-x86_64-0.13.0/lib/std/start.zig:438:36: note: called from here
    std.posix.exit(callMainWithArgs(argc, argv, envp));
                   ~~~~~~~~~~~~~~~~^~~~~~~~~~~~~~~~~~
referenced by:
    _start: /home/ci/deps/zig-linux-x86_64-0.13.0/lib/std/start.zig:351:40
    remaining reference traces hidden; use '-freference-trace' to see all reference traces

Ключевое слово unreachable служит для того, чтобы заверить, что никаких ошибок не случится:

16-unreachable.zig
const std = @import("std");

pub fn main() void {
    const file = std.fs.cwd().openFile("does_not_exist/foo.txt", .{}) catch unreachable;
    file.writeAll("all your codebase are belong to us\n") catch unreachable;
}
Shell
$ zig build-exe 16-unreachable.zig
$ ./16-unreachable
thread 3030318 panic: attempt to unwrap error: FileNotFound
/home/ci/deps/zig-linux-x86_64-0.13.0/lib/std/posix.zig:1768:23: 0x10699b0 in openatZ (16-unreachable)
            .NOENT => return error.FileNotFound,
                      ^
/home/ci/deps/zig-linux-x86_64-0.13.0/lib/std/fs/Dir.zig:880:16: 0x1039a94 in openFileZ (16-unreachable)
    const fd = try posix.openatZ(self.fd, sub_path, os_flags, 0);
               ^
/home/ci/deps/zig-linux-x86_64-0.13.0/lib/std/fs/Dir.zig:827:5: 0x103723e in openFile (16-unreachable)
    return self.openFileZ(&path_c, flags);
    ^
/home/ci/actions-runner-website/_work/www.ziglang.org/www.ziglang.org/assets/zig-code/features/16-unreachable.zig:4:77: 0x1034fcf in main (16-unreachable)
    const file = std.fs.cwd().openFile("does_not_exist/foo.txt", .{}) catch unreachable;
                                                                            ^
/home/ci/deps/zig-linux-x86_64-0.13.0/lib/std/start.zig:514:22: 0x10347d9 in posixCallMainAndExit (16-unreachable)
            root.main();
                     ^
/home/ci/deps/zig-linux-x86_64-0.13.0/lib/std/start.zig:266:5: 0x1034341 in _start (16-unreachable)
    asm volatile (switch (native_arch) {
    ^
???:?:?: 0x0 in ??? (???)
(process terminated by signal)

Это вызывает неопределённое поведение в небезопасных режимах сборки, поэтому используйте его только при полной уверенности в успехе.

Трассировки стеков на всех платформах

Трассировки стеков и трассировки возвратов ошибок, показанные на этой странице, работают на всех целевых платформах уровня поддержки 1 и некоторых уровня поддержки 2. И даже на независимых целевых платформах!

Стандартная библиотека также предоставляет возможность захватить трассировку стека в любое время и впоследствии вывести её в стандартный поток ошибок:

17-stack-traces.zig
const std = @import("std");

var address_buffer: [8]usize = undefined;

var trace1 = std.builtin.StackTrace{
    .instruction_addresses = address_buffer[0..4],
    .index = 0,
};

var trace2 = std.builtin.StackTrace{
    .instruction_addresses = address_buffer[4..],
    .index = 0,
};

pub fn main() void {
    foo();
    bar();

    std.debug.print("first one:\n", .{});
    std.debug.dumpStackTrace(trace1);
    std.debug.print("\n\nsecond one:\n", .{});
    std.debug.dumpStackTrace(trace2);
}

fn foo() void {
    std.debug.captureStackTrace(null, &trace1);
}

fn bar() void {
    std.debug.captureStackTrace(null, &trace2);
}
Shell
$ zig build-exe 17-stack-traces.zig
$ ./17-stack-traces
first one:
/home/ci/deps/zig-linux-x86_64-0.13.0/lib/std/debug.zig:365:29: 0x10396e7 in captureStackTrace (17-stack-traces)
            addr.* = it.next() orelse {
                            ^
/home/ci/actions-runner-website/_work/www.ziglang.org/www.ziglang.org/assets/zig-code/features/17-stack-traces.zig:26:32: 0x103717c in foo (17-stack-traces)
    std.debug.captureStackTrace(null, &trace1);
                               ^
/home/ci/actions-runner-website/_work/www.ziglang.org/www.ziglang.org/assets/zig-code/features/17-stack-traces.zig:16:8: 0x10350c8 in main (17-stack-traces)
    foo();
       ^
/home/ci/deps/zig-linux-x86_64-0.13.0/lib/std/start.zig:514:22: 0x1034979 in posixCallMainAndExit (17-stack-traces)
            root.main();
                     ^


second one:
/home/ci/deps/zig-linux-x86_64-0.13.0/lib/std/debug.zig:365:29: 0x10396e7 in captureStackTrace (17-stack-traces)
            addr.* = it.next() orelse {
                            ^
/home/ci/actions-runner-website/_work/www.ziglang.org/www.ziglang.org/assets/zig-code/features/17-stack-traces.zig:30:32: 0x103719c in bar (17-stack-traces)
    std.debug.captureStackTrace(null, &trace2);
                               ^
/home/ci/actions-runner-website/_work/www.ziglang.org/www.ziglang.org/assets/zig-code/features/17-stack-traces.zig:17:8: 0x10350cd in main (17-stack-traces)
    bar();
       ^
/home/ci/deps/zig-linux-x86_64-0.13.0/lib/std/start.zig:514:22: 0x1034979 in posixCallMainAndExit (17-stack-traces)
            root.main();
                     ^

Эту технику можно увидеть в действии в проекте GeneralPurposeDebugAllocator.

Обобщённые структуры данных и функции

Типы являются значениями, которые должны быть известны на этапе компиляции:

18-types.zig
const std = @import("std");
const assert = std.debug.assert;

test "types are values" {
    const T1 = u8;
    const T2 = bool;
    assert(T1 != T2);

    const x: T2 = true;
    assert(x);
}
Shell
$ zig test 18-types.zig
1/1 18-types.test.types are values...OK
All 1 tests passed.

Обобщённая структура данных — это просто функция, которая возвращает type:

19-generics.zig
const std = @import("std");

fn List(comptime T: type) type {
    return struct {
        items: []T,
        len: usize,
    };
}

pub fn main() void {
    var buffer: [10]i32 = undefined;
    var list = List(i32){
        .items = &buffer,
        .len = 0,
    };
    list.items[0] = 1234;
    list.len += 1;

    std.debug.print("{d}\n", .{list.items.len});
}
Shell
$ zig build-exe 19-generics.zig
$ ./19-generics
10

Рефлексия во время компиляции и выполнение кода на этапе компиляции

Встроенная функция @typeInfo предоставляет возможности для рефлексии:

20-reflection.zig
const std = @import("std");

const Header = struct {
    magic: u32,
    name: []const u8,
};

pub fn main() void {
    printInfoAboutStruct(Header);
}

fn printInfoAboutStruct(comptime T: type) void {
    const info = @typeInfo(T);
    inline for (info.Struct.fields) |field| {
        std.debug.print(
            "{s} has a field called {s} with type {s}\n",
            .{
                @typeName(T),
                field.name,
                @typeName(field.type),
            },
        );
    }
}
Shell
$ zig build-exe 20-reflection.zig
$ ./20-reflection
20-reflection.Header has a field called magic with type u32
20-reflection.Header has a field called name with type []const u8

Стандартная библиотека Zig использует эту технику для реализации форматированного вывода. Несмотря на то, что Zig является компактным и простым языком, его форматированный вывод полностью реализован на самом Zig. В то же время, в C ошибки компиляции для printf жёстко запрограммированы в компиляторе. Аналогично, в Rust макрос для форматированного вывода также встроен в компилятор.

Zig также может выполнять функции и блоки кода на этапе компиляции. В некоторых случаях, например, при инициализации глобальных переменных, выражение неявно вычисляется на этапе компиляции. В других ситуациях можно явно выполнить код на этапе компиляции, используя ключевое слово comptime. Это может быть особенно эффективно в сочетании с утверждениями (assert):

21-comptime.zig
const std = @import("std");
const assert = std.debug.assert;

fn fibonacci(x: u32) u32 {
    if (x <= 1) return x;
    return fibonacci(x - 1) + fibonacci(x - 2);
}

test "compile-time evaluation" {
    var array: [fibonacci(6)]i32 = undefined;

    @memset(&array, 42);

    comptime {
        assert(array.len == 12345);
    }
}
Shell
$ zig test 21-comptime.zig
/home/ci/deps/zig-linux-x86_64-0.13.0/lib/std/debug.zig:412:14: error: reached unreachable code
assets/zig-code/features/21-comptime.zig:15:15: note: called from here

Интеграция с библиотеками C без FFI

Функция @cImport позволяет напрямую импортировать типы, переменные, функции и простые макросы для использования в Zig. Она даже переводит встроенные функции из C в Zig.

Вот пример генерации синусоидальной волны с использованием libsoundio:

sine.zig

22-sine-wave.zig
const c = @cImport(@cInclude("soundio/soundio.h"));
const std = @import("std");

fn sio_err(err: c_int) !void {
    switch (err) {
        c.SoundIoErrorNone => {},
        c.SoundIoErrorNoMem => return error.NoMem,
        c.SoundIoErrorInitAudioBackend => return error.InitAudioBackend,
        c.SoundIoErrorSystemResources => return error.SystemResources,
        c.SoundIoErrorOpeningDevice => return error.OpeningDevice,
        c.SoundIoErrorNoSuchDevice => return error.NoSuchDevice,
        c.SoundIoErrorInvalid => return error.Invalid,
        c.SoundIoErrorBackendUnavailable => return error.BackendUnavailable,
        c.SoundIoErrorStreaming => return error.Streaming,
        c.SoundIoErrorIncompatibleDevice => return error.IncompatibleDevice,
        c.SoundIoErrorNoSuchClient => return error.NoSuchClient,
        c.SoundIoErrorIncompatibleBackend => return error.IncompatibleBackend,
        c.SoundIoErrorBackendDisconnected => return error.BackendDisconnected,
        c.SoundIoErrorInterrupted => return error.Interrupted,
        c.SoundIoErrorUnderflow => return error.Underflow,
        c.SoundIoErrorEncodingString => return error.EncodingString,
        else => return error.Unknown,
    }
}

var seconds_offset: f32 = 0;

fn write_callback(
    maybe_outstream: ?[*]c.SoundIoOutStream,
    frame_count_min: c_int,
    frame_count_max: c_int,
) callconv(.C) void {
    _ = frame_count_min;
    const outstream: *c.SoundIoOutStream = &maybe_outstream.?[0];
    const layout = &outstream.layout;
    const float_sample_rate: f32 = @floatFromInt(outstream.sample_rate);
    const seconds_per_frame = 1.0 / float_sample_rate;
    var frames_left = frame_count_max;

    while (frames_left > 0) {
        var frame_count = frames_left;

        var areas: [*]c.SoundIoChannelArea = undefined;
        sio_err(c.soundio_outstream_begin_write(
            maybe_outstream,
            @ptrCast(&areas),
            &frame_count,
        )) catch |err| std.debug.panic("write failed: {s}", .{@errorName(err)});

        if (frame_count == 0) break;

        const pitch = 440.0;
        const radians_per_second = pitch * 2.0 * std.math.pi;
        var frame: c_int = 0;
        while (frame < frame_count) : (frame += 1) {
            const float_frame: f32 = @floatFromInt(frame);
            const sample = std.math.sin((seconds_offset + float_frame *
                seconds_per_frame) * radians_per_second);
            {
                var channel: usize = 0;
                while (channel < @as(usize, @intCast(layout.channel_count))) : (channel += 1) {
                    const channel_ptr = areas[channel].ptr;
                    const sample_ptr: *f32 = @alignCast(@ptrCast(&channel_ptr[@intCast(areas[channel].step * frame)]));
                    sample_ptr.* = sample;
                }
            }
        }
        const float_frame_count: f32 = @floatFromInt(frame_count);
        seconds_offset += seconds_per_frame * float_frame_count;

        sio_err(c.soundio_outstream_end_write(maybe_outstream)) catch |err| std.debug.panic("end write failed: {s}", .{@errorName(err)});

        frames_left -= frame_count;
    }
}

pub fn main() !void {
    const soundio = c.soundio_create();
    defer c.soundio_destroy(soundio);

    try sio_err(c.soundio_connect(soundio));

    c.soundio_flush_events(soundio);

    const default_output_index = c.soundio_default_output_device_index(soundio);
    if (default_output_index < 0) return error.NoOutputDeviceFound;

    const device = c.soundio_get_output_device(soundio, default_output_index) orelse return error.OutOfMemory;
    defer c.soundio_device_unref(device);

    std.debug.print("Output device: {s}\n", .{device.*.name});

    const outstream = c.soundio_outstream_create(device) orelse return error.OutOfMemory;
    defer c.soundio_outstream_destroy(outstream);

    outstream.*.format = c.SoundIoFormatFloat32NE;
    outstream.*.write_callback = write_callback;

    try sio_err(c.soundio_outstream_open(outstream));

    try sio_err(c.soundio_outstream_start(outstream));

    while (true) c.soundio_wait_events(soundio);
}

$ zig build-exe sine.zig -lsoundio -lc
$ ./sine
Output device: Built-in Audio Analog Stereo
^C

Этот код на Zig значительно проще, чем эквивалентный код на C, и обеспечивает более высокий уровень безопасности. Всё это достигается благодаря прямому импорту заголовочного файла C – без каких-либо обёрток.

Zig лучше использует библиотеки C, чем сами C-библиотеки.

Zig также является компилятором C

Вот пример сборки C-кода с помощью Zig:

hello.c

#include <stdio.h>

int main(int argc, char **argv) {
    printf("Hello world\n");
    return 0;
}
$ zig build-exe hello.c --library c
$ ./hello
Hello world

Флаг --verbose-cc позволяет увидеть, какие команды компилятора C были выполнены:

$ zig build-exe hello.c --library c --verbose-cc
zig cc -MD -MV -MF .zig-cache/tmp/42zL6fBH8fSo-hello.o.d -nostdinc -fno-spell-checking -isystem /home/andy/dev/zig/build/lib/zig/include -isystem /home/andy/dev/zig/build/lib/zig/libc/include/x86_64-linux-gnu -isystem /home/andy/dev/zig/build/lib/zig/libc/include/generic-glibc -isystem /home/andy/dev/zig/build/lib/zig/libc/include/x86_64-linux-any -isystem /home/andy/dev/zig/build/lib/zig/libc/include/any-linux-any -march=native -g -fstack-protector-strong --param ssp-buffer-size=4 -fno-omit-frame-pointer -o .zig-cache/tmp/42zL6fBH8fSo-hello.o -c hello.c -fPIC

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

$ time zig build-exe hello.c --library c --verbose-cc

real	0m0.027s
user	0m0.018s
sys	0m0.009s

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

Zig может не только компилировать C-код, но есть и веская причина выбрать Zig в качестве компилятора для C: Zig поставляется с libc.

Экспорт функций, переменных и типов для использования в C-коде

Одним из сценариев использования Zig является экспорт библиотеки через C ABI (Application Binary Interface) для вызова другими языками программирования. Ключевое слово export перед функциями, переменными и типами делает их частью интерфейса библиотеки:

mathtest.zig

23-math-test.zig
export fn add(a: i32, b: i32) i32 {
    return a + b;
}

Для создания статической библиотеки:

$ zig build-lib mathtest.zig

Для создания общей библиотеки:

$ zig build-lib mathtest.zig -dynamic

Вот пример с использованием системы сборки Zig:

test.c

#include "mathtest.h"
#include <stdio.h>

int main(int argc, char **argv) {
    int32_t result = add(42, 1337);
    printf("%d\n", result);
    return 0;
}

build.zig

24-build.zig
const Builder = @import("std").build.Builder;

pub fn build(b: *Builder) void {
    const lib = b.addSharedLibrary("mathtest", "mathtest.zig", b.version(1, 0, 0));

    const exe = b.addExecutable("test", null);
    exe.addCSourceFile("test.c", &[_][]const u8{"-std=c99"});
    exe.linkLibrary(lib);
    exe.linkSystemLibrary("c");

    b.default_step.dependOn(&exe.step);

    const run_cmd = exe.run();

    const test_step = b.step("test", "Test the program");
    test_step.dependOn(&run_cmd.step);
}

$ zig build test
1379

Первоклассная поддержка кросс-компиляции

Zig может создавать сборки для любой целевой платформы из Таблицы поддержки (см. последние релизные заметки) с уровнем поддержки 3 или выше. Никаких "кросс-компиляторов" устанавливать не требуется. Вот пример программы "Hello, World!" для текущей платформы:

4-hello.zig
const std = @import("std");

pub fn main() void {
    std.debug.print("Hello, world!\n", .{});
}
Shell
$ zig build-exe 4-hello.zig
$ ./4-hello
Hello, world!

Теперь чтобы собрать её для x86_64-windows, x86_64-macos и aarch64-linux:

$ zig build-exe hello.zig -target x86_64-windows
$ file hello.exe
hello.exe: PE32+ executable (console) x86-64, for MS Windows
$ zig build-exe hello.zig -target x86_64-macos
$ file hello
hello: Mach-O 64-bit x86_64 executable, flags:<NOUNDEFS|DYLDLINK|TWOLEVEL|PIE>
$ zig build-exe hello.zig -target aarch64-linux
$ file hello
hello: ELF 64-bit LSB executable, ARM aarch64, version 1 (SYSV), statically linked, with debug_info, not stripped

Это работает на любой платформе с уровнем поддержки 3+ и для любой платформы с уровнем поддержки 3+.

Zig поставляется с libc

Доступные целевые платформы libc можно узнать с помощью команды zig targets:

...
 "libc": [
  "aarch64_be-linux-gnu",
  "aarch64_be-linux-musl",
  "aarch64_be-windows-gnu",
  "aarch64-linux-gnu",
  "aarch64-linux-musl",
  "aarch64-windows-gnu",
  "armeb-linux-gnueabi",
  "armeb-linux-gnueabihf",
  "armeb-linux-musleabi",
  "armeb-linux-musleabihf",
  "armeb-windows-gnu",
  "arm-linux-gnueabi",
  "arm-linux-gnueabihf",
  "arm-linux-musleabi",
  "arm-linux-musleabihf",
  "arm-windows-gnu",
  "mips64el-linux-gnuabi64",
  "mips64el-linux-gnuabin32",
  "mips64el-linux-musl",
  "mips64-linux-gnuabi64",
  "mips64-linux-gnuabin32",
  "mips64-linux-musl",
  "mipsel-linux-gnu",
  "mipsel-linux-musl",
  "mips-linux-gnu",
  "mips-linux-musl",
  "powerpc64le-linux-gnu",
  "powerpc64le-linux-musl",
  "powerpc64-linux-gnu",
  "powerpc64-linux-musl",
  "powerpc-linux-gnu",
  "powerpc-linux-musl",
  "riscv64-linux-gnu",
  "riscv64-linux-musl",
  "s390x-linux-gnu",
  "s390x-linux-musl",
  "sparc-linux-gnu",
  "sparcv9-linux-gnu",
  "wasm32-freestanding-musl",
  "x86-linux-gnu",
  "x86-linux-musl",
  "x86-windows-gnu",
  "x86_64-linux-gnu",
  "x86_64-linux-gnux32",
  "x86_64-linux-musl",
  "x86_64-windows-gnu"
 ],

Это означает, что --library c для этих целевых платформ не зависит ни от каких системных файлов!

Давайте снова посмотрим на пример "Hello, World!" на C:

$ zig build-exe hello.c --library c
$ ./hello
Hello world
$ ldd ./hello
	linux-vdso.so.1 (0x00007ffd03dc9000)
	libc.so.6 => /lib/libc.so.6 (0x00007fc4b62be000)
	libm.so.6 => /lib/libm.so.6 (0x00007fc4b5f29000)
	libpthread.so.0 => /lib/libpthread.so.0 (0x00007fc4b5d0a000)
	libdl.so.2 => /lib/libdl.so.2 (0x00007fc4b5b06000)
	librt.so.1 => /lib/librt.so.1 (0x00007fc4b58fe000)
	/lib/ld-linux-x86-64.so.2 => /lib64/ld-linux-x86-64.so.2 (0x00007fc4b6672000)

glibc не поддерживает статические сборки, но musl предоставляет такую возможность:

$ zig build-exe hello.c --library c -target x86_64-linux-musl
$ ./hello
Hello world
$ ldd hello
  not a dynamic executable

В этом примере Zig скомпилировал musl libc из исходного кода и затем провел линковку. Сборка musl libc для x86_64-linux сохраняется благодаря системе кэширования, что позволяет в любой момент мгновенно получить к ней доступ.

Данная функциональность доступна на любой платформе. Пользователи Windows и macOS могут компилировать код на Zig и C и связываться с libc для любой из указанных выше целевых платформ. Аналогично, код может быть перекомпилирован для других архитектур:

$ zig build-exe hello.c --library c -target aarch64-linux-gnu
$ file hello
hello: ELF 64-bit LSB executable, ARM aarch64, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux-aarch64.so.1, for GNU/Linux 2.0.0, with debug_info, not stripped

В некоторых аспектах Zig является даже лучшим компилятором C, чем сами компиляторы C!

То, что предлагает Zig, намного шире, чем просто комплект инструментов для кросс-компиляции. Например, общий размер заголовков libc, которые поставляются с Zig, составляет 22 МиБ в распакованном виде. В то же время одни только заголовки для musl libc + linux на платформе x86_64 занимают 8 МиБ, а для glibc — 3.1 МиБ (glibc не включает заголовки linux), и при этом Zig в настоящее время включает 40 библиотек libc. Простое их объединение заняло бы 444 МиБ. Но благодаря инструменту process_headers и тщательной ручной работе, архивы с бинарниками Zig занимают приблизительно 30 МиБ, несмотря на поддержку libc для всех этих целевых платформ, а также compiler-rt, libunwind и libcxx, и несмотря на то, что Zig совместим с clang как компилятор C. Для сравнения, бинарная сборка clang версии 8.0.0 для Windows с сайта llvm.org составляет 132 МиБ.

Имейте в виду, что только целевые платформы с уровнем поддержки 1 были тщательно протестированы. В планах добавить больше библиотек libc (включая для Windows) и увеличить тестовое покрытие для всех libc.

Мы работаем над менеджером пакетов для Zig. В перспективе будет возможность создавать пакеты для библиотек на C, что сделает систему сборки Zig привлекательной как для разработчиков на Zig, так и для программистов на C.

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

Zig поставляется с собственной системой сборки, так что вам не понадобятся Make, CMake или другие подобные инструменты.

$ zig init-exe
Created build.zig
Created src/main.zig

Далее попробуйте `zig build --help` или `zig build run`

src/main.zig

25-all-bases.zig
const std = @import("std");

pub fn main() anyerror!void {
    std.debug.print("All your base are belong to us.\n");
}

build.zig

26-build.zig
const Builder = @import("std").build.Builder;

pub fn build(b: *Builder) void {
    const mode = b.standardReleaseOptions();
    const exe = b.addExecutable("example", "src/main.zig");
    exe.setBuildMode(mode);

    const run_cmd = exe.run();

    const run_step = b.step("run", "Run the app");
    run_step.dependOn(&run_cmd.step);

    b.default_step.dependOn(&exe.step);
    b.installArtifact(exe);
}

Давайте посмотрим на меню --help.

$ zig build --help
Usage: zig build [steps] [options]

Steps:
  install (default)      Copy build artifacts to prefix path
  uninstall              Remove build artifacts from prefix path
  run                    Run the app

General Options:
  --help                 Print this help and exit
  --verbose              Print commands before executing them
  --prefix [path]        Override default install prefix
  --search-prefix [path] Add a path to look for binaries, libraries, headers

Project-Specific Options:
  -Dtarget=[string]      The CPU architecture, OS, and ABI to build for.
  -Drelease-safe=[bool]  optimizations on and safety on
  -Drelease-fast=[bool]  optimizations on and safety off
  -Drelease-small=[bool] size optimizations on and safety off

Advanced Options:
  --build-file [file]         Override path to build.zig
  --cache-dir [path]          Override path to zig cache directory
  --override-lib-dir [arg]    Override path to Zig lib directory
  --verbose-tokenize          Enable compiler debug output for tokenization
  --verbose-ast               Enable compiler debug output for parsing into an AST
  --verbose-link              Enable compiler debug output for linking
  --verbose-ir                Enable compiler debug output for Zig IR
  --verbose-llvm-ir           Enable compiler debug output for LLVM IR
  --verbose-cimport           Enable compiler debug output for C imports
  --verbose-cc                Enable compiler debug output for C compilation
  --verbose-llvm-cpu-features Enable compiler debug output for LLVM CPU features

Можно увидеть, что один из доступных шагов — это run.

$ zig build run
All your base are belong to us.

Вот несколько примеров скриптов сборки:

Конкурентность через асинхронные функции

В Zig 0.5.0 появились асинхронные функции. Они не зависят от операционной системы или даже от памяти в куче. Это значит, что асинхронные функции доступны и для независимых целевых платформ.

Zig автоматически понимает, является ли функция асинхронной, и позволяет использовать async/await для неасинхронных функций, что в свою очередь означает, что библиотеки Zig независимы от блокирующего и асинхронного ввода-вывода. Zig избегает окраски функций.

Стандартная библиотека Zig реализует цикл событий (event loop), который распределяет асинхронные функции по пулу потоков для достижения M:N конкурентности. Безопасность многопоточности и обнаружение гонок находятся в стадии активной разработки и исследования.

Широкий спектр поддерживаемых целевых платформ

Zig применяет систему "уровней поддержки" для обозначения уровня поддержки различных целевых платформ.

Таблица поддержки по состоянию на Zig 0.11.0

Дружелюбное отношение к мейнтейнерам пакетов

Эталонная версия компилятора Zig пока ещё не является полностью самодостаточной, однако для перехода от системного C++ компилятора к полностью самодостаточному компилятору Zig для любой целевой платформы потребуется всего 3 шага. Майя Рашиш отмечает, что портирование Zig на другие платформы — это увлекательный и быстрый процесс.

Режимы сборки без отладки воспроизводимы/детерминированы.

Есть JSON версия страницы загрузки.

Несколько членов команды Zig имеют опыт поддержания пакетов.