Zig,理想的C语言替代品?还是……?
Zig, the Ideal C Replacement Or?

原始链接: http://bitshifters.cc/2025/05/04/zig.html

Zig 是一种旨在取代 C 语言的系统编程语言,它强调显式控制和安全性。虽然支持者吹捧其革命性和性能,但这些说法存在争议。其核心理念“无隐藏控制流”要求显式内存管理和错误处理,这可能会牺牲程序员的便利性以换取最优性能。 Zig 的编译时特性能够实现强大的元编程,但也使工具变得复杂。其错误处理机制比较新颖,需要显式处理“Result”类型。冗长的代码,尤其是在类型转换和循环语法方面,是常见的批评。在发布版本中,安全检查通常会被禁用,这引发了一些担忧。 尽管经过近十年的开发,Zig 1.0 仍然遥遥无期,原因是功能范围不断扩大。具有讽刺意味的是,它最大的成功在于其跨编译工具(“zig cc”),该工具可以独立于语言本身使用。Zig 是否真正超越 C 语言还有待商榷,因为 Odin 提供了更多高级便利。Zig 关注底层设计的做法可能混淆了底层和高性能的概念,在某些方面使其比 C 语言更繁琐。

Hacker News 最新 | 过去 | 评论 | 提问 | 展示 | 招聘 | 提交 登录 Zig:低级编程的新方向? (bitshifters.cc) 48 分,来自 ksec,1 天前 | 隐藏 | 过去 | 收藏 | 2 条评论 mitchbob 1 天前 [–] 之前的讨论(9 条评论,2 天前):https://news.ycombinator.com/item?id=43942094 回复 dang 1 天前 | 父评论 [–] 评论已移至此处。谢谢! 回复 考虑申请 YC 2025 年夏季批次!申请截止日期为 5 月 13 日 指南 | 常见问题 | 列表 | API | 安全 | 法律 | 申请 YC | 联系我们 搜索:

原文

Zig is a general-purpose systems programming language designed by Andrew Kelley. The language bills itself as “a general-purpose programming language and toolchain for maintaining robust, optimal, and reusable software”.

Proponents of Zig often argue that the language is a revolutionary low-level language that represents a massive shift in how you think about programming. They claim it’s the best C replacement language. This is high praise indeed, so let’s see if it can actually live up to the hyperbole.

Philosophy

Its core philosophy emphasises explicit control: the language motto is “No hidden control flow”. In practice, this means all memory allocation, error handling, and control-flow constructs must be spelled out by the programmer.

Kelley has stressed that the language is for creating “optimal” solutions at the cost of some programmer inconvenience. Correctness and robustness are equally held as important goals for Zig code.

Critics argue that Zig adding undefined behaviour compared to C for additional possible compiler optimisation benefits runs counter to these last two goals, although the Zig documentation claims they are not in opposition:

“Zig uses undefined behavior as a razor sharp tool for both bug prevention and performance enhancement.” link

At the same time, even Zig’s docs acknowledge that ReleaseFast and ReleaseSmall have no protection against UB that isn’t detected when testing with Debug or ReleaseSafe.*

From this, we can conclude that the Zig approach is to assume that all undefined behaviour will be caught while testing in safe mode, and the application can then safely be run without checks and with full performance in production.

This is different from most other languages that either retain many checks, or at least try to retain well-defined behaviour as much as possible.

* EDIT: On the Zig discord there was some resistance to this saying ReleaseSafe was a good alternative to be used in production to ensure safety. However, since this has a non-neglible overhead, trading safety for a potential large cost in performance I think my point stands. It is not comparable to languages like Rust in this respect.

Doubtful claims of excellence

Zig made an early splash with the claim that “Zig is faster than C” in early talks by Kelley. This turned out not to be quite true. The benchmarks turned out to be meaningless, as the Zig code had been compiled to the native architecture, while the C code was compiled for a generic CPU. When this discrepancy was removed, they compiled to the same code.*

The docs still talk about Zig being faster than C, and in the community, many assume it’s true, even though the docs do mention “native arch” by default:

“For native targets, advanced CPU features are enabled (-march=native), thanks to the fact that Cross-compiling is a first-class use case.” link

Here I am not sure why “cross-compiling” being a first-class use case has to do with compiling for the native CPU. In practice, accidentally compiling for native architecture and then being unable to share the executable happens to many beginners.

