zig 语言深入了解 版本 v0.11.0

Zig 是一种通用的编程语言和工具链,用于维护健壮、最优和可重用的软件

⚡ 一种简单的语言

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

  • 没有隐式控制流
  • 没有隐式内存分配
  • 没有预处理器,没有宏

⚡ 编译期代码执行

基于编译期代码执行和惰性求值的全新元编程方法

  • 编译期调用任意函数
  • 在没有运行时开销的情况下,将类型作为值进行操作
  • 编译期模拟目标架构

⚡ 用Zig维护代码

逐步改善你的C/C++/Zig代码库

  • 将Zig作为一个零依赖的,支持开箱即用交叉编译的C/C++编译器
  • 利用 zig build在所有平台上创建一个一致的开发环境
  • 在C/C++项目中添加一个Zig编译单元,跨语言LTO默认启用

深入了解

功能特色

小巧而简单的语言

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

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

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

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

隐式控制流的例子:

  • D 有 @property 函数,可以让你的方法调用看起来像是成员访问,因此在上面的例子中,c.d可能会调用一个函数。
  • C++,D和Rust 有运算符重载,因此+可能会调用一个函数。
  • C++,D和Go 可以抛出和捕获异常,因此foo()可能会抛出一个异常,并且将会阻止 bar() 被调用。

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

性能和安全:全都要

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

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

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

test.zig

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

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

test.zig

test "integer overflow at runtime" {
    var x: u8 = 255;
    x += 1;
}
$ zig test test.zig
1/1 test.integer overflow at runtime... thread 2745 panic: integer overflow
/home/runner/work/www.ziglang.org/www.ziglang.org/doctest-ac44854c/test.zig:3:7: 0x2246ee in test.integer overflow at runtime (test)
    x += 1;
      ^
/home/runner/work/www.ziglang.org/www.ziglang.org/zig/lib/test_runner.zig:181:28: 0x22d8b9 in mainTerminal (test)
        } else test_fn.func();
                           ^
/home/runner/work/www.ziglang.org/www.ziglang.org/zig/lib/test_runner.zig:36:28: 0x22573a in main (test)
        return mainTerminal();
                           ^
/home/runner/work/www.ziglang.org/www.ziglang.org/zig/lib/std/start.zig:360:22: 0x224c2c in posixCallMainAndExit (test)
    while (envp_optional[envp_count]) |_| : (envp_count += 1) {}
                     ^
/home/runner/work/www.ziglang.org/www.ziglang.org/zig/lib/std/start.zig:243:5: 0x224781 in _start (test)
    asm volatile (switch (native_arch) {
    ^
???:?:?: 0x0 in ??? (???)
error: the following test command crashed:
/home/runner/.cache/zig/o/b3ff31bae03b09705fb49703322f2aec/test

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

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

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

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

说到性能,Zig比C快:

  • 参考实现使用 LLVM 作为后端进行最先进的优化。
  • 其他项目所谓的“链接时优化”,在 Zig 是自动达成的。
  • 多亏了对交叉编译的一流支持,对于原生构建目标,高级 CPU 特性可以被启用(相当于 -march=native)。
  • 精心选择的未定义行为。例如,在 Zig 中,有符号和无符号整数在溢出时都属于未定义的行为,而在 C 中仅有有符号整数的溢出属于未定义行为,这有助于实现 C 语言里没有的优化
  • Zig直接暴露了SIMD 向量类型,使得编写跨平台的向量化代码更容易。

请注意,Zig不是一个完全安全的语言。有兴趣关注 Zig 安全故事的用户,可以订阅下面这些链接:

Zig与C竞争,而不是依赖于它

Zig标准库里集成了libc,但是不依赖于它:

hello.zig

const std = @import("std");<
>>
pub fn main() void {
    std.debug.print("Hello, world!\n", .{});
}
$ zig build-exe hello.zig
$ ./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

顺序无关的顶层声明

全局变量等顶层声明与顺序无关,并进行惰性分析。全局变量的初始值在编译时进行求值

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;
$ zig test global_variables.zig
1/1 test.global variables... OK
All 1 tests passed.

用可选类型代替空指针

在其他编程语言中,空引用是许多运行时异常的来源,甚至被指责为计算机科学中最严重的错误

不加修饰的 Zig 指针不可为空:

test "null @intToPtr" {
    const foo: *i32 = @ptrFromInt(0x0);
    _ = foo;
}
$ zig test test.zig
doctest-6a1fbdc6/test.zig:2:35: error: pointer type '*i32' does not allow address zero
    const foo: *i32 = @ptrFromInt(0x0);
                                  ^~~

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

const std = @import("std");
const assert = std.debug.assert;
test "null @intToPtr" {
    const ptr: ?*i32 = @ptrFromInt(0x0);
    assert(ptr == null);
}
$ zig test optional_syntax.zig
1/1 test.null @intToPtr... OK
All 1 tests passed.

要解开一个可选的值,可以使用 orelse来提供一个默认值:

// malloc prototype included for reference
extern fn malloc(size: size_t) ?*u8;
fn doAThing() ?*Foo {
    const ptr = malloc(1234) orelse return null;
    // ...
}

另一种选择是使用 if:

fn doAThing(optional_foo: ?*Foo) void {
    // do some stuff
    if (optional_foo) |foo| {
        doSomethingWithFoo(foo);
    }
    // do some stuff
}

相同的语法也可以在 while 种使用:

iterator.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});
    }
}
$ zig build-exe iterator.zig
$ ./iterator
hello
this
is
dog

