17611538698
info@21cto.com

2026 年,Zig 与 Rust 的对决

编程语言 0 11 19小时前

导读:本文作者谈及了Zig的优势,以及后来使用Rust的一些特性感受。

图片

大约 3 年前,在编写代理程序之前,我用 Zig 和尚不安全的 Rust 编写了一个字节码虚拟机和垃圾回收器,当时我觉得编写不安全代码的人性化方面,Zig 更胜一筹。

可惜的是,我现在手动编写代码的机会越来越少,这意味着我使用 Zig 的理由也越来越少。Zig 的各项功能带来的 1.5 到 5 倍的人类 DX 生产力提升,远不及Rust 编码代理带来的 100 倍提升。

Zig 的许多最棒的功能都是根据人体工程学设计的,但这对于黑客特工来说其实并不那么重要。

我将谈谈最喜欢的 Zig 功能,以及为什么它们现在不再那么重要了。

分配器接口

这是我最喜欢的 Zig 功能,使用专门的分配器来优化代码路径(例如 Arena、堆栈回退等),这些会让你感觉自己像个天才。
// A real example:// Reading a line of user input requires a heap allocator because// in theory the input length is unbounded. In practice though,// the input is almost always a short search query or path that// fits in well under 1kb.//// stackFallback puts a fixed-size buffer on the stack and only// hits the heap when the input overflows it. The common case// costs zero heap allocations. The rare long input still works// because the allocator silently upgrades to the heap.
var stack_fallback = std.heap.stackFallback(256, heap_allocator);const alloc = stack_fallback.get();const line =try reader.readUntilDelimiterAlloc(alloc,'\n',4096);defer alloc.free(line);

