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

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

文章目录

原文地址:https://ziglang.org/learn/why_zig_rust_d_cpp/

无隐藏的控制流程

如果 Zig 代码看起来不像是在进行函数调用,那么它就不是。这意味着你可以确信以下代码只会调用 foo()bar() ,而无需知道任何类型的信息:

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

下面是一些具有隐藏控制流程的例子:

  • D语言的 @property 函数(实际上是通过字段访问方式调用的方法),因此在上述代码中, c.d 可能调用了某个函数。
  • C++、D和Rust中的运算符重载使得加号 + 可能会调用一个函数。
  • C++、D和Go语言中的抛出/捕获异常机制意味着 foo() 可能会抛出异常,从而阻止了 bar() 的调用。当然,即使在Zig中, foo() 也可能因死锁而使 bar() 无法调用,在任何图灵完备的语言中都可能发生这种情况。

这种设计决策的目的在于提高代码可读性。

无隐藏分配

对于堆内存分配,Zig采取了较为宽松的处理方式。它没有 new 关键字或其他使用堆分配器(例如字符串连接运算符)的语句。堆的概念由库和应用程序代码管理,并非由语言本身管理。

下面是一些具有隐藏分配的例子:

  • Go语言中的 defer 会为函数局部栈分配内存,这种控制流程的方式不仅难以理解,而且如果在循环内部使用 defer ,还可能导致内存不足的失败。
  • C++中的协程会在调用协程时分配堆内存。
  • 在Go中,一个函数调用可能会导致堆分配,因为goroutines会为每个调用栈分配小型栈,并且当调用栈深度足够大时会被重新调整大小。
  • Rust的主要标准库API在发生内存不足的情况下会崩溃,而接受分配器参数的替代API只是后来添加的功能(见rust-lang/rust#29802)。

几乎所有的垃圾收集语言都充斥着隐藏的内存分配,因为垃圾回收器在清理阶段会隐藏这些分配的事实。隐藏分配的主要问题在于它们阻止了代码复用,不必要地限制了可以部署该代码的环境数量。简而言之,存在这样的使用场景,其中必须能够依赖于控制流程和函数调用不会产生额外内存分配的影响,因此编程语言只有在能提供这种保证的情况下才能服务于这些用途。

在Zig中,标准库中有用于堆内存管理的功能,但这些都是可选的标准库特性,并非内置于语言本身。如果你从未初始化过堆分配器,则可以确信你的程序不会进行堆内存分配。

每一个需要在标准库中分配堆内存的API都会接受一个 Allocator 参数来实现这一功能。这意味着Zig标准库支持独立目标(freestanding targets)。例如,可以使用 std.ArrayListstd.AutoHashMap 来进行裸金属编程!

自定义分配器使手动内存管理变得轻松。Zig 中有一个用户调试的分配器,可以发现“释放后使用”和“双重释放”两种错误,并且能够自动检测并打印内存泄漏的堆栈跟踪信息。还有一种区域分配器,可以将任何数量的分配捆绑在一起并在一次操作中全部释放,而不是独立管理每个分配。根据特定应用的需求,可以使用专门的分配器来改善性能或内存使用。

非标准库的核心支持

如前所述,Zig拥有完全可选的标准库。每个标准库API只有在被使用时才会编译到你的程序中。Zig对是否链接libc或不链接libc都提供了同等的支持。Zig对于裸金属和高性能开发都非常友好。

这是最好的两种方式的结合;例如,在Zig中,WebAssembly程序不仅可以利用标准库的正常功能,而且与支持编译到WebAssembly的语言相比,生成的二进制文件仍然非常小巧。

适用于库的通用语言

编程之中的圣杯是代码复用。遗憾的是,在实践中,我们经常不得不反复地重新发明轮子。有时这种做法是可以理解的:

  • 如果一个应用有实时需求,则任何使用垃圾收集或其他非确定性行为的库都作为依赖关系被排除在外。 通过Zig,开发者可以灵活选择是否使用标准库中的API,这为编写自定义库提供了一定程度的灵活性。这种可选的标准库支持使得Zig能够适应多种不同的环境和需求,并在满足特定功能的同时保持代码的可维护性和可靠性。
  • 如果语言使得忽略错误过于容易,从而难以验证库是否正确处理并传递了错误,这可能会诱使人们绕过库并重新实现它。了解自己正确处理所有相关错误,Zig被设计成程序员可以做的最懒惰的事情就是正确处理错误,因此我们可以合理地相信库会正确地将错误传递上去。
  • 目前从实用角度来看,C语言是最灵活且兼容性最好的语言。任何不能与C代码交互的语言都面临着过时的风险。Zig正在努力成为新的通用库语言,通过同时提供遵循外部函数 C ABI的方法,并引入预防常见实现中错误的安全性和语言设计。

构建系统和包管理器的工具链

不仅仅是编程语言:Zig不仅仅是一种编程语言,它附带了一个适用于传统C/C++项目的构建系统和包管理系统

  • 替换工具链:你不仅可以使用Zig代码代替C或C++代码,还可以用Zig来替代autotools、cmake、make、scons、ninja等。此外,它还提供了一个原生依赖包管理器。即使项目的所有代码都在C或C++中,这个构建系统也适用。
  • 简化源码构建:通过将ffmpeg移植到Zig的构建系统(例如ffmpeg的Zig版本),在任何支持的系统上仅使用一个50MB的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代码输出后的端口。
  • 内置并发和缓存的构建系统。
  • 支持C和C++代码与libc的支持的编译。
  • GCC/Clang命令行兼容性,使用=zig cc=进行替代。
  • Windows资源编译器支持

评论

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