有了 C++、D 和 Rust,为什么还需要 Zig?
没有隐式控制流
如果 Zig 代码看起来不像是在调用一个函数,那么它就不是。这意味着你可以确定下面的代码只会先调用 foo()
,然后调用 bar()
,不需要知道任何元素的类型,这一点也是可以保证的:
var a = b + c.d;
foo();
bar();
隐式控制流的例子:
- D 有
@property
函数,可以让你的方法调用看起来像是成员访问,因此在上面的例子中,c.d
可能会调用一个函数。 - C++,D 和 Rust 有运算符重载,因此
+
可能会调用一个函数。 - C++,D 和 Go 可以抛出和捕获异常,因此
foo()
可能会抛出一个异常,并且将会阻止bar()
被调用。(当然,即使在 Zig 中foo()
也可能存在死锁,并阻止bar()
被调用,但是这可能发生在任何一个图灵完备的语言里。)
这个设计决定的目的是可读性。
没有隐式内存分配
Zig 语言不干预堆内存分配。没有new
关键字或其他任何使用堆分配器的语言功能(例如字符串连接运算符[1])。整个堆都是由库或者用户代码而非语言本身所管理的。
隐式内存分配的例子:
- 在 Go 中,
defer
关键字在函数本地栈中分配内存。不仅是一种不直观的控制流,而且一旦在循环中使用defer
,还可能因内存不足而失败。 - 在 C++ 中,coroutine 特性需要分配堆内存以调用。
- 在 Go 中,goroutine 分配的栈因调用栈变深而调整大小,因而调用函数就可能引发内存分配。
- 在 Rust 中,标准库 API 会因为内存不足而崩溃。接受分配器参数的替代 API 尚在讨论(rust-lang/rust#29802)。
几乎所有的包含垃圾收集的语言都充满了隐式内存分配,不过垃圾收集器把隐式内存分配的证据隐藏在清理的那一侧。
隐藏内存分配的主要问题在于,它阻止了一段代码的可重用性,从而不必要的限制了适合代码部署的环境数量。简而言之,在某些用例中,必须能依赖于控制流和函数调用不产生内存分配的副作用,因此,一门语言必须能在切实提供这些保证的情况下才能为这些用例提供服务。
在 Zig 中,有一些标准库功能提供了堆分配器并且可以配合堆分配器,但这些都是可选的标准库特性,而不是内置在语言本身中的。如果你从不初始化堆分配器,那么你可以确信你的程序永远不会引起堆分配。
每一个需要分配内存的标准库特性都会接受一个分配器参数来进行内存分配。这意味着 Zig 的标准库特性支持裸金属目标。例如 std.ArrayList
和std.AutoHashMap
都可以用于裸金属编程!
自定义内存分配器使得手动管理内存变得轻而易举。Zig 有一个调试目的的分配器,可以在“释放后使用”和“双重释放”的情况下保证安全性。它能自动检测,并在内存泄露的时候打印堆栈跟踪;还有一个 Arena 分配器,可以让你将多个分配请求合并成一个,并统一释放,而不是独立的释放。特殊用途的分配器可以用来提高性能或内存的使用,以满足任何特定应用程序的需要。
[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 的合理的语言来代替项目的构建系统。它还提供包管理系统,从而可以依赖其他 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 资源编译器