手动内存管理

用Zig编写的库可以在任何地方使用:

为了达到这个目的,Zig程序员必须管理自己的内存,必须处理内存分配失败。

Zig标准库也是如此。任何需要分配内存的函数都会接受一个分配器参数。因此,Zig标准库甚至可以用于裸金属(freestanding)的目标。

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

关于defer的例子,请看无需FFI/bindings的C库集成。下面是一个使用errdefer的例子:

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;
    }
};

错误处理的全新诠释

错误是值,不可忽略:

discard.zig

const std = @import("std");<
>>
pub fn main() void {
    _ = std.fs.cwd().openFile("does_not_exist/foo.txt", .{});
}
$ zig build-exe discard.zig
doctest-f1fb3d37/discard.zig:4:30: error: error is discarded
    _ = std.fs.cwd().openFile("does_not_exist/foo.txt", .{});
        ~~~~~~~~~~~~~~~~~~~~~^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
doctest-f1fb3d37/discard.zig:4:30: note: consider using 'try', 'catch', or 'if'
referenced by:
    callMain: zig/lib/std/start.zig:564:17
    initEventLoopAndCallMain: zig/lib/std/start.zig:508:34
    remaining reference traces hidden; use '-freference-trace' to see all reference traces

错误可以被 catch 所处理:

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;
}
$ zig build-exe catch.zig
$ ./catch
unable to open file: error.FileNotFound
all your codebase are belong to us

关键词 try 是 catch |err| return err 的简写:

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");
}
$ zig build-exe try.zig
$ ./try
error: FileNotFound
/home/runner/work/www.ziglang.org/www.ziglang.org/zig/lib/std/os.zig:1719:23: 0x24c646 in openatZ (try)
            .NOENT => return error.FileNotFound,
                      ^
/home/runner/work/www.ziglang.org/www.ziglang.org/zig/lib/std/fs.zig:1196:13: 0x22147b in openFileZ (try)
            try os.openatZ(self.fd, sub_path, os_flags, 0);
            ^
/home/runner/work/www.ziglang.org/www.ziglang.org/zig/lib/std/fs.zig:1128:9: 0x21e8ae in openFile (try)
        return self.openFileZ(&path_c, flags);
        ^
/home/runner/work/www.ziglang.org/www.ziglang.org/doctest-c4b703da/try.zig:4:18: 0x21e6ed in main (try)
    const file = try std.fs.cwd().openFile("does_not_exist/foo.txt", .{});
                 ^

请注意这是一个 错误返回跟踪 ,而不是 堆栈跟踪。代码没有付出解开堆栈的代价来获得该跟踪。

在错误值上使用 switch 关键词可以用于确保所有可能的错误都被处理:

test.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;
}
$ zig test test.zig
doctest-5bf70dea/test.zig:4:40: error: switch must handle all possibilities
    _ = parseInt("hi", 10) catch |err| switch (err) {};
                                       ^~~~~~~~~~~~~~~
doctest-5bf70dea/test.zig:4:40: note: unhandled error value: 'error.InvalidCharacter'
doctest-5bf70dea/test.zig:4:40: note: unhandled error value: 'error.DigitExceedsRadix'
doctest-5bf70dea/test.zig:4:40: note: unhandled error value: 'error.Overflow'

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

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;
}
$ zig build-exe unreachable.zig
$ ./unreachable
thread 2838 panic: attempt to unwrap error: FileNotFound
/home/runner/work/www.ziglang.org/www.ziglang.org/zig/lib/std/os.zig:1719:23: 0x24edd6 in openatZ (unreachable)
            .NOENT => return error.FileNotFound,
                      ^