Rust 过去的问题在于没有 Allocator 接口的等效项,如果你想要一个Vec使用自定义分配器的集合,你实际上必须复制粘贴标准版本并对其进行修改才能使用它(这便是 Bumpalo 所做的, 看看源代码在https://github.com/fitzgen/bumpalo/tree/main/src/collections,集合是连接到 Bump 分配器的标准版本的分支)。

nightly 版本中早就有了Allocatortrait,现在看来效果不错。因为它是一个 trait,所以它是静态分发,而 Zig 的分发是基于虚函数表的。

与 Zig 不同,目前还没有一个社区普遍认可的、基于内存分配器来设计参数化数据结构的约定,但人工智能改变了这一切,使得复制粘贴代码并进行修改变得轻而易举。

就我的使用场景而言,它已经足够好了。

任意位宽整数 + 打包结构体


这是我最喜欢的 Zig 功能之一。它让实现 DOD 风格的 CPU 缓存优化以及诸如标记指针、NaN 装箱等操作变得非常简单,甚至让创建位标志也变得非常容易。

这里有一个实际的例子。当通过 Objective -C 运行时 C API 使用 Metal 等 Objective-C API(https://developer.apple.com/documentation/objectivec/objective-c-runtime?language=objc时,`a`id可能是一个带标签的指针,而不是一个对齐的堆对象指针。

我通过传递一个带标签的指针,并让代码假定对象已对齐,从而遇到了NSNumber未定义行为 (UB)。

所以,你需要一个简单的“堆指针 vs. 标记立即数”检查。以下是一个简化的 Objective-C 标记指针布局:最低一位表示“不是堆指针”,接下来的 3 位标识类槽,剩余的 60 位是有效载荷:

pub const TaggedClass =  enum(u3) {    ns_atom =  0,    ns_string =  1,    ns_number =  2,    ns_date =  3,};
pub const ObjcTaggedPointer =  packed struct {
    is_tagged :  bool =  true,
    class : TaggedClass,
    payload :  u60,
    pub fn ns_number(n :  u60) ObjcTaggedPointer {       return. {          .class =.ns_number, .payload = n       };
  }
pub fn from_raw(raw :  u64) ObjcTaggedPointer {    return @bitCast(raw);}
pub fn raw(self : ObjcTaggedPointer)  u64 {
  return @bitCast(self);
}
pub fn is_ns_number(self : ObjcTaggedPointer)  bool{
  return self.is_tagged  and self.class  == .ns_number;
}};

Rust 的等效实现是在构造函数中将 Objective-C 类槽位以 OR 方式传入,并在每次访问时将其屏蔽。该位置只是一个u64常量,而不是一个真正的类型:

pub struct ObjcTaggedPointer(u64);
impl ObjcTaggedPointer{
  const TAG_MASK : u64 = 0b1;
  const CLASS_MASK : u64 = 0b1110;
  const CLASS_SHIFT : u64 = 1;
  const PAYLOAD_SHIFT : u64 = 4;
  const CLASS_NS_ATOM : u64 = 0;
  const CLASS_NS_STRING : u64 = 1;
  const CLASS_NS_NUMBER : u64 = 2;
  const CLASS_NS_DATE : u64 = 3;
  pub fn ns_number(: u64)->Self  {
        Self(
          (n  << Self ::PAYLOAD_SHIFT)
              | (Self ::CLASS_NS_NUMBER  << Self ::CLASS_SHIFT)
              | Self ::TAG_MASK,
          )
  }
  pub fn is_ns_number(self)->bool{
            self.0 & Self ::TAG_MASK  != 0
        && (self.0 & Self ::CLASS_MASK) >> Self ::CLASS_SHIFT  == Self ::CLASS_NS_NUMBER  }}

你可以看出 Rust 原生的方式并不人性化。实际上,使用像bitfield / bitflags这样的 crate 会更好,它们都依赖于过程宏来实现,但我个人觉得它们不如 Zig 的打包结构体好用。

但是,现在有了编码代理,我根本不在乎手动编写代码有多么麻烦。

计算时间


这是 Zig 最炫酷的特性,除了少数晦涩的依赖类型语言之外,没有其他编程语言的编译时求值能像 Zig 那样出色。

我原以为我会非常想念它,但实际上并没有。对我来说,95% 的 comptime 用法都是用来创建 Zig 版本的带参数类型的通用数据结构。例如下列代码:

fn ArrayList(comptime T :  type)  type{  return struct  {
        items : [] T,
        capacity :  usize,
        allocator : Allocator,
    };}
const IntList =  ArrayList(i32);

我认为 Rust 的类型系统设计得更不错(见下一节)。

在剩下的 5% 的情况下,没有编译时求值简直糟糕透了。唯一可靠的替代方案是通过代码生成。我目前正在开发一款游戏,其中包含一些硬编码的碰撞箱几何数据,这些数据由一个工具生成,我想将它们嵌入到一个数据结构中。如果没有编译时求值,我只能让 Claude 写一个脚本来生成 Rust 文件。不过,我发现自己其实并不太需要编译时求值。

Rust 的类型系统


我宁愿牺牲编译时间来换取 Rust 设计更完善的类型系统,尤其是在有界多态(traits/typeclasss)方面。在 Zig 中实现类似的功能简直是噩梦。

此外,我认为 Rust 的类型系统允许你强制执行更多不变式,并防止编码代理犯常见错误。在我的游戏中,我使用了 euclid crate,它本质上允许你避免混淆坐标空间(这在图形编程中是一个非常常见的问题),方法是为每个坐标空间创建专门的类型(例如 `x`Point或`y` Point):

use euclid :: {  point2, vec2, Point2D, Translation2D, Vector2D};
struct WorldSpace;
struct ScreenSpace;
type WorldPoint = Point2DWorldSpace>;
type WorldVector = Vector2DWorldSpace>;
type ScreenPoint = Point2DScreenSpace>;
fn main(){
  let player : WorldPoint = point2(10 .020 .0);  let movement : WorldVector = vec2(5 .00 .0);
  // Allowed: point + vector in the same space.  let moved_player  = player  + movement;
  // Allowed: explicitly convert from world space to screen space.  let world_to_screen = Translation2D ::WorldSpaceScreenSpace>::new (100 .050 .0);
  let player_on_screen : ScreenPoint = world_to_screen.transform_point(moved_player);
  // Disallowed: can't mix coordinate spaces.  let bad : ScreenPoint = player;}

这样可以避免智能体犯将世界空间坐标与屏幕空间坐标混淆的愚蠢错误:

无需处理内存问题


由于编码代理允许编写的代码量增加 100 倍,这也意味着您需要检查的 Zig 代码量增加 100 倍,以查找内存问题。如果没有形式化验证,现在需要枚举的搜索空间范围要大得多,才能找到 bug。

如今代码量如此庞大,Rust 的吸引力也随之增强。Rust 的缺点在于它会降低开发者的效率,尤其是在不熟悉借用检查器的情况下,但有了代码代理,这个问题就迎刃而解了。

如果你在 Rust 中使用 ` \`,可以使用像miriunsafe这样的工具,让编码代理运行代码,以确保它不会导致未定义行为,或者在处理 `\` 时没有违反 Rust 的别名规则

结语

我仍然怀念用 Zig 编写代码,并且认为它是一门很棒的语言,但我更喜欢 Rust,而且编码代理与 Rust 的兼容性更好。

作者:场长

参考:

https://zackoverflow.dev/writing/zig-vs-rust-in-2026

评论

我要赞赏作者

请扫描二维码,使用微信支付哦。

分享到微信