← 返回 学习

深入了解

功能特色

小巧而简单的语言

专注于调试你的应用程序,而不是调试你的编程语言知识。

Zig 的完整语法可以被 580 行的 PEG 语法文件所描述。

没有隐式控制流,没有隐式内存分配,没有预处理器,也没有宏。如果 Zig 代码看起来不像是在调用一个函数,那么它就不是。这意味着你可以确定下面的代码只会先调用 foo(),然后调用 bar(),不需要知道任何元素的类型,这一点也是可以保证的:

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

隐式控制流的例子:

Zig 将所有的控制流完全用语言关键字和函数调用来表达,以此促进代码的维护性和可读性。

性能和安全:全都要

Zig 有 4 种构建模式,它们可以从全局到代码作用域的粒度下被任意混合以匹配需求。

参数DebugReleaseSafeReleaseFastReleaseSmall
优化 - 提升运行速度,降低可调试能力,减慢编译期间OnOnOn
运行时安全检查 - 降低运行速度,增大体积,用崩溃代替未定义行为OnOn

以下是编译期整数溢出的例子,无关编译模式选择:

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

这是在启用了安全检查的构建中运行时的场景:

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 125288 panic: integer overflow
/home/ci/.cache/act/dc871dc959025405/hostexecutor/zig-code/features/2-integer-overflow-runtime.zig:3:7: 0x1238175 in test.integer overflow at runtime (2-integer-overflow-runtime.zig)
    x += 1;
      ^