/home/runner/work/www.ziglang.org/www.ziglang.org/zig/lib/std/fs.zig:1196:13: 0x22284b in openFileZ (unreachable)
            try os.openatZ(self.fd, sub_path, os_flags, 0);
            ^
/home/runner/work/www.ziglang.org/www.ziglang.org/zig/lib/std/fs.zig:1128:9: 0x22088e in openFile (unreachable)
        return self.openFileZ(&path_c, flags);
        ^
/home/runner/work/www.ziglang.org/www.ziglang.org/doctest-8a6e2cef/unreachable.zig:4:77: 0x21eb49 in main (unreachable)
    const file = std.fs.cwd().openFile("does_not_exist/foo.txt", .{}) catch unreachable;
                                                                            ^
/home/runner/work/www.ziglang.org/www.ziglang.org/zig/lib/std/start.zig:360:22: 0x21e35c in posixCallMainAndExit (unreachable)
    while (envp_optional[envp_count]) |_| : (envp_count += 1) {}
                     ^
/home/runner/work/www.ziglang.org/www.ziglang.org/zig/lib/std/start.zig:243:5: 0x21deb1 in _start (unreachable)
    asm volatile (switch (native_arch) {
    ^
???:?:?: 0x0 in ??? (???)
(process terminated by signal)

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

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

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

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

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);
}
$ zig build-exe stack_traces.zig
$ ./stack_traces
first one:
/home/runner/work/www.ziglang.org/www.ziglang.org/zig/lib/std/debug.zig:295:29: 0x222535 in captureStackTrace (stack_traces)
            addr.* = it.next() orelse {
                            ^
/home/runner/work/www.ziglang.org/www.ziglang.org/doctest-012874c6/stack_traces.zig:26:32: 0x2208dc in foo (stack_traces)
    std.debug.captureStackTrace(null, &trace1);
                               ^
/home/runner/work/www.ziglang.org/www.ziglang.org/doctest-012874c6/stack_traces.zig:16:8: 0x21ecb8 in main (stack_traces)
    foo();
       ^
/home/runner/work/www.ziglang.org/www.ziglang.org/zig/lib/std/start.zig:360:22: 0x21e57c in posixCallMainAndExit (stack_traces)
    while (envp_optional[envp_count]) |_| : (envp_count += 1) {}
                     ^
second one:
/home/runner/work/www.ziglang.org/www.ziglang.org/zig/lib/std/debug.zig:295:29: 0x222535 in captureStackTrace (stack_traces)
            addr.* = it.next() orelse {
                            ^
/home/runner/work/www.ziglang.org/www.ziglang.org/doctest-012874c6/stack_traces.zig:30:32: 0x2208fc in bar (stack_traces)
    std.debug.captureStackTrace(null, &trace2);
                               ^
/home/runner/work/www.ziglang.org/www.ziglang.org/doctest-012874c6/stack_traces.zig:17:8: 0x21ecbd in main (stack_traces)
    bar();
       ^
/home/runner/work/www.ziglang.org/www.ziglang.org/zig/lib/std/start.zig:360:22: 0x21e57c in posixCallMainAndExit (stack_traces)
    while (envp_optional[envp_count]) |_| : (envp_count += 1) {}
                     ^

你可以在正在进行的 GeneralPurposeDebugAllocator 项目中看到这种技术的应用。

泛型数据结构和函数

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

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);
}
$ zig test types.zig
1/1 test.types are values... OK
All 1 tests passed.

泛型数据结构简单来说就是一个函数返回一个类型:

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,
    };
    std.debug.print("{d}\n", .{list.items.len});
}
$ zig build-exe generics.zig
$ ./generics
10

编译期反射和编译期代码执行

@typeInfo 内置函数可以用于提供编译期反射:

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),
            },
        );
    }
}
$ zig build-exe reflection.zig
$ ./reflection
reflection.Header has a field called magic with type u32
reflection.Header has a field called name with type []const u8

Zig 标准库使用这种技术来实现格式化打印。尽管是一种 小巧而简洁的语言,但 Zig 的格式化打印完全是在 Zig 中实现的。同时,在 C 语言中,printf 的编译错误是硬编码到编译器中的。同样,在 Rust 中,格式化打印的宏也是硬编码到编译器中的。

Zig 还可以在编译期对函数和代码块求值。在某些情况下,比如全局变量初始化,表达式会在编译期隐式地进行求值。除此之外我们还可以使用 comptime 关键字显式地在编译期求值。把它与断言相结合就可以变得尤为强大了:

test.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;
    comptime {
        assert(array.len == 12345);
    }
}
$ zig test test.zig
zig/lib/std/debug.zig:342:14: error: reached unreachable code
    if (!ok) unreachable; // assertion failure
             ^~~~~~~~~~~
doctest-e63e98ca/test.zig:13:15: note: called from here
        assert(array.len == 12345);
        ~~~~~~^~~~~~~~~~~~~~~~~~~~

无需 FFI/bindings 的 C 库集成

@cImport 可以为Zig直接导入类型,变量,函数和简单的宏。它甚至能将C内联函数翻译成Zig函数。

这是一个利用 libsoundio 库发出正弦波的例子:

sine.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头文件来实现的——无需 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

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

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。

hello.zig

const std = @import("std");<
>>
pub fn main() void {
    std.debug.print("Hello, world!\n", .{});
}
$ zig build-exe hello.zig
$ ./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": [
  "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",
  "i386-linux-gnu",
  "i386-linux-musl",
  "i386-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_64-linux-gnu",
  "x86_64-linux-gnux32",
  "x86_64-linux-musl",
  "x86_64-windows-gnu"
 ],

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

让我们再看看C语言Hello, World例子

$ 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的缓存仍然有效,所以当再次需要这个libc的时候,它就会被立即使用。

这意味着这个功能可以在任何平台上使用。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捆绑在一起。例如,Zig提供的libc头文件总大小为22MiB,未压缩。同时,仅 x86_64 上的musl libc+linux头文件就有8MiB,glibc有3.1 MiB(glibc 不包括Linux头文件),然而Zig目前附带的libc有40个。如果是天真的捆绑发布,那就是 444 MiB。然而,多亏了我做的这个 process_headers 工具,以及一些老式的手工劳动,Zig 二进制压缩包的总容量仍然只有 30 MiB,尽管它支持所有这些目标的 libc,以及compiler-rt、libunwind和libcxx,尽管它是一个Clang兼容的C编译器。作为比较,来自 llvm.org 的 clang 8.0.0 本身的 Windows二进制构建有132MiB这么大。

请注意,只有一级支持目标得到了彻底测试。我们有计划增加更多的 libc(包括Windows平台),并提高对所有libc的测试覆盖率

我们还计划有一个 Zig 包管理器,但还没有完成。其中一个功能是可以为 C 库创建一个包,这将使 Zig 构建系统对 Zig 程序员和 C 程序员都有吸引力。

Zig 构建系统

Zig自带构建系统,所以你不需要make、cmake之类的东西。

$ zig init-exe
Created build.zig
Created src/main.zig
Next, try `zig build --help` or `zig build run`

src/main.zig

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

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

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

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

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

使用异步函数进行并发

Zig 0.5.0引入了异步函数(英文)。该功能不依赖于宿主操作系统,甚至不依赖于堆分配的内存。这意味着异步函数可以用于裸金属(freestanding)目标。

Zig 自动推导函数是否为异步,并允许在非异步函数上进行 async/await,这意味着 Zig 库对阻塞与异步 I/O 是不可知的Zig 避免了函数染色(英文)

Zig 标准库实现了一个事件循环,将异步函数复用到线程池上,实现 M:N 并发。多线程安全和竞争检测是尚在积极研究的领域。

支持广泛的目标

Zig 使用“支持等级”系统来描述不同目标的支持程度。需要注意的是,一级支持的门槛很高——二级支持还是相当有用的。

支持表

free standingLinux 3.16+macOS 10.13+Windows 8.1+FreeBSD 12.0+NetBSD 8.0+DragonFly​BSD 5.8+UEFIx86_64一级支持一级支持一级支持二级支持二级支持二级支持二级支持二级支持arm64一级支持二级支持二级支持三级支持三级支持三级支持未知三级支持arm32一级支持二级支持未知三级支持三级支持三级支持未知三级支持mips32 LE一级支持二级支持未知未知三级支持三级支持未知未知i386一级支持二级支持四级支持二级支持三级支持三级支持未知二级支持riscv64一级支持二级支持未知未知三级支持三级支持未知三级支持bpf三级支持三级支持未知未知三级支持三级支持未知未知hexagon三级支持三级支持未知未知三级支持三级支持未知未知mips32 BE三级支持三级支持未知未知三级支持三级支持未知未知mips64三级支持三级支持未知未知三级支持三级支持未知未知amdgcn三级支持三级支持未知未知三级支持三级支持未知未知sparc三级支持三级支持未知未知三级支持三级支持未知未知s390x三级支持三级支持未知未知三级支持三级支持未知未知lanai三级支持三级支持未知未知三级支持三级支持未知未知powerpc32三级支持三级支持四级支持未知三级支持三级支持未知未知powerpc64三级支持三级支持四级支持未知三级支持三级支持未知未知avr四级支持四级支持未知未知四级支持四级支持未知未知riscv32四级支持四级支持未知未知四级支持四级支持未知四级支持xcore四级支持四级支持未知未知四级支持四级支持未知未知nvptx四级支持四级支持未知未知四级支持四级支持未知未知msp430四级支持四级支持未知未知四级支持四级支持未知未知r600四级支持四级支持未知未知四级支持四级支持未知未知arc四级支持四级支持未知未知四级支持四级支持未知未知tce四级支持四级支持未知未知四级支持四级支持未知未知le四级支持四级支持未知未知四级支持四级支持未知未知amdil四级支持四级支持未知未知四级支持四级支持未知未知hsail四级支持四级支持未知未知四级支持四级支持未知未知spir四级支持四级支持未知未知四级支持四级支持未知未知kalimba四级支持四级支持未知未知四级支持四级支持未知未知shave四级支持四级支持未知未知四级支持四级支持未知未知renderscript四级支持四级支持未知未知四级支持四级支持未知未知

WebAssembly 支持表

free standingemscriptenWASIwasm32一级支持三级支持一级支持wasm64四级支持四级支持四级支持

支持等级

一级支持

  • Zig不仅可以为这些目标生成机器代码,而且标准库的跨平台抽象也有这些目标的实现。因此,写一个不依赖 libc 的纯 Zig 应用是可行的。
  • CI 服务器在每次提交到主分支时都会自动测试这些目标,并在下载页面更新预建二进制文件的链接。
  • 这些目标具有调试信息功能,因此在失败的断言时产生堆栈跟踪
  • 即使在交叉编译时,libc 也可以用于这些目标。
  • 所有的行为测试和适用的标准库测试都在这些目标上通过,所有的语言功能都能正常工作。

二级支持

  • 标准库支持这个目标,但有可能一些 API 会给出“Unsupported OS”的编译错误。可以用 libc 或其他库链接来填补标准库的空白。
  • 这些目标都是已知的,但可能不会自动测试,所以偶尔会有倒退。
  • 随着我们对一级支持的努力,这些目标的一些测试可能会被禁用。

三级支持

  • 标准库对这个目标的存在几乎一无所知。
  • 因为Zig是基于LLVM的,所以它有能力为这些目标构建,而LLVM默认启用了目标。
  • 这些目标并不经常被测试:人们可能需要为Zig做出贡献,以便为这些目标构建。
  • Zig 编译器可能需要更新一些东西,如
  • C 整数类型的大小是多少
  • 这个目标的 C ABI 调用约定是什么
  • bootstrap代码和默认panic处理函数
  • zig targets 保证包含这个目标。

四级支持

  • 对这些目标的支持完全是试验性的。
  • LLVM可能会将目标作为实验性目标,这意味着你需要使用 Zig 提供的二进制文件来使目标可用,或者使用特殊的配置标志从源码构建 LLVM ,如果目标可用,zig targets 将显示目标。
  • 这个目标可能会被官方认为是废弃的,比如 macos/i386,在这种情况下,这个目标将永远停留在四级。
  • 这个目标可能只支持输出汇编(--emit asm),而不支持输出对象文件。

对包维护者友好

虽然 Zig 编译器还没有完全自托管,但无论如何,从拥有一个系统 C++ 编译器到拥有一个适用于任何目标的完全自托管的 Zig 编译器,将保持正好 3 步。正如 Maya Rashish 所指出的那样,将 Zig 移植到其他平台是有趣且快速的

调试模式的构建是可重现/确定的。

这是JSON格式的下载页面

Zig 团队的几位成员都有维护软件包的经验。



© GVGNN 2013-2026