之前学 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 语言的基本语法:
|
|
上面代码加了部分注释,算是对 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 实现泛型的基础,类型是一等成员,可以进行函数调用或赋值。比如下面这个例子:
|
|
这里的 LinkedList 定义了一个泛型链表,类型在编译期确定。使用方式如下:
|
|
指针
上面小节带领读者领略了 Zig 的基本语法,这一小节介绍下 Zig 里面的另一个巧妙设计:指针。
指针是 C 语言最贴近机器的抽象,表示一个内存地址,如果是像 int32 这样的基本类型还好,编译器能够确定指向的长度,但如果指向的是个数组,单有一个指针是不能确定元素个数的,一般都需要额外保存一个 length 字段。Zig 改进了这一点,定义了以下三种指针类型:
*T
指向一个元素的指针,比如*u8
[*]T
指向多个元素的指针,与 C 中指向数组的指针类似,没有长度信息*[N]T
指向 N 个元素数组的指针,长度信息可以通过array_ptr.len
获取。在需要时,这种指针可以自动转成上面的[*]T
。
除了上述三种指针外,Zig 最常用的一种结构是 slice,形式为 []T
。可以看作是下面的结构体:
|
|
- slice
[]T
与数组[N]T
的区别在于前者的长度是在运行时确定,后再是在编译时确定
此外,为了方便与 C 交互,Zig 中还定义了 Sentinel-Terminated Pointers,语法为 [*:x]T
,表示最后以 x
结尾。Zig 字符串字面量就是 *const [N:0]u8
类型:
|
|
[*:0]u8
和[*:0]const u8
分别等价于 C 中的char *
和const char *
。
这几种类型的指针可以相互转化,这也是初学者容易刚开始写代码时,容易混淆的地方,下面示例给出了常用的转化:
|
|
C interop
Zig 命令行工具集成了 clang,所以 zig 不仅仅可以用来编译 Zig 代码,还可以用来编译 C/C++ 代码, zig cc
在社区内就非常流行,比如下面这篇文章:
得益于 zig 不依赖 libc 的特性,zig 可以非常方便的用来进行交叉编译。
除了命令行工具,zig 在语言层面也对 C 提供了很好的支持,可以直接引用 C 的代码:
|
|
Zig 还集成了 pkg-config,方便链接 C 社区内丰富的类库,在代码中直接用 @cImport
导入即可:
|
|
此外,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 中的一小段代码来举例:
|
|
下面给出一示例,将 Zig 导出成 lib,供 C 调用(Zig 目前的版本不会输出头文件,需要自己手写,参考 #6753):
|
|
|
|
最后用下面的构建脚本来编译:
|
|
内存控制
Rust 采用严格的所有权支持保证内存安全,Zig 如果解决这个问题呢?答案是:Allocator,首先看一示例:
|
|
page_allocator
是 Zig 提供的内存分配器之一,可以看到用法与 C 中类似,使用完分配的内存后,需要手动 free
,那么 Zig 如何保证内存安全呢?答案是 GeneralPurposeAllocator
,这种分配器会阻止 double-free、use-after-free、leaks 等的发生,比如:
|
|
Zig 就是通过提供多种分配器来满足不同场景的需求,既没有 C 那么危险,也没有 Rust 的那种严格,算是在 C 与 Rust 中取了一个平衡。关于如何选择分配器,可以参考:
周边工具
构建
zig 自带里命令行工具就够用了,不需要 make 之类的额外工具
zig build
构建,描述文件是build.zig
zig init-exe
初始化应用项目zig init-lib
初始化类库项目
开发
目前有 zls 这个官方支持的 language server,主流编辑器都支持,补全、定义跳转都支持;此外,有 zig fmt
进行代码格式化。
文档
严重不足,写代码需要参考类库源码。幸运的是 std 的质量比较高,容易相对阅读。不过由于现在 Zig 还在快速开放中,std 中的 API 时不时会有 breaking change,需要定期更新到 master 上的最新版。
|
|
上面的错误即表示新版本去掉了这个字段。
Zen
|
|
总结
本文对 Zig 语言进行了初步介绍,没有设计到多线程、async 等内容,这些内容相对高级,后面等有更多经验时再来介绍吧。
Zig 作为一门现代语言,很多特性都是从 C 的弊端出发设计的,因此规避了很多问题,光是这一点就很有价值;基于 comptime 的泛型实现更是业界首创。
根据目前上面的视频来看,最早会在 2025 年发布 1.0,这个过程可能有些漫长,但这并不妨碍爱好者进行尝试!第一步,先从加入社区开始吧!