/home/ci/deps/zig-x86_64-linux-0.16.0/lib/compiler/test_runner.zig:291:25: 0x11f35c6 in mainTerminal (test_runner.zig)
        if (test_fn.func()) |_| {
                        ^
/home/ci/deps/zig-x86_64-linux-0.16.0/lib/compiler/test_runner.zig:73:28: 0x11f2de2 in main (test_runner.zig)
        return mainTerminal(init);
                           ^
/home/ci/deps/zig-x86_64-linux-0.16.0/lib/std/start.zig:699:88: 0x11ef5f5 in callMain (std.zig)
    if (fn_info.params[0].type.? == std.process.Init.Minimal) return wrapMain(root.main(.{
                                                                                       ^
/home/ci/deps/zig-x86_64-linux-0.16.0/lib/std/start.zig:190:5: 0x11eefd1 in _start (std.zig)
    asm volatile (switch (native_arch) {
    ^
error: the following test command terminated with signal ABRT:
/home/ci/.cache/act/dc871dc959025405/hostexecutor/.zig-cache/o/7e2c031adc3bbd86c484a64a15390fa3/test --seed=0xb3fdb21

这些栈跟踪在所有目标上可用,包括裸机(freestanding)

有了 Zig,人们可以依赖启用安全检查的构建模式,并在性能瓶颈处选择性地禁用安全检查。例如前面的例子可以这样修改:

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

Zig 将未定义行为作为一个利器,既可以预防 bug,又可以提升性能。

说到性能,Zig 比 C 快。

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 并移除调试符号,单线程模式构建,可以产生一个以 x86_64-linux 为目标的 9.8 KiB 的静态可执行文件:

$ 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
/home/ci/.cache/act/dc871dc959025405/hostexecutor/zig-code/features/6-null-to-ptr.zig:2:35: error: pointer type '*i32' does not allow address zero
    const foo: *i32 = @ptrFromInt(0x0);
                                  ^~~

当然,任何类型都可以通过在前面加上 ? 来变成一个可选类型

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.tokenizeAny(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 标准库甚至可以用于裸机(freestanding)的目标。

除了对错误处理的全新诠释,Zig 还提供了 defererrdefer,使所有的资源管理——不仅仅是内存——变得简单且易于验证。

defer 的一个例子,请看无须 FFI/bindings 的 C 库集成。这是一个使用 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/.cache/act/dc871dc959025405/hostexecutor/zig-code/features/12-errors-as-values.zig:4:15: error: root source file struct 'fs' has no member named 'cwd'
    _ = std.fs.cwd().openFile("does_not_exist/foo.txt", .{});
        ~~~~~~^~~~
/home/ci/deps/zig-x86_64-linux-0.16.0/lib/std/fs.zig:1:1: note: struct declared here
//! File System.
^~~~~~~~~~~~~~~~
referenced by:
    callMain [inlined]: /home/ci/deps/zig-x86_64-linux-0.16.0/lib/std/start.zig:698:59
    callMainWithArgs [inlined]: /home/ci/deps/zig-x86_64-linux-0.16.0/lib/std/start.zig:638:20
    posixCallMainAndExit: /home/ci/deps/zig-x86_64-linux-0.16.0/lib/std/start.zig:590:38
    2 reference(s) hidden; use '-freference-trace=5' to see all references

错误可以被 catch 所处理:

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

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

    const file: Io.File = Io.Dir.cwd().openFile(io, "does_not_exist/foo.txt", .{}) catch |err| label: {
        std.debug.print("unable to open file: {}\n", .{err});
        break :label .stderr();
    };

    var file_writer = file.writer(io, &.{});
    file_writer.interface.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

关键词 trycatch |err| return err 的简写:

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

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

    const file = try Io.Dir.cwd().openFile(io, "does_not_exist/foo.txt", .{});
    defer file.close(io);

    var file_writer = file.writer(io, &.{});
    try file_writer.interface.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-x86_64-linux-0.16.0/lib/std/Io/Threaded.zig:4866:35: 0x11905c0 in dirOpenFilePosix (std.zig)
                        .NOENT => return error.FileNotFound,
                                  ^
/home/ci/deps/zig-x86_64-linux-0.16.0/lib/std/Io/Dir.zig:578:5: 0x1033170 in openFile (std.zig)
    return io.vtable.dirOpenFile(io.userdata, dir, sub_path, options);
    ^
/home/ci/.cache/act/dc871dc959025405/hostexecutor/zig-code/features/14-errors-try.zig:7:18: 0x11d6b88 in main (14-errors-try.zig)
    const file = try Io.Dir.cwd().openFile(io, "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 test 15-errors-switch.zig
/home/ci/.cache/act/dc871dc959025405/hostexecutor/zig-code/features/15-errors-switch.zig:4:40: error: switch must handle all possibilities
    _ = parseInt("hi", 10) catch |err| switch (err) {};
                                       ^~~~~~~~~~~~~~~
/home/ci/.cache/act/dc871dc959025405/hostexecutor/zig-code/features/15-errors-switch.zig:4:40: note: unhandled error value: 'error.Overflow'
/home/ci/.cache/act/dc871dc959025405/hostexecutor/zig-code/features/15-errors-switch.zig:4:40: note: unhandled error value: 'error.InvalidCharacter'
/home/ci/.cache/act/dc871dc959025405/hostexecutor/zig-code/features/15-errors-switch.zig:4:40: note: unhandled error value: 'error.DigitExceedsRadix'

而关键词 unreachable 用于断言不会发生错误:

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

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

    const file = Io.Dir.cwd().openFile(io, "does_not_exist/foo.txt", .{}) catch unreachable;
    defer file.close(io);

    var file_writer = file.writer(io, &.{});
    file_writer.interface.writeAll("all your codebase are belong to us\n") catch unreachable;
}
Shell
$ zig build-exe 16-unreachable.zig
$ ./16-unreachable
thread 125316 panic: attempt to unwrap error: FileNotFound
error return context:
/home/ci/deps/zig-x86_64-linux-0.16.0/lib/std/Io/Threaded.zig:4866:35: 0x11905c0 in dirOpenFilePosix (std.zig)
                        .NOENT => return error.FileNotFound,
                                  ^
/home/ci/deps/zig-x86_64-linux-0.16.0/lib/std/Io/Dir.zig:578:5: 0x1033170 in openFile (std.zig)
    return io.vtable.dirOpenFile(io.userdata, dir, sub_path, options);
    ^

stack trace:
/home/ci/.cache/act/dc871dc959025405/hostexecutor/zig-code/features/16-unreachable.zig:7:81: 0x11dc981 in main (16-unreachable.zig)
    const file = Io.Dir.cwd().openFile(io, "does_not_exist/foo.txt", .{}) catch unreachable;
                                                                                ^
/home/ci/deps/zig-x86_64-linux-0.16.0/lib/std/start.zig:737:30: 0x11d73e3 in callMain (std.zig)
    return wrapMain(root.main(.{
                             ^
/home/ci/deps/zig-x86_64-linux-0.16.0/lib/std/start.zig:190:5: 0x11d6a71 in _start (std.zig)
    asm volatile (switch (native_arch) {
    ^
(process terminated by signal)

这将会在不安全构建中出现未定义行为,因此请确保只在一定会成功的地方使用。

在所有目标上启用堆栈跟踪

本页所展示的堆栈跟踪和错误返回跟踪适用于所有一级支持和部分二级支持目标,甚至裸机(freestanding)目标

此外,标准库能在任何一点捕获堆栈跟踪,然后将其转储为标准错误:

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

var address_buffer: [8]usize = undefined;

var trace1: std.debug.StackTrace = .{
    .return_addresses = address_buffer[0..4],
    .skipped = .none,
};

var trace2: std.debug.StackTrace = .{
    .return_addresses = address_buffer[4..],
    .skipped = .none,
};

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 {
    trace1 = std.debug.captureCurrentStackTrace(.{}, address_buffer[0..4]);
}

fn bar() void {
    trace2 = std.debug.captureCurrentStackTrace(.{}, address_buffer[4..]);
}
Shell
$ zig build-exe 17-stack-traces.zig
$ ./17-stack-traces
first one:
/home/ci/.cache/act/dc871dc959025405/hostexecutor/zig-code/features/17-stack-traces.zig:26:48: 0x11d8c1f in foo (17-stack-traces.zig)
    trace1 = std.debug.captureCurrentStackTrace(.{}, address_buffer[0..4]);
                                               ^
/home/ci/.cache/act/dc871dc959025405/hostexecutor/zig-code/features/17-stack-traces.zig:16:8: 0x11d772c in main (17-stack-traces.zig)
    foo();
       ^
/home/ci/deps/zig-x86_64-linux-0.16.0/lib/std/start.zig:698:59: 0x11d7041 in callMain (std.zig)
    if (fn_info.params.len == 0) return wrapMain(root.main());
                                                          ^
/home/ci/deps/zig-x86_64-linux-0.16.0/lib/std/start.zig:190:5: 0x11d6a71 in _start (std.zig)
    asm volatile (switch (native_arch) {
    ^
(additional stack frames may have been skipped...)


second one:
/home/ci/.cache/act/dc871dc959025405/hostexecutor/zig-code/features/17-stack-traces.zig:30:48: 0x11d7d2f in bar (17-stack-traces.zig)
    trace2 = std.debug.captureCurrentStackTrace(.{}, address_buffer[4..]);
                                               ^
/home/ci/.cache/act/dc871dc959025405/hostexecutor/zig-code/features/17-stack-traces.zig:17:8: 0x11d7731 in main (17-stack-traces.zig)
    bar();
       ^
/home/ci/deps/zig-x86_64-linux-0.16.0/lib/std/start.zig:698:59: 0x11d7041 in callMain (std.zig)
    if (fn_info.params.len == 0) return wrapMain(root.main());
                                                          ^
/home/ci/deps/zig-x86_64-linux-0.16.0/lib/std/start.zig:190:5: 0x11d6a71 in _start (std.zig)
    asm volatile (switch (native_arch) {
    ^
(additional stack frames may have been skipped...)

标准库的 DebugAllocator 使用了这种技巧以汇报泄露与二次释放。

泛型数据结构和函数

类型和值必须在编译期已知:

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 关键字显式地在编译期求值。把它与断言相结合就可以变得尤为强大了:

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-x86_64-linux-0.16.0/lib/std/debug.zig:420:14: error: reached unreachable code
    if (!ok) unreachable; // assertion failure
             ^~~~~~~~~~~
/home/ci/.cache/act/dc871dc959025405/hostexecutor/zig-code/features/21-comptime.zig:15:15: note: called at comptime here
        assert(array.len == 12345);
        ~~~~~~^~~~~~~~~~~~~~~~~~~~

无需 FFI/bindings 的 C 库集成

@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 = @ptrCast(@alignCast(&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 头文件来实现的——无需 API 绑定。

Zig 比 C 更擅长使用 C 库。

Zig 也是 C 编译器

这有一个简单的使用 Zig 编译 C 代码的例子:

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 导出一个库,供其他编程语言调用。在函数、变量和类型前面的 export 关键字会使它们成为库 API 的一部分:

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 可以为支持表(查看最新发行说明)中的任何三级支持或更高的目标构建。不需要安装“交叉编译工具链”之类的东西。这是一个原生的 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

在任意三级支持以上的目标平台,都可以构建任何三级支持以上的目标。

Zig 与 libc 一起发布

你可以通过 zig targets 命令获得可用的 libc 目标:

...
.libc = .{
  "arc-linux-gnu",
  "arm-freebsd-eabihf",
  "arm-linux-gnueabi",
  "arm-linux-gnueabihf",
  "arm-linux-musleabi",
  "arm-linux-musleabihf",
  "arm-netbsd-eabi",
  "arm-netbsd-eabihf",
  "armeb-linux-gnueabi",
  "armeb-linux-gnueabihf",
  "armeb-linux-musleabi",
  "armeb-linux-musleabihf",
  "armeb-netbsd-eabi",
  "armeb-netbsd-eabihf",
  "thumb-linux-musleabi",
  "thumb-linux-musleabihf",
  "thumb-windows-gnu",
  "thumbeb-linux-musleabi",
  "thumbeb-linux-musleabihf",
  "aarch64-freebsd-none",
  "aarch64-linux-gnu",
  "aarch64-linux-musl",
  "aarch64-maccatalyst-none",
  "aarch64-macos-none",
  "aarch64-netbsd-none",
  "aarch64-windows-gnu",
  "aarch64_be-linux-gnu",
  "aarch64_be-linux-musl",
  "aarch64_be-netbsd-none",
  "csky-linux-gnueabi",
  "csky-linux-gnueabihf",
  "hexagon-linux-musl",
  "loongarch64-linux-gnu",
  "loongarch64-linux-gnusf",
  "loongarch64-linux-musl",
  "loongarch64-linux-muslsf",
  "m68k-linux-gnu",
  "m68k-linux-musl",
  "m68k-netbsd-none",
  "mips-linux-gnueabi",
  "mips-linux-gnueabihf",
  "mips-linux-musleabi",
  "mips-linux-musleabihf",
  "mips-netbsd-eabi",
  "mips-netbsd-eabihf",
  "mipsel-linux-gnueabi",
  "mipsel-linux-gnueabihf",
  "mipsel-linux-musleabi",
  "mipsel-linux-musleabihf",
  "mipsel-netbsd-eabi",
  "mipsel-netbsd-eabihf",
  "mips64-linux-gnuabi64",
  "mips64-linux-gnuabin32",
  "mips64-linux-muslabi64",
  "mips64-linux-muslabin32",
  "mips64el-linux-gnuabi64",
  "mips64el-linux-gnuabin32",
  "mips64el-linux-muslabi64",
  "mips64el-linux-muslabin32",
  "powerpc-linux-gnueabi",
  "powerpc-linux-gnueabihf",
  "powerpc-linux-musleabi",
  "powerpc-linux-musleabihf",
  "powerpc-netbsd-eabi",
  "powerpc-netbsd-eabihf",
  "powerpc64-freebsd-none",
  "powerpc64-linux-gnu",
  "powerpc64-linux-musl",
  "powerpc64le-freebsd-none",
  "powerpc64le-linux-gnu",
  "powerpc64le-linux-musl",
  "riscv32-linux-gnu",
  "riscv32-linux-musl",
  "riscv64-freebsd-none",
  "riscv64-linux-gnu",
  "riscv64-linux-musl",
  "s390x-linux-gnu",
  "s390x-linux-musl",
  "sparc-linux-gnu",
  "sparc-netbsd-none",
  "sparc64-linux-gnu",
  "sparc64-netbsd-none",
  "wasm32-wasi-musl",
  "x86-freebsd-none",
  "x86-linux-gnu",
  "x86-linux-musl",
  "x86-netbsd-none",
  "x86-windows-gnu",
  "x86_64-freebsd-none",
  "x86_64-linux-gnu",
  "x86_64-linux-gnux32",
  "x86_64-linux-musl",
  "x86_64-linux-muslx32",
  "x86_64-maccatalyst-none",
  "x86_64-macos-none",
  "x86_64-netbsd-none",
  "x86_64-windows-gnu",
},
...

这意味着在这些目标上使用 -lc不依赖任何系统文件

让我们再看看 C 语言 Hello World 示例

$ zig build-exe hello.c -lc
$ ./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 -lc -target x86_64-linux-musl
$ ./hello
Hello world
$ ldd hello
  not a dynamic executable

在这个例子中,Zig 从源码构建 musl libc 然后将其链接到输出文件中。由于缓存系统,musl libc 的缓存仍然有效,所以当再次需要这个 libc 的时候,它就会被立即使用。

这意味着这个功能可以在任何平台上使用。Windows 和 macOS 用户可以为上面列出的任何目标构建 Zig 和 C 代码,并与 libc 链接。同样的代码也可以为其他架构交叉编译:

$ zig build-exe hello.c -lc -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 捆绑在一起。例如,Zig 提供的 libc 头文件未压缩时总大小为 130 MiB。同时,仅 x86_64 上的 musl libc 和 Linux 头文件就有 8 MiB,glibc 有 3.1 MiB(glibc 缺少 Linux 头文件),而 Zig 目前提供了 97 个 libc。如果采用简单的捆绑方式,Zig 的体积将达到 776 MiB。尽管 Zig 支持所有这些目标的 libc,以及 compiler-rt、libunwind 和 libcxx,而且尽管它还是一个 Clang 兼容的 C 编译器,但多亏了 process_headers 工具,以及一些体力劳动,Zig 二进制压缩包的总容量仍然只有大约 50 MiB。相比之下,llvm.org 提供的的 clang 8.0.0 本身的 Windows 二进制包就有 132 MiB 之大。

Zig 构建系统与包管理器

Zig 自带构建系统,所以你不需要单独的管理构建过程的工具。

$ zig init
info: created build.zig
info: created build.zig.zon
info: created src/main.zig
info: created src/root.zig
info: see `zig build --help` for a menu of options

src/main.zig

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

pub fn main() !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
  test                         Run tests

Project-Specific Options:
  -Dtarget=[string]            The CPU architecture, OS, and ABI to build for
  -Dcpu=[string]               Target CPU features to add or subtract
  -Dofmt=[string]              Target object format
  -Ddynamic-linker=[string]    Path to interpreter on the target system
  -Doptimize=[enum]            Prioritize performance, safety, or binary size
                                 Supported Values:
                                   Debug
                                   ReleaseSafe
                                   ReleaseFast
                                   ReleaseSmall

System Integration Options:
  --search-prefix [path]       Add a path to look for binaries, libraries, headers
  --sysroot [path]             Set the system root directory (usually /)
  --libc [file]                Provide a file which specifies libc paths

  --system [pkgdir]            Disable package fetching; enable all integrations
  -fsys=[name]                 Enable a system integration
  -fno-sys=[name]              Disable a system integration

  Available System Integrations:                Enabled:
  (none)                                        -

General Options:
  -p, --prefix [path]          Where to install files (default: zig-out)
  --prefix-lib-dir [path]      Where to install libraries
  --prefix-exe-dir [path]      Where to install executables
  --prefix-include-dir [path]  Where to install C header files

  --release[=mode]             Request release mode, optionally specifying a
                               preferred optimization mode: fast, safe, small

  -fdarling,  -fno-darling     Integration with system-installed Darling to
                               execute macOS programs on Linux hosts
                               (default: no)
  -fqemu,     -fno-qemu        Integration with system-installed QEMU to execute
                               foreign-architecture programs on Linux hosts
                               (default: no)
  --libc-runtimes [path]       Enhances QEMU integration by providing dynamic libc
                               (e.g. glibc or musl) built for multiple foreign
                               architectures, allowing execution of non-native
                               programs that link with libc.
  -frosetta,  -fno-rosetta     Rely on Rosetta to execute x86_64 programs on
                               ARM64 macOS hosts. (default: no)
  -fwasmtime, -fno-wasmtime    Integration with system-installed wasmtime to
                               execute WASI binaries. (default: no)
  -fwine,     -fno-wine        Integration with system-installed Wine to execute
                               Windows programs on Linux hosts. (default: no)

  -h, --help                   Print this help and exit
  -l, --list-steps             Print available steps
  --verbose                    Print commands before executing them
  --color [auto|off|on]        Enable or disable colored error messages
  --error-style [style]        Control how build errors are printed
    verbose                    (Default) Report errors with full context
    minimal                    Report errors after summary, excluding context like command lines
    verbose_clear              Like 'verbose', but clear the terminal at the start of each update
    minimal_clear              Like 'minimal', but clear the terminal at the start of each update
  --multiline-errors [style]   Control how multi-line error messages are printed
    indent                     (Default) Indent non-initial lines to align with initial line
    newline                    Include a leading newline so that the error message is on its own lines
    none                       Print as usual so the first line is misaligned
  --summary [mode]             Control the printing of the build summary
    all                        Print the build summary in its entirety
    new                        Omit cached steps
    failures                   (Default if short-lived) Only print failed steps
    line                       (Default if long-lived) Only print the single-line summary
    none                       Do not print the build summary
  -j<N>                        Limit concurrent jobs (default is to use all CPU cores)
  --maxrss <bytes>             Limit memory usage (default is to use available memory)
  --skip-oom-steps             Instead of failing, skip steps that would exceed --maxrss
  --test-timeout <timeout>     Limit execution time of unit tests, terminating if exceeded.
                               The timeout must include a unit: ns, us, ms, s, m, h
  --fetch[=mode]               Fetch dependency tree (optionally choose laziness) and exit
    needed                     (Default) Lazy dependencies are fetched as needed
    all                        Lazy dependencies are always fetched
  --watch                      Continuously rebuild when source files are modified
  --debounce <ms>              Delay before rebuilding after changed file detected
  --webui[=ip]                 Enable the web interface on the given IP address
  --fuzz[=limit]               Continuously search for unit test failures with an optional
                               limit to the max number of iterations. The argument supports
                               an optional 'K', 'M', or 'G' suffix (e.g. '10K'). Implies
                               '--webui' when no limit is specified.
  --time-report                Force full rebuild and provide detailed information on
                               compilation time of Zig source code (implies '--webui')
     -fincremental             Enable incremental compilation
  -fno-incremental             Disable incremental compilation

Advanced Options:
  -freference-trace[=num]      How many lines of reference trace should be shown per compile error
  -fno-reference-trace         Disable reference trace
  -fallow-so-scripts           Allows .so files to be GNU ld scripts
  -fno-allow-so-scripts        (default) .so files must be ELF files
  --build-file [file]          Override path to build.zig
  --cache-dir [path]           Override path to local Zig cache directory
  --global-cache-dir [path]    Override path to global Zig cache directory
  --zig-lib-dir [arg]          Override path to Zig lib directory
  --build-runner [file]        Override path to build runner
  --seed [integer]             For shuffling dependency traversal order (default: random)
  --build-id[=style]           At a minor link-time expense, embeds a build ID in binaries
      fast                     8-byte non-cryptographic hash (COFF, ELF, WASM)
      sha1, tree               20-byte cryptographic hash (ELF, WASM)
      md5                      16-byte cryptographic hash (ELF)
      uuid                     16-byte random UUID (ELF, WASM)
      0x[hexstring]            Constant ID, maximum 32 bytes (ELF, WASM)
      none                     (default) No build ID
  --debug-log [scope]          Enable debugging the compiler
  --debug-pkg-config           Fail if unknown pkg-config flags encountered
  --debug-rt                   Debug compiler runtime libraries
  --verbose-link               Enable compiler debug output for linking
  --verbose-air                Enable compiler debug output for Zig AIR
  --verbose-llvm-ir[=file]     Enable compiler debug output for LLVM IR
  --verbose-llvm-bc=[file]     Enable compiler debug output for LLVM BC
  --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

你可以看到,其中一个可用的步骤被运行。

$ zig build run
All your base are belong to us.
Run `zig build test` to run the tests.

以下是一些构建脚本的例子:

支持广泛的目标

Zig 使用“支持等级”系统来描述不同目标的支持程度。

截至 Zig 0.15 的支持表格

对包维护者友好

即使 Zig 已经自举,从源码构建只依赖系统 C/C++ 工具链与 LLVM,使用标准 CMake 构建步骤多亏了一个基于 WebAssembly 的启动过程

对于那些想避免二进制大对象的发行版,有一个文档细致的流程,来无须二进制复现 Zig

未来,我们希望启发一个第三方来实现一个可以将此过程简化到 O(1) 的基于 C 的 Zig 解释器。

构建系统将系统集成作为一个明确意图。例如,--system <path> 标志关闭包抓取并开启所有 -fsys=[name] 选项。那些“系统集成选项”于构建脚本中可用,使包维护者与上游开发者协作。包维护者只需更少的补丁,且上游作者可以用刻意的选择回应系统集成构建配置。

非调试构建模式是可复现的/确定性的。

有一个下载页的 JSON 版本