在已经存在 C++/D/Rust 的情况下,为什么还要 Zig

发布: 2023-05-14   更新: 2023-05-16   分类: 编程语言   标签: zig

文章目录

原文地址,本文对部分章节进行了重新润色。

没有隐式控制流

如果 Zig 代码看起来不像是在调用函数,那么它就不是。这意味着您可以确定以下代码仅调用 foo()bar() ,并且无需知道任何类型即可保证这一点:

1
2
3
var a = b + c.d;
foo();
bar();

隐藏控制流的例子:

  • D 具有 @property 函数,这些函数使用起来像字段访问,但其实是个方法调用。因此在上面的示例中, c.d 可能会调用一个函数。
  • C++、D 和 Rust 具有运算符重载,因此 + 运算符可能会调用函数。
  • C++、D、Go 都有 throw/catch 异常,所以 foo() 可能会抛出异常,阻止 bar() 被调用。 当然,即使在 Zig 中 foo() 可能会死锁并阻止 bar() 被调用,但这在任何图灵完备语言中都可能发生。

此设计决策的目的是提高可读性。

没有隐式内存分配

Zig 在堆分配方面采用了一种不干涉的方法。它没有 new 关键字或其他语言中使用堆分配的功能(例如字符串连接运算符)。堆的整个概念由库和应用程序代码管理,而不是由语言管理。

隐藏分配的例子:

  • Go 的 defer 会在函数的堆栈上分配内存。除了作为此控制流工作的不直观方式外,如果 defer 在循环内使用,它还可能导致内存不足故障。
  • C++ 为了调用协程,协程会分配堆内存。
  • 在 Go 中,函数调用可能会导致堆分配,因为 goroutines 会分配小堆栈,当调用堆栈足够深时,这些堆栈会调整大小。
  • 主要的 Rust 标准库 API 在内存不足的情况下会崩溃,接受分配器参数的备用 API 是事后才想到的(参见 rust-lang/rust#29802 )。

几乎所有基于垃圾收集的高级语言都有散落的隐藏分配,只不过垃圾收集器会进行自动回收从而不容易让开发者意识到。

隐藏内存分配的主要问题在于,它阻止了一段代码的可重用性,从而不必要的限制了适合代码部署的环境。 简而言之,在某些用例中,必须能依赖于控制流和函数调用不产生内存分配的副作用,因此,一门语言必须能在切实提供这些保证的情况下才能为这些用例提供服务。

在 Zig 中,有一些标准库功能提供了堆分配器并且可以与之配合使用,但这些都是可选的标准库特性,而不是内置在语言本身中的。 如果你从不初始化堆分配器,那么你可以确信你的程序永远不会引起堆分配。

每一个需要分配内存的标准库特性都会接受一个分配器参数来进行内存分配。这意味着Zig的标准库特性支持裸金属目标(freestanding targets)。 例如 std.ArrayListstd.AutoHashMap 都可以用于裸机编程(bare metal programming)!

自定义内存分配器使得手动管理内存变得轻而易举。Zig 有一个调试目的的分配器,可以在“释放后使用”和“双重释放”的情况下保证安全性。 它能自动检测,并在内存泄露的时候打印堆栈跟踪;还有一个 Arena 分配器,可以让你将多个分配请求合并成一个,并统一释放,而不是独立的释放。 特殊用途的分配器可以用来提高性能或内存的使用,以满足任何特定应用程序的需要。

无标准库的一流支持

如上所述,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 等。 而在此之上,Zig(将)提供一个本地依赖的包管理器。这个构建系统的目的是为了使得即使一个项目的全部代码库都是C或C++也能适用。

apt-get、pacman、homebrew等系统包管理器对最终用户的体验很有帮助,但它们可能不足以满足开发人员的需求。 语言专用的包管理器可以形成没有贡献者和有大量贡献者之间的差距。对于开源项目来说,项目构建的难度对潜在贡献者来说是一个巨大的障碍。 特别是对于 C/C++ 项目来说,依赖关系可能是致命的,尤其是在没有包管理器的 Windows 上。 即使只是构建 Zig 本身,大多数潜在的贡献者在 LLVM 依赖上也会遇到困难。 Zig(将)为项目提供一种直接依赖原生库的方式

不需要依赖用户的系统包管理器来获得正确的版本,而且这种方式几乎可以保证不管使用的是什么系统,也不管目标平台是什么,都可以在第一次尝试时就成功地构建项目。

Zig 用一种使用声明式 API 的合理的语言来代替项目的构建系统。它还提供包管理系统,从而可以依赖其他 C 库。 有了声明依赖的能力,就能实现更高层次的抽象,从而实现可重用高级代码的大量涌现。

简单性

C++、Rust和D有大量的特性,它可能会打乱你正在编写的应用程序的实际含义。人们发现自己是在调试自己的编程语言知识,而不是调试应用程序本身。

Zig没有宏也没有元编程,但仍然足够强大,可以清晰、不重复地表达复杂的程序。 即使是在有宏的 Rust 里, format! 也是特例,它是在编译器内部实现的。 与此同时,Zig 中的等价函数是在标准库中实现的,编译器中没有特例代码。

工具性

可以从这里下载 Zig。Zig 提供了 Linux、Windows、MacOS 和 FreeBSD 的二进制存档。通过使用这些归档文件,可以获得:

  • 通过下载并解压单个压缩包进行安装,无需配置系统
  • 静态编译,没有运行时依赖
  • 使用成熟的、得到良好支持的LLVM基础架构,支持大多数主要平台,并进行深度优化
  • 开箱即用的支持大多数主要平台上交叉编译
  • 提供libc的源代码,在任何支持的平台上需要时都会动态编译
  • 包括带缓存的构建系统
  • 编译具有libc依赖的C/C++项目

评论

欢迎读者通过邮件与我交流,也可以在 MastodonTwitter 上关注我。