为什么有了 C++、D 和 Rust 还要用 Zig?
没有隐藏的控制流
如果 Zig 代码看起来不像是在跳转调用函数,那么它就不是。这意味着您可以确信以下代码只调用 foo()
,然后调用 bar()
,并且这种保证无需知道任何类型。
var a = b + c.d;
foo();
bar();
隐藏控制流的示例
- D 语言有
@property
函数,这些方法看起来像字段访问,因此在上述示例中,c.d
可能会调用一个函数。 - C++、D 和 Rust 支持运算符重载,因此
+
运算符可能调用一个函数。 - C++、D 和 Go 有抛出/捕获异常(有时也称为 panic/recover),因此
foo()
可能会抛出异常,阻止bar()
被调用。(当然,即使在 Zig 中,foo()
也可能发生死锁,从而阻止bar()
被调用,但这在任何图灵完备的语言中都可能发生。)
此设计决策的目的是提高可读性。
没有隐藏的内存分配
Zig 在堆内存分配方面采取了放任不管的态度。没有 new
关键字或任何其他使用堆分配器的语言特性(例如字符串连接运算符[1])。堆的整个概念由库和应用程序代码管理,而不是由语言本身管理。
隐藏内存分配的示例
- Go 语言的
defer
会在函数局部栈上分配内存。除了这种控制流方式不直观外,如果在循环中使用defer
,它还可能导致内存不足故障。 - C++ 协程为了调用协程会分配堆内存。
- 在 Go 中,函数调用可能导致堆分配,因为 goroutine 会分配小的栈,当调用栈足够深时,这些栈会重新调整大小。
- Rust 主要的标准库 API 在内存不足时会 panic,而接受分配器参数的替代 API 则是后来才考虑的(参见 rust-lang/rust#29802)。
几乎所有带有垃圾回收器的语言都散布着隐藏的内存分配,因为垃圾回收器在清理时隐藏了证据。
隐藏内存分配的主要问题在于它阻碍了代码的可重用性,不必要地限制了代码可以部署到的环境数量。简而言之,在某些用例中,人们必须能够依赖控制流和函数调用不会产生内存分配的副作用,因此,一种编程语言只有在能够真实提供这种保证的情况下才能满足这些用例。
在 Zig 中,有提供和使用堆分配器的标准库特性,但这些都是可选的标准库特性,而不是语言本身内置的。如果您从不初始化堆分配器,您可以确信您的程序不会进行堆分配。
每个需要分配堆内存的标准库特性都接受一个 Allocator
参数来完成此操作。这意味着 Zig 标准库支持独立目标。例如,std.ArrayList
和 std.AutoHashMap
可以用于裸机编程!
自定义分配器使手动内存管理变得轻而易举。Zig 有一个调试分配器,可以在使用已释放内存和重复释放内存的情况下维护内存安全。它会自动检测并打印内存泄漏的堆栈跟踪。还有一个竞技场分配器,您可以将任意数量的分配捆绑在一起并一次性全部释放,而不是独立管理每个分配。专用分配器可用于根据任何特定应用程序的需求提高性能或内存使用率。
[1]: 实际上有一个字符串连接运算符(通常是数组连接运算符),但它只在编译时工作,因此仍然不会进行任何运行时堆分配。
对无标准库的一流支持
如上所述,Zig 有一个完全可选的标准库。每个标准库 API 只有在您使用它时才会被编译到您的程序中。Zig 对链接或不链接 libc 都提供同等支持。Zig 对裸机和高性能开发非常友好。
这兼具两者的优点;例如在 Zig 中,WebAssembly 程序既可以使用标准库的常规特性,与支持编译到 WebAssembly 的其他编程语言相比,仍能生成最小的二进制文件。
一种用于库的可移植语言
编程领域的圣杯之一是代码复用。遗憾的是,在实践中,我们常常发现自己一遍又一遍地重复造轮子。通常这是有道理的。
- 如果一个应用程序有实时性要求,那么任何使用垃圾回收或任何其他非确定性行为的库都不能作为依赖项。
- 如果一种语言让忽略错误变得过于容易,从而难以验证库是否正确处理并上报错误,那么人们可能会倾向于忽略该库并重新实现它,因为知道自己已正确处理了所有相关错误。Zig 的设计使得程序员最省力的做法就是正确处理错误,因此人们可以合理地相信库会正确地向上冒泡错误。
- 目前,从实用角度来看,C 语言是最通用和最便携的语言。任何无法与 C 代码交互的语言都面临着被冷落的风险。Zig 试图通过同时使其易于遵循外部函数的 C ABI,并引入安全性和语言设计来防止实现中的常见错误,从而成为新的库可移植语言。
现有项目的包管理器和构建系统
Zig 除了是一种编程语言外,还是一个工具链。它带有一个 构建系统和包管理器,即使在传统的 C/C++ 项目中也很有用。
您不仅可以用 Zig 代码代替 C 或 C++ 代码,还可以将 Zig 用作 autotools、cmake、make、scons、ninja 等的替代品。最重要的是,它还为原生依赖项提供了包管理器。即使项目的所有代码都是 C 或 C++,这个构建系统也适用。例如,通过将 ffmpeg 移植到 zig 构建系统,只需下载 50 MiB 的 zig,就可以在任何受支持的系统上为任何受支持的系统编译 ffmpeg。对于开源项目,这种简化的从源代码构建(甚至交叉编译)的能力,可能是获得或失去宝贵贡献者的关键。
apt-get、pacman、homebrew 等系统包管理器对于最终用户体验至关重要,但它们可能无法满足开发人员的需求。特定于语言的包管理器可能是项目有无贡献者的关键。对于开源项目,使项目成功构建的难度是潜在贡献者的巨大障碍。对于 C/C++ 项目,拥有依赖项可能是致命的,尤其是在没有包管理器的 Windows 上。即使仅仅构建 Zig 本身,大多数潜在贡献者也会在 LLVM 依赖项上遇到困难。Zig 提供了一种项目直接依赖原生库的方式——无需依赖用户系统包管理器提供正确的版本,并且无论使用何种系统以及针对何种平台,实际上都能保证项目首次构建成功。
其他语言也有包管理器,但它们不像 Zig 那样能消除烦人的系统依赖。
Zig 可以用一种合理的语言和声明式 API 来替代项目的构建系统,该 API 也提供了包管理,从而能够真正依赖其他 C 库。拥有依赖项的能力实现了更高层次的抽象,从而促进了可重用高级代码的普及。
简洁性
C++、Rust 和 D 具有如此多的特性,以至于它们可能会分散您对所开发应用程序实际意义的注意力。人们会发现自己在调试对编程语言的了解,而不是调试应用程序本身。
Zig 没有宏,但仍然足够强大,能够以清晰、不重复的方式表达复杂的程序。即使是 Rust 也有像 format!
这样的特殊宏,它是在编译器本身中实现的。而在 Zig 中,等效函数是在标准库中实现的,编译器中没有特殊情况代码。
工具链
Zig 可以从下载页面获取。Zig 提供适用于 Linux、Windows 和 macOS 的二进制归档文件。以下描述了您通过这些归档文件获得的内容
- 通过下载和解压单个归档文件即可安装,无需系统配置
- 静态编译,因此没有运行时依赖
- 支持使用 LLVM 进行优化发布构建,同时使用 Zig 的自定义后端以获得更快的编译性能
- 还支持一个用于输出 C 代码的后端
- 开箱即用地支持交叉编译到大多数主流平台
- 附带 libc 源代码,可根据需要在任何支持的平台上动态编译
- 包含支持并发和缓存的构建系统
- 编译支持 libc 的 C 和 C++ 代码
- 通过
zig cc
提供即插即用的 GCC/Clang 命令行兼容性 - Windows 资源编译器