开发者生态
morning
为什么使用 Zig 而非 C 进行系统编程
摘要
After I wrote Why Zig Over Rust for Systems Programming , the obvious next question was C. The Rust question was about safety and how much ceremony I was willing to accept for it. The C question is di...
the
and
The
that
Zig
was
name
for
not
language
2026-11-14
1 阅读
约8分钟阅读
torutofu
字号:
在我写完为什么 Zig Over Rust 进行系统编程之后,显然下一个问题是 C。Rust 问题是关于安全性以及我愿意接受多少仪式。 C 的问题不同,因为 Zig 和 C 想要相同的东西。两者都是没有垃圾收集器的小语言,并且都接近机器的功能。所以这篇文章并不是要离开 C 去做一些完全不同的事情。这是关于 C 语言中耗费我时间的部分以及 Zig 如何处理它们的内容。所有代码都是在 Mac 上使用 Zig 0.17 和 clang 编译的。 C是UNIX的语言。内核、C 库和大多数其他语言最终调用的系统库都是用 C 编写的,手册页用 C 原型和 C 示例来描述它们。 20 世纪 90 年代的 C 程序今天仍然可以编译,并且几乎所有制造的 CPU 都有一个 C 编译器。这种语言很小,Kernighan 和 Ritchie 所著的《C 编程语言》一书就涵盖了所有内容。 Zig 本身并不试图取代其中任何一个。它直接与 C 库对话,并在需要时使用 C ABI。我写这篇文章不是因为我认为 C 不好;而是因为我认为 C 不好。 C 是我最喜欢的编程语言!我写它是因为我不想再用 C 编写新代码。我最不喜欢的 C 代码是一个函数,它获取多个资源,并且在中途失败时必须以相反的顺序释放它们。通常的答案(也是 Linux 内核使用的答案)是 goto : struct user * user_new ( const char * name, size_t n_groups) { struct user * u = malloc ( sizeof * u); if (u == NULL) goto 失败; u -> 名称 = strdup(名称); if (u -> name == NULL) 转到fail_user; u -> groups = calloc(n_groups, sizeof * u -> groups); if (u -> groups == NULL) 转到fail_name;返回你; failed_name: 自由 (u -> 名称); fail_user:空闲(u);失败:返回NULL;它有效,但清理远离分配,并且标签的顺序必须与手动分配的顺序相匹配。 Zig 中的相同函数如下所示: fn newUser (allocator: std.mem.Allocator, name: [] const u8 , n_groups: usize ) !* User { const u = try allocator.创建(用户); errdefer 分配器。摧毁(u); u.name = 尝试分配器。欺骗( u8 ,名称); errdefer 分配器。免费(你的名字); u.groups = 尝试分配器。分配(u32,n_groups);返回你;每个 errdefer 位于它撤消的分配的正下方,并且仅在函数返回错误时才运行。 Zig 以相反的顺序运行它们,因此没有标签需要保持顺序。我更喜欢的是我可以测试每条失败路径。 std.testing.FailingAllocator 类型在第一次、第二次或第三次分配时失败,具体取决于fail_index 的值。下面的测试分配器报告任何未释放的内存:测试“分配失败时 newUser 不会泄漏”{ for ( 0 .. 3 ) |我| { var 失败 = std.testing.FailingAllocator. init (std.testing.allocator, .{ .fail_index = i });尝试标准测试。 ExpectError ( error .OutOfMemory, newUser (failing.allocator (), "mihalis" , 4 )); $ zig test user.zig 1/1 user.test.newUser 在分配失败时不会泄漏...OK 所有 1 测试均已通过。在 C 版本中测试相同的三个路径需要 malloc() 的包装器或外部工具。在 C 中,指向函数的数组变成指针,并且长度单独传播(如果有的话)。下面的 C 函数有一个差一错误,用 <= 代替 < : int sum ( const int * a, size_t n) { int Total = 0 ; for ( size_t i = 0 ; i <= n; i ++ ) 总计 += a[i];返回总计;对于 {1, 2, 3} 数组,它在我的 Mac 上打印 sum = 7 并以 0 退出。额外的 1 来自数组后面内存中的任何内容。 -Wall -Wextra 的 clang 和程序本身都没有抱怨。在 Zig 中,切片携带其长度,因此同样的错误会在调试和安全模式下停止程序: fn sum (a: [] const i32 ) i32 { var Total: i32 = 0 ;对于 ( 0 .. a.len + 1 ) |我|总计 += a[i];返回总计; $ zig run sum.zig 线程 1278459 恐慌:索引越界:索引 3,len 3 sum.zig:5:38:sum ( sum ) 中的 0x1023c5b4b for ( 0..a.len + 1 ) |i|总计 += a [ i ] ; ^ 在真正的 Zig 代码中,您可以编写 (a) |x|而且这个错误根本不可能发生。字符串的工作方式相同。 Zig 字符串是具有长度的字节片,因此不会有 strlen() 调用在内存中查找 0 字节,也不会因为有人忘记了终止符而短一个字节的缓冲区。在 C 中,函数通过返回 -1 或 NULL 并设置 errno ,或者通过返回错误代码,或者以手册页描述的其他方式来报告错误。没有什么强迫你去检查。许多 C 代码调用 close() 和 fclose() 而不查看返回值,编译器对此没有问题。在 Zig 中,错误是返回类型的一部分,编译器拒绝编译您忽略其错误的调用。你必须使用 try 、 catch 或 if 。失败路径在源代码中,就在调用旁边,这也是我喜欢 Go 的原因
这篇文章对您有帮助吗?
订阅66必读
每日精选科技资讯,直达你的邮箱