之前学 C 的时候写了一个 oh-my-github 的项目,一直没有测试,C 里面的单元测试框架比较多,不知道选哪个好,不如直接用 Zig 来做测试,正好也看到这篇文章 Testing and building C projects with Zig,感觉是个不错的选择。其实我听说 Zig 语言有半年了,不依赖 libc、 better C interop、robust 这几个特性我都挺感兴趣的。这篇文章就带领读者一窥 Zig 的真实面貌。
本文涉及代码可在这里找到。
在和社区小伙伴共同努力下,尝试搭建了 Zig 语言中文社区,便于 Zig 中文用户交流分享心得:
Why Zig
Zig 官网上有一篇文章:有了C++、D和Rust,为什么还需要Zig?,这里面介绍了 Zig 与其他类似语言的区别,核心的一点就是:简单。
没有隐式控制流、没有隐式内存分配、统一的构建工具,用尽可能少的特例(没有宏与元编程),来支持编写复杂程序。
Hello World
这一小节使用 Zig 语言官网上的例子,来介绍 Zig 语言的基本语法:
1// 引入标准库
2const std = @import("std");
3const json = std.json;
4// 定义多行的字符串
5const payload =
6 \\{
7 \\ "vals": {
8 \\ "testing": 1,
9 \\ "production": 42
10 \\ },
11 \\ "uptime": 9999
12 \\}
13;
14// 定义一个结构体类型,类型和 u8 等基础类型一样是一等成员,可以作为值来使用
15const Config = struct {
16 vals: struct { testing: u8, production: u8 },
17 uptime: u64,
18};
19// 定义一个全局变量,不可变,同时定义了一个 block,label 为 x
20// 由于 config 定义在顶级作用域(Zig 中称为 container level),因此它的赋值会在编译期执行
21const config = x: {
22 // 使用 var 定义一个可变的局部变量
23 var stream = json.TokenStream.init(payload);
24 const res = json.parse(Config, &stream, .{});
25 break :x res catch unreachable;
26};
27
28// main 函数的返回值为 !void,这里省略了具体错误类型,由编译器自动推导
29pub fn main() !void {
30 if (config.vals.production > 50) {
31 @compileError("only up to 50 supported");
32 }
33 std.log.info("up={d}", .{config.uptime});
34}上面代码加了部分注释,算是对 Zig 语法的简单介绍,下面重点介绍 json 解析部分的语法:
json.parse的函数签名是parse(comptime T: type, tokens: *TokenStream, options: ParseOptions) ParseError(T)!T
- 第一个参数中的 comptime 关键字表示该参数是在编译期时执行,T 的类型是 type,表示类型值
返回值 ParseError(T)!T 表示返回值可能出错,类似 Rust 中的 Result 类型
- ParseError(T) 会在编译期执行,得出具体错误类型。Zig 采用 comptime 来实现泛型
json.parse(Config, &stream, .{})中的.{}表示匿名结构体,类型可以由编译器推导出来,结构体内没有对字段进行初始化,是由于 ParseOptions 中的字符都有默认值break :x表示退出 block 快,后面跟着返回值res catch unreachable表示取出正常情况下的值,相当于 Rust 中的 res.unwrap()
上面这个示例代码展示了 Zig 中一些特有的语法,最主要的是 comptime,它是 Zig 实现泛型的基础,类型是一等成员,可以进行函数调用或赋值。比如下面这个例子:
1fn LinkedList(comptime T: type) type {
2 return struct {
3 pub const Node = struct {
4 // ?T 表示 Option 类型,值可能为 null
5 prev: ?*Node,
6 next: ?*Node,
7 data: T,
8 };
9
10 first: ?*Node,
11 last: ?*Node,
12 len: usize,
13 };
14}这里的 LinkedList 定义了一个泛型链表,类型在编译期确定。使用方式如下:
1// test 定义了一个测试代码快,zig test main.zig 时会执行这里面的代码
2test "linked list" {
3 // Functions called at compile-time are memoized. This means you can
4 // do this:
5 // expect 可能返回错误,try 表示 catch |err| return err
6 // 类似于 Rust 中的 result?
7 try expect(LinkedList(i32) == LinkedList(i32));
8
9 var list = LinkedList(i32) {
10 .first = null,
11 .last = null,
12 .len = 0,
13 };
14 try expect(list.len == 0);
15
16 // Since types are first class values you can instantiate the type
17 // by assigning it to a variable:
18 const ListOfInts = LinkedList(i32);
19 try expect(ListOfInts == LinkedList(i32));
20
21 var node = ListOfInts.Node {
22 .prev = null,
23 .next = null,
24 .data = 1234,
25 };
26 var list2 = LinkedList(i32) {
27 .first = &node,
28 .last = &node,
29 .len = 1,
30 };
31
32 // When using a pointer to a struct, fields can be accessed directly,
33 // without explicitly dereferencing the pointer.
34 // So you can do
35 // option.? 相当于 rust 中的 option.unwrap(),取出其中的值
36 try expect(list2.first.?.data == 1234);
37 // instead of try expect(list2.first.?.*.data == 1234);
38}指针
上面小节带领读者领略了 Zig 的基本语法,这一小节介绍下 Zig 里面的另一个巧妙设计:指针。
指针是 C 语言最贴近机器的抽象,表示一个内存地址,如果是像 int32 这样的基本类型还好,编译器能够确定指向的长度,但如果指向的是个数组,单有一个指针是不能确定元素个数的,一般都需要额外保存一个 length 字段。Zig 改进了这一点,定义了以下三种指针类型:
*T指向一个元素的指针,比如*u8[*]T指向多个元素的指针,与 C 中指向数组的指针类似,没有长度信息*[N]T指向 N 个元素数组的指针,长度信息可以通过array_ptr.len获取。在需要时,这种指针可以自动转成上面的[*]T。
除了上述三种指针外,Zig 最常用的一种结构是 slice,形式为 []T 。可以看作是下面的结构体:
1const slice = struct {
2 ptr: [*]T,
3 len: usize,
4}- slice
[]T与数组[N]T的区别在于前者的长度是在运行时确定,后再是在编译时确定
此外,为了方便与 C 交互,Zig 中还定义了 Sentinel-Terminated Pointers,语法为 [*:x]T ,表示最后以 x 结尾。Zig 字符串字面量就是 *const [N:0]u8 类型:
1test "string literal" {
2 try expect(@TypeOf("hello") == *const [5:0]u8);
3}[*:0]u8和[*:0]const u8分别等价于 C 中的char *和const char *。
这几种类型的指针可以相互转化,这也是初学者容易刚开始写代码时,容易混淆的地方,下面示例给出了常用的转化:
1const std = @import("std");
2const os = std.os;
3const expect = std.testing.expect;
4
5pub fn main() !void {
6 // [N]T --> *[N]T or []T
7 // *[N]T --> []T
8 {
9 var array = [_]u8{ 1, 2, 3 };
10 try expect(@TypeOf(array) == [3]u8);
11
12 var ptr_to_array = array[0..];
13 try expect(@TypeOf(ptr_to_array) == *[3]u8);
14 var slice: []u8 = array[0..];
15 try expect(@TypeOf(slice) == []u8);
16
17 var slice2: []u8 = ptr_to_array[0..];
18 try expect(@TypeOf(slice2) == []u8);
19
20 const str: [*:0]const u8 = "hello";
21 const str2: [*]const u8 = str;
22 _ = str2;
23 }
24 // [N]T --> [:x]T
25 // [:x]T --> []T
26 {
27 var array = [_]u8{ 3, 2, 1, 0, 3, 2, 1, 0 };
28
29 var runtime_length: usize = 3;
30 const terminated_slice = array[0..runtime_length :0];
31 try expect(@TypeOf(terminated_slice) == [:0]u8);
32
33 // thread 11571310 panic: sentinel mismatch
34 // const slice2 = array[0..runtime_length :1];
35 // _ = slice2;
36
37 // error: expected type 'void', found '[:1]u8'
38 // _ = array[0..runtime_length :1];
39
40 const slice = array[0..terminated_slice.len];
41 try expect(@TypeOf(slice) == []u8);
42 }
43}C interop
Zig 命令行工具集成了 clang,所以 zig 不仅仅可以用来编译 Zig 代码,还可以用来编译 C/C++ 代码, zig cc 在社区内就非常流行,比如下面这篇文章:
得益于 zig 不依赖 libc 的特性,zig 可以非常方便的用来进行交叉编译。
除了命令行工具,zig 在语言层面也对 C 提供了很好的支持,可以直接引用 C 的代码:
1const std = @import("std");
2
3pub extern "c" fn printf(format: [*:0]const u8, ...) c_int;
4
5pub fn main() !void {
6 _ = printf("Hello, World!\n");
7 _ = std.c.printf("Hello, Zig!\n");
8}Zig 还集成了 pkg-config,方便链接 C 社区内丰富的类库,在代码中直接用 @cImport 导入即可:
1// zig build-exe cimport.zig -lc $(pkg-config --libs raylib) $(pkc-config --cflags raylib)
2const ray = @cImport({
3 @cInclude("raylib.h");
4});
5
6pub fn main() void {
7 const screenWidth = 800;
8 const screenHeight = 450;
9
10 ray.InitWindow(screenWidth, screenHeight, "raylib [core] example - basic window");
11 defer ray.CloseWindow();
12
13 ray.SetTargetFPS(60);
14
15 while (!ray.WindowShouldClose()) {
16 ray.BeginDrawing();
17 defer ray.EndDrawing();
18
19 ray.ClearBackground(ray.RAYWHITE);
20 ray.DrawText("Hello, World!", 190, 200, 20, ray.LIGHTGRAY);
21 }
22}
此外,Zig 提供了 zig translate-c 命令来将 C 代码转成 Zig 代码,遇到调用 C 库困难时,可以参考这个命令。对于 C 中的指针 Zig 用 [*c]T 表示,称为 C 指针,与 Zig 中的指针支持同样的操作,一般会把 C 指针转成 Optional Pointer ?*T 来用, null 就表示 C 指针为空。
为了让 C 能够调用 Zig,有一点需要注意:
struct/enum/union 默认没有内存结构保证,需要用
extern或packet关键字声明成符合 C ABI 内存格式的,
默认没有内存结构保证的主要原因是方便 Zig 进行优化,比如:
- 字段重排,在保证对齐的前提下取得更小的体积
- 通过合适的方式对齐字段,保证最快的访问速度
更多可参考:
在 Rust 中 struct 默认也是不保证内存结构的,原因和 zig 类似。这里用 C 中的一小段代码来举例:
1struct {
2 int64_t a; // 8
3 int16_t b; // 2
4 char c; // 1
5 // padding 5
6} a = {.a=1,.b=2,.c='a'};
7printf("a %lu\n", sizeof(a));
8
9struct {
10 int16_t b; // 2
11 // padding 6
12 int64_t a; // 8
13 char c; // 1
14 // padding 7
15} b = { .a=1, .b=2, .c='a' };
16printf("b %lu\n", sizeof(b));
17// 在 64 位 8 字节对齐的机器上会依次输出 16,24
下面给出一示例,将 Zig 导出成 lib,供 C 调用(Zig 目前的版本不会输出头文件,需要自己手写,参考 #6753):
1// cat mathtest.zig
2export fn add(a: i32, b: i32) i32 {
3 return a + b;
4}
5// 编译成静态库,或加 -dynamic 选项编译成动态库
6// zig build-lib mathtest.zig1#include "mathtest.h"
2#include <stdio.h>
3
4int main(int argc, char **argv) {
5 int32_t result = add(42, 1337);
6 printf("%d\n", result);
7 return 0;
8}最后用下面的构建脚本来编译:
1const Builder = @import("std").build.Builder;
2
3pub fn build(b: *Builder) void {
4 const lib = b.addSharedLibrary("mathtest", "mathtest.zig", b.version(1, 0, 0));
5
6 const exe = b.addExecutable("test", null);
7 exe.addCSourceFile("test.c", &[_][]const u8{"-std=c99"});
8 exe.linkLibrary(lib);
9 exe.linkSystemLibrary("c");
10
11 b.default_step.dependOn(&exe.step);
12
13 const run_cmd = exe.run();
14
15 const test_step = b.step("test", "Test the program");
16 test_step.dependOn(&run_cmd.step);
17}
18
19// $ zig build test
20// 1379内存控制
Rust 采用严格的所有权支持保证内存安全,Zig 如果解决这个问题呢?答案是:Allocator,首先看一示例:
1const std = @import("std");
2const expect = std.testing.expect;
3
4test "allocation" {
5 const allocator = std.heap.page_allocator;
6
7 const memory = try allocator.alloc(u8, 100);
8 defer allocator.free(memory);
9
10 try expect(memory.len == 100);
11 try expect(@TypeOf(memory) == []u8);
12}page_allocator 是 Zig 提供的内存分配器之一,可以看到用法与 C 中类似,使用完分配的内存后,需要手动 free ,那么 Zig 如何保证内存安全呢?答案是 GeneralPurposeAllocator ,这种分配器会阻止 double-free、use-after-free、leaks 等的发生,比如:
1test "GPA" {
2 var gpa = std.heap.GeneralPurposeAllocator(.{}){};
3 const allocator = gpa.allocator();
4 defer {
5 const leaked = gpa.deinit();
6 if (leaked) expect(false) catch @panic("TEST FAIL"); //fail test; can't try in defer as defer is executed after we return
7 }
8
9 const bytes = try allocator.alloc(u8, 100);
10 defer allocator.free(bytes);
11}Zig 就是通过提供多种分配器来满足不同场景的需求,既没有 C 那么危险,也没有 Rust 的那种严格,算是在 C 与 Rust 中取了一个平衡。关于如何选择分配器,可以参考:
周边工具
构建
zig 自带里命令行工具就够用了,不需要 make 之类的额外工具
zig build构建,描述文件是build.zigzig init-exe初始化应用项目zig init-lib初始化类库项目
开发
目前有 zls 这个官方支持的 language server,主流编辑器都支持,补全、定义跳转都支持;此外,有 zig fmt 进行代码格式化。
文档
严重不足,写代码需要参考类库源码。幸运的是 std 的质量比较高,容易相对阅读。不过由于现在 Zig 还在快速开放中,std 中的 API 时不时会有 breaking change,需要定期更新到 master 上的最新版。
1./main.zig:24:9: error: no member named 'read' in struct 'std.fs.file.File.OpenFlags'
2 .read = true,
3 ^上面的错误即表示新版本去掉了这个字段。
Zen
1$ zig zen
2
3 * Communicate intent precisely.
4 * Edge cases matter.
5 * Favor reading code over writing code.
6 * Only one obvious way to do things.
7 * Runtime crashes are better than bugs.
8 * Compile errors are better than runtime crashes.
9 * Incremental improvements.
10 * Avoid local maximums.
11 * Reduce the amount one must remember.
12 * Focus on code rather than style.
13 * Resource allocation may fail; resource deallocation must succeed.
14 * Memory is a resource.
15 * Together we serve the users.总结
本文对 Zig 语言进行了初步介绍,没有设计到多线程、async 等内容,这些内容相对高级,后面等有更多经验时再来介绍吧。
Zig 作为一门现代语言,很多特性都是从 C 的弊端出发设计的,因此规避了很多问题,光是这一点就很有价值;基于 comptime 的泛型实现更是业界首创。
根据目前上面的视频来看,最早会在 2025 年发布 1.0,这个过程可能有些漫长,但这并不妨碍爱好者进行尝试!第一步,先从加入社区开始吧!

Zig 预计 2025 发布 1.0