The other big claim, which caused conflict with the Rust community, is the claim of safety without trade-offs, or as the Zig docs say: “Performance and Safety: Choose Two”.

As we already saw, Zig doesn’t really acknowledge that there is a problem running with UB in production as long as it has been tested with debug mode on. This should rightly be criticised. Anyone with experience of projects that have gone through lengthy testing can testify: bugs still happen in production. Even in full debug mode, Zig only checks UB and not overall correctness – for that, something like contracts are needed, and Zig does not have them.

Another claim was Zig’s “colourblind” async, which “solved the async colouring problem”. However, it was later removed from the language, and it’s currently waiting for problems with the implementation and semantics to be worked out.

Async is interesting in other ways, as it seems to grossly violate the “explicitness” principle of Zig, but was nonetheless added.

* EDIT: According to Zig users this should more be taken to mean that it’s easier to write code that is fast when using Zig than when using C or C++. This is a quite different from what the documentation and what the early talks by Andrew Kelley is claiming. I think I have to stand by my criticism here.

First impressions

The first Hello World looks like this:

const std = @import("std");

pub fn main() void {
    std.debug.print("Hello, world!\n", .{});
}

We start by defining the constant std, which gives us an alias to the struct/module (Zig doesn’t differentiate between the two) containing the standard library.

Second, we print using the standard “debug” print, and Zig’s explicitness is already on full display: it uses full paths as recommended – although we could possibly have aliased it somewhat further – and there is that little .{} at the end.

This .{} is because Zig lacks vaargs or default arguments, so .{}, which is an empty anonymous struct in Zig, has to be passed even though we don’t really have any argument to format.

Already here we see a vast rift between Zig and Odin, which we previously looked at. In fact, at this point, it is similarly verbose to the infamous Java hello world of System.out.println("Hello, World").

If we want to do anything more complicated, we need to use a build.zig script*. There is a way to use Zig’s package manager, but to do that we must dig deep into how Zig build and “zon” files work. Once this is done (in my particular case, I copied a project that already had a build.zig with related files for raylib development).

After a lot of fiddling and consulting the docs (as the errors are not helpful), you might discover that this is the

var pos: ray.Vector2 = .{ .x = 640, .y = 320 };

In case someone wonders: no, var pos: ray.Vector2 = .{ 640, 320 }; doesn’t work.

Of course, if you don’t use pos, the compiler will say that this is an error and refuse to compile as well. And if you discard that error (typically using _ = pos;), the compiler will complain that it should be a const.

Not warnings here, compilation errors.

Converting the example from the Odin article, we get this:

const std = @import("std");
const rl = @import("raylib.zig").raylib;

pub fn main() void {
    rl.InitWindow(1280, 768, "Testing");
    var pos: rl.Vector2 = .{ .x = 640, .y = 320 };
    while (!rl.WindowShouldClose()) {
        rl.BeginDrawing();
        rl.ClearBackground(rl.BLUE);
        rl.DrawRectangleV(pos, .{.x = 32, .y = 32}, rl.GREEN);
        if (rl.IsKeyDown(rl.KEY_LEFT)) {
           pos.x -= 400 * rl.GetFrameTime();
        }
        if (rl.IsKeyDown(rl.KEY_RIGHT)) {
           pos.x += 400 * rl.GetFrameTime();
        }
        if (rl.IsKeyDown(rl.KEY_UP)) {
           pos.y -= 400 * rl.GetFrameTime();
        }
        if (rl.IsKeyDown(rl.KEY_DOWN)) {
           pos.y += 400 * rl.GetFrameTime();
        }
        rl.EndDrawing();
    }
    rl.CloseWindow();
}

Zig can’t compete with Odin’s bundled “vendor”, of course, but the whole process of using build.zig is so much work up front trying to figure out how the Zig build system works – a daunting task if you’re still learning the language, whereas for Odin it “just works”.

The mandatory names in the initialisers (.x etc.) feel just superfluous and unergonomic for things like vectors. Despite looking more like C than Odin, Zig keeps having its own way of doing things, violating expectations.

* EDIT: I asked about this on the Zig discord, and I was told the following: “99.5% of zig code can be compiled by zig build-exe. In any case, zig init will generate a basic one that rarely needs to be changed. Build.zig is easier to learn if you know zig than cmake if you know zig, because it’s already zig code.” However, my point is that as a beginner one might not know enough Zig to edit the build.zig in the first place, so this to me is placing the cart before the horse.

