深入了解

功能特色

小巧而简单的语言

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

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

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

var a = b + c.d;
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-73544298/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.test.integer overflow at runtime... thread 2754 panic: integer overflow
/home/runner/work/www.ziglang.org/www.ziglang.org/doctest-6ec0fa0b/test.zig:3:7: 0x1038e9e in test.integer overflow at runtime (test)
    x += 1;
      ^
/home/runner/work/www.ziglang.org/www.ziglang.org/zig/lib/compiler/test_runner.zig:158:25: 0x10442a2 in mainTerminal (test)
        if (test_fn.func()) |_| {
                        ^
/home/runner/work/www.ziglang.org/www.ziglang.org/zig/lib/compiler/test_runner.zig:35:28: 0x103a28b in main (test)
        return mainTerminal();
                           ^
/home/runner/work/www.ziglang.org/www.ziglang.org/zig/lib/std/start.zig:501:22: 0x10393c9 in posixCallMainAndExit (test)
            root.main();
                     ^
/home/runner/work/www.ziglang.org/www.ziglang.org/zig/lib/std/start.zig:253:5: 0x1038f31 in _start (test)
    asm volatile (switch (native_arch) {
    ^
???:?:?: 0x0 in ??? (???)
error: the following test command crashed:
/home/runner/.cache/zig/o/862f9f8bb2d352a332df2ef6beaa8f78/test

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

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

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

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

说到性能,Zig比C快:

请注意,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 global_variables.test.global variables... OK
All 1 tests passed.

用可选类型代替空指针

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

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

test "null @intToPtr" {
    const foo: *i32 = @ptrFromInt(0x0);
    _ = foo;
}
$ zig test test.zig
doctest-c593b190/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 optional_syntax.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-64c33a7c/discard.zig:4:30: error: error is discarded
    _ = std.fs.cwd().openFile("does_not_exist/foo.txt", .{});
        ~~~~~~~~~~~~~~~~~~~~~^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
doctest-64c33a7c/discard.zig:4:30: note: consider using 'try', 'catch', or 'if'
referenced by:
    callMain: zig/lib/std/start.zig:501:17
    callMainWithArgs: zig/lib/std/start.zig:469:12
    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

关键词 trycatch |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/posix.zig:1768:23: 0x1066558 in openatZ (try)
            .NOENT => return error.FileNotFound,
                      ^
/home/runner/work/www.ziglang.org/www.ziglang.org/zig/lib/std/fs/Dir.zig:880:16: 0x1036f37 in openFileZ (try)
    const fd = try posix.openatZ(self.fd, sub_path, os_flags, 0);
               ^
/home/runner/work/www.ziglang.org/www.ziglang.org/zig/lib/std/fs/Dir.zig:827:5: 0x1033a7e in openFile (try)
    return self.openFileZ(&path_c, flags);
    ^
/home/runner/work/www.ziglang.org/www.ziglang.org/doctest-587e5ce6/try.zig:4:18: 0x10338c8 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-66503384/test.zig:4:40: error: switch must handle all possibilities
    _ = parseInt("hi", 10) catch |err| switch (err) {};
                                       ^~~~~~~~~~~~~~~
doctest-66503384/test.zig:4:40: note: unhandled error value: 'error.InvalidCharacter'
doctest-66503384/test.zig:4:40: note: unhandled error value: 'error.DigitExceedsRadix'
doctest-66503384/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 2981 panic: attempt to unwrap error: FileNotFound
/home/runner/work/www.ziglang.org/www.ziglang.org/zig/lib/std/posix.zig:1768:23: 0x1069498 in openatZ (unreachable)
            .NOENT => return error.FileNotFound,
                      ^
/home/runner/work/www.ziglang.org/www.ziglang.org/zig/lib/std/fs/Dir.zig:880:16: 0x1038717 in openFileZ (unreachable)
    const fd = try posix.openatZ(self.fd, sub_path, os_flags, 0);
               ^
/home/runner/work/www.ziglang.org/www.ziglang.org/zig/lib/std/fs/Dir.zig:827:5: 0x1035eee in openFile (unreachable)
    return self.openFileZ(&path_c, flags);
    ^
/home/runner/work/www.ziglang.org/www.ziglang.org/doctest-d63cd8ce/unreachable.zig:4:77: 0x1033c9f 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:501:22: 0x10334a9 in posixCallMainAndExit (unreachable)
            root.main();
                     ^
/home/runner/work/www.ziglang.org/www.ziglang.org/zig/lib/std/start.zig:253:5: 0x1033011 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:356:29: 0x10383b7 in captureStackTrace (stack_traces)
            addr.* = it.next() orelse {
                            ^
/home/runner/work/www.ziglang.org/www.ziglang.org/doctest-0605ce97/stack_traces.zig:26:32: 0x1035e1c in foo (stack_traces)
    std.debug.captureStackTrace(null, &trace1);
                               ^
/home/runner/work/www.ziglang.org/www.ziglang.org/doctest-0605ce97/stack_traces.zig:16:8: 0x1033d88 in main (stack_traces)
    foo();
       ^
/home/runner/work/www.ziglang.org/www.ziglang.org/zig/lib/std/start.zig:501:22: 0x1033639 in posixCallMainAndExit (stack_traces)
            root.main();
                     ^


second one:
/home/runner/work/www.ziglang.org/www.ziglang.org/zig/lib/std/debug.zig:356:29: 0x10383b7 in captureStackTrace (stack_traces)
            addr.* = it.next() orelse {
                            ^
/home/runner/work/www.ziglang.org/www.ziglang.org/doctest-0605ce97/stack_traces.zig:30:32: 0x1035e3c in bar (stack_traces)
    std.debug.captureStackTrace(null, &trace2);
                               ^
/home/runner/work/www.ziglang.org/www.ziglang.org/doctest-0605ce97/stack_traces.zig:17:8: 0x1033d8d in main (stack_traces)
    bar();
       ^
/home/runner/work/www.ziglang.org/www.ziglang.org/zig/lib/std/start.zig:501:22: 0x1033639 in posixCallMainAndExit (stack_traces)
            root.main();
                     ^

你可以在正在进行的 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 types.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,
    };
    list.items[0] = 1234;
    list.len += 1;

    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;

    @memset(&array, 42);

    comptime {
        assert(array.len == 12345);
    }
}
$ zig test test.zig
zig/lib/std/debug.zig:403:14: error: reached unreachable code
    if (!ok) unreachable; // assertion failure
             ^~~~~~~~~~~
doctest-226130b9/test.zig:15: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+UEFI
x86_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 standingemscriptenWASI
wasm32一级支持三级支持一级支持
wasm64四级支持四级支持四级支持

支持等级

一级支持

二级支持

三级支持

四级支持

对包维护者友好

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

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

这是JSON格式的下载页面

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