Error handling

Zig’s error handling is fairly novel: it returns a kind of “Result” type that needs to be immediately handled. Zig errors can also optionally be implicitly returned or invoke a panic:

const std = @import("std");

pub fn main() !void {
    const file = std.fs.cwd().openFile("foo.txt", .{}) catch |err| label: {
        std.debug.print("unable to open file: {}\n", .{err});
        return;
    };
    // "try" rethrows the error, similar to Odin's "or_return"
    // const file = try std.fs.cwd().openFile("foo.txt", .{});
    // Alternatively panic:
    // const file = std.fs.cwd().openFile("foo.txt", .{}) catch unreachable;
    defer file.close();
    try file.writeAll("test");
}

Unlike exceptions or even Odin’s approach, there is no simple way to handle all errors in a single sweep, which means that usually Zig code will use try and handle all results at a higher level. We could continue diving into error handling, but for a supposedly simple language, Zig has a lot of new features that need covering.

Zig comptime

According to many Zig proponents, the poster child for Zig’s versatility and simplicity is its compile-time execution. As we’ll see, Zig’s compile time is more of a cross between a toned-down Jai meta-programming and C++ templates.

Zig’s compile-time execution serves three main goals:

  1. To enable polymorphic functions.
  2. To generate generic types.
  3. To conditionally compile code.

The primary mechanism comes from Zig taking untyped or compile-time-only arguments. Compile-time-only arguments are things like types, which only have representation at compile-time. Untyped arguments allow compile-time “duck typing”.

Unfortunately, the “duck typing” suffers the same problem as C++ templates – that instantiating them creates an error that possibly is shown rather deep into the generic library code.

The generic types that Zig can create are more flexible than C++ templates, though, being able to implement things as SoA (struct of array) types at compile-time. The cost of this is that an IDE that wishes to provide checking on generic Zig types would need to execute the entire generic function in order to understand its actual layout. It is clear from this that, like Jai, Zig does not prioritise IDE friendliness.

This complexity is also something a human reader will have to deal with, and the comptime here is quite implicit in what it does. This implicitness is even worse when conditionally compiling code.

The following code will compile:

const foo : bool = false;
if (foo) {
    const x : i32 = 1.2;
    _ = x;
}

But setting foo to true instead will reveal a compile-time error as the implicit conversion from 1.2 to i32 isn’t allowed. Similarly, a function that isn’t called will not even be type-checked. This allows the somewhat humorous situation where you can write a function and marvel that it compiles in Zig, just to notice that you forgot to call it or declare it public and therefore it’s not actually checked.

While not checking conditionally evaluated code is quite reasonable, the highly implicit way Zig implements it is rather questionable, to say the least.

There is no reason why an ordinary if should lazily evaluate its branches. The language could have used a construct like inline if (which it uses for comptime versions of for loops) to make it explicit, but has chosen to make it more implicit.

While Zig’s comptime undoubtedly makes the language more powerful and unifies three different concepts – generics, compile-time evaluation, and polymorphism – it comes at a clear lack of clarity and explicitness.

It is unclear how this matches the language motto of “no hidden control flow”.

Verbosity

Critics say that Zig is verbose. Is there some merit to this? We already mentioned the long paths (std.debug.print) in passing. Another commonly raised problem is the casts. Zig has tens of different cast operations as Zig “builtins” (special functions that provide functionality that otherwise would not be available) to specify exactly from what category we’re converting to and from (e.g. @intFromFloat), which causes math-heavy code to balloon.

One user submitted the following on Reddit:

fn fib(n: u8) u32 {
    const sqrt_5: f32 = comptime std.math.sqrt(5.0);
    const golden_ratio: f32 = (1.0 + sqrt_5) / 2.0;

    const n_f32: f32 = @floatFromInt(n);
    const result_f32 = @round(std.math.pow(f32, golden_ratio, n_f32) / sqrt_5);

    return @intFromFloat(result_f32);
}

Proponents argue that this makes code “honest” and predictable.

Another complaint has been the added noise due to the syntax changes from C, such as replacing for loops with Zig’s while:*

var block_h: u32 = 0;
while (block_h < block_cnt_h) : (block_h += 1) {
    var block_w: u32 = 0;
    while (block_w < block_cnt_w) : (block_w += 1) {
        // Loop body
    }
}

This translates to the following in C:

for (unsigned block_h = 0; block_h < block_cnt_h; block_h++ ) {
    for (unsigned block_w = 0; block_w < block_cnt_w; block_w++ ) {
        // Loop body
    }
}

Although the Zig community, on the whole, seems happy with Zig’s syntax despite this, it’s hard to know if this is confirmation bias (i.e. people who don’t like the syntax do not use Zig) or if it’s just something that grows on you.

A common refrain on forums is that Zig’s learning curve is steep: it has “strong opinions” about syntax and requires learning many new idioms. As one commenter on Hacker News warns, if a developer does not “appreciate simplicity and control over tiny details,” Zig “will not bring anything particularly interesting” link

* EDIT: On the Zig discord I was told that Zig has a for loop. However, this is actually a foreach style for loop where you cannot modify the counter to make jumps (important when filtering lists for example), so I do not consider them equivalent

The road to Zig 1.0?

Another concern for Zig to tackle is the apparently ever-increasing scope of the project. Zig has soon been in development for ten years, with no clear end in sight. Because Zig is funded by donations, this is not the death knell it would be for a commercial project. However, it remains a long-term concern if the language cannot reach version 1.0. It should be noted that ten years is a fairly common timeframe for a language to reach 1.0, and Odin – about a year “younger” than Zig – is clearly much closer to the elusive milestone.

It is clearly not a lack of funding or contributors: Zig has enough of both. The problem appears more like classic feature creep. The Zig compiler has now been rewritten more than once*; it is also aiming to replace LLVM with its own backends, its own linker, and so on. The list seems endless.

Even if these components are not strictly required for 1.0, they all have to be coordinated, leaving less time for the language itself.

* EDIT: This turned out to be a misconception on my part: the version of the compiler that mixed C++ and Zig was never considered a real rewrite, but only a stepping stone to the pure Zig version of the compiler. The new bootstrap process (the way the the Zig compiler in Zig compiles itself) is discussed here, so Zig has only been fully rewritten once. I stand corrected.

Zig: the good parts

After all this doom and gloom, let’s focus on the area where Zig is already king: cross-platform compilation.

Ironically, cross-platform compilation is already available out of the box with LLVM, but Clang – which builds on LLVM – makes it non-trivial to cross-compile from start to finish.

While solving this problem for Zig, the team realised that with their bundled Clang library, they could offer a better frontend to Clang, providing cross-compilation out of the box.

This has been enormously successful, with major companies adopting “zig cc” (Zig’s Clang frontend) as their preferred compiler for cross-compilation. This cements the Zig compiler as a worthwhile product – even though, ironically, it has little to do with the language itself.

With Zig now present in toolchains, there have been pushes to get companies using build.zig to script their builds, as a way of selling the language itself. While this has seen some success, it hasn’t led to official adoption by any major company.

While Zig users swear by build.zig’s elegance, it’s not universally loved.

Summary

Zig aims to be a modern C replacement with a focus on explicit control, safety, and zero hidden behaviour – but it often trades usability for perceived ideological purity. Features like comptime offer powerful metaprogramming capabilities, but they make standalone tooling and IDE support significantly harder. While it is touted as being safer and faster than C without compromises, it’s doubtful that the faster than C claim holds up under close scrutiny. It’s also worth noting that Zig usually disables safety checks in release builds.

Despite nearly a decade of development, Zig 1.0 still feels far off. Ironically, its biggest success isn’t the language itself but its excellent cross-compilation tooling – so much so that many use zig cc as a drop-in Clang replacement without writing a single line of Zig.

But is this really the best alternative to C? Odin offers more high-level conveniences, while Zig is far more bare-bones. At times, it seems as though within the Zig community, “low-level” and “high performance” are conflated with “no abstractions”, as if something as thin and anæmic as the C standard library were a necessary condition for speed. In several ways, Zig is actually more painful to work with than the C it aims to replace. The argument that Zig is a great C alternative seems to have little to support it.

Aside from first mover advantage, there seems to be little to recommend Zig over Odin or other C alternatives, but as we all know, it’s not always the best alternative that wins.


Discuss this at Hacker News: https://news.ycombinator.com/item?id=43942094 or on r/Programming: https://www.reddit.com/r/programming/comments/1kjigtz/zig_the_ideal_c_replacement_or/

EDITED 2025-05-10 with feedback from the Zig Discord.

联系我们 contact @ memedata.com