乐于分享
好东西不私藏

Zig 如何用源码实现编译即执行(第33篇)

Zig 如何用源码实现编译即执行(第33篇)

大家好,我是 Zachel,欢迎来到 Zig 源码学习系列第33篇!

姐妹们/兄弟们,我最近又把 Zig 主仓库的源码翻了个底朝天,这次真的被“编译即执行”这个设计惊艳到了!Zig 的 comptime 能力简直是黑科技中的黑科技——你在源码里写一段逻辑,它就能在编译阶段直接跑起来,生成常量、展开泛型、甚至做复杂的计算,却完全零运行时开销。我第一次看到 Sema.zig 里那个“ZIR 解释器”的时候,忍不住在心里惊呼:这编译器也太懂程序员了吧!

今天我们就一起钻进源码,看看 Zig 到底是怎么用自己的源码,实现这个“编译即执行”的魔法。

1. 背景/现象引入

在日常开发里,你是不是经常遇到这种场景:想在编译期就把配置算好、把数组预生成、把泛型参数的类型检查提前做完?其他语言要么只能用宏(丑陋)、要么只能用有限的 const fn(受限),而 Zig 直接告诉你——“把代码写成普通函数就行,我帮你在编译时跑”。

这就是 Zig 的 comptime:任何表达式、函数、甚至循环,只要处于 comptime 上下文,就能被编译器直接执行。结果直接变成二进制里的常量,运行时零成本。超级常用,却又强大到让人上头:写一个 comptime 函数,就能生成整个数据结构、做元编程,还能提前捕获错误。

但它是怎么实现的呢?答案藏在 Zig 编译器自己的源码里——它把自己变成了一个解释器!

2. 源码深度解析

Zig 编译流程是:Parser → AstGen → ZIR(Zig Intermediate Representation)→ Sema(语义分析)→ Codegen。

其中“编译即执行”的核心,就在 src/Sema.zig 这个文件中(Codeberg 主仓库:https://codeberg.org/ziglang/zig/src/branch/master/src/Sema.zig)。

Sema 本质上就是一个 ZIR 解释器。它逐条处理 ZIR 指令,当发现当前 Block 是 comptime 时,就直接求值,而不是生成运行时代码。

来看几个关键函数(直接来自最新主分支源码):

fn isComptime(block: Block) bool {
    return block.comptime_reason != null;
}

这个简单的判断,决定了后面所有指令的执行路径。

再看如何强制要求 comptime 值:

fn resolveConstValue(
    sema: *Sema,
    block: *Block,
    src: LazySrcLoc,
    inst: Air.Inst.Ref,
    reason: ?ComptimeReason,
) CompileError!Value {
    assert(reason != null or block.isComptime());
    return sema.resolveValue(inst) orelse {
        return sema.failWithNeededComptime(block, src, reason);
    };
}

如果值不是 comptime-known,就会触发 failWithNeededComptime,给出人性化的报错(我们第30篇聊过的 ErrorBundle 就在这里发挥作用)。

更酷的是 comptime 专属分配:

fn newComptimeAlloc(sema: *Sema, block: *Block, src: LazySrcLoc, ty: Type, alignment: Alignment) !ComptimeAllocIndex {
    // ... 创建 ComptimeAlloc 记录
    const idx = sema.comptime_allocs.items.len;
    try sema.comptime_allocs.append(sema.gpa, .{ ... });
    return @enumFromInt(idx);
}

还有处理 comptime 控制流的 analyzeBodyRuntimeBreak 和 analyzeFnBody,它们会把 comptime break 转成运行时逻辑,或者直接在解释器里执行。

Sema 里还有一大堆 zirXXX 函数(比如 zirAllocComptimezirPaniczirCompileError),每一条都在 switch 里根据 block.isComptime() 分支处理——这就是“用源码实现编译即执行”的灵魂。

3. 核心知识点全面拆解

Zig 的设计理念是:编译器和语言统一用 Zig 写,所以 comptime 执行天然高效且安全。

  • ZIR 是桥梁:源码先转成 ZIR(一种类似字节码的中间表示),Sema 就是它的解释器。
  • ComptimeReason:记录为什么进入 comptime(comptime 参数、comptime-only 类型、inline 调用等),报错时能给出精准 note。
  • ComptimeAlloc & InternPool:comptime 下的变量/指针/分配都记录在 InternPool 里,实现值共享和去重,避免内存爆炸。
  • 泛型求值:泛型函数的参数和返回类型会在 comptime 单独切换 sema.code 和 inst_map 求值,保证类型安全。
  • 控制流与安全:comptime 里支持 break/continue,但 Sema 会用 ComptimeBreak 错误优雅处理;@panic/@trap 在 comptime 直接报编译错误。
  • 性能与配额:为了防止无限循环,comptime 有 @setEvalBranchQuota,这是语言层面的防护。
  • 安全性:comptime-only 类型(如 typecomptime_int)强制在编译期解析,杜绝运行时滥用。

整个过程零运行时开销,因为结果直接内联进最终二进制。

4. 实际代码实例

实例1:普通 comptime 计算(基础用法)

const std = @import("std");

fn factorial(comptime n: u32) u32 {
    if (n == 0) return 1;
    return n * factorial(n - 1);
}

pub fn main() void {
    const result = comptime factorial(10);  // 编译期直接算好
    std.debug.print("10! = {}\n", .{result});
}

运行结果:10! = 3628800 —— 编译后二进制里已经是个常量。

实例2:comptime 生成数组(进阶用法)

const std = @import("std");

fn generateTable(comptime size: usize) [size]u32 {
    var table: [size]u32 = undefined;
    for (0..size) |i| {
        table[i] = @intCast(i * i);  // comptime 循环
    }
    return table;
}

const squares = comptime generateTable(10);

pub fn main() void {
    std.debug.print("squares: {any}\n", .{squares});
}

输出:squares: { 0, 1, 4, 9, 16, 25, 36, 49, 64, 81 } —— 整个数组在编译期生成,运行时零成本。

实例3:黑科技——comptime 泛型元编程

const std = @import("std");

fn makeStruct(comptime fields: []const []const u8) type {
    var struct_fields: [fields.len]std.builtin.Type.StructField = undefined;
    inline for (fields, 0..) |name, i| {
        struct_fields[i] = .{
            .name = name,
            .type = u32,
            .default_value = null,
            .is_comptime = false,
            .alignment = @alignOf(u32),
        };
    }
    return @Type(.{ .@"struct" = .{
        .layout = .auto,
        .fields = &struct_fields,
        .decls = &.{},
        .is_tuple = false,
    }});
}

const MyStruct = comptime makeStruct(&.{ "x", "y" });

pub fn main() void {
    const s = MyStruct{ .x = 1, .y = 2 };
    std.debug.print("Dynamic struct: {}\n", .{s});
}

这个例子在编译期动态生成结构体类型——真正的元编程!

5. 对比/彩蛋

对比 C++ 模板:模板是“文本展开”,容易爆炸且调试地狱;Zig comptime 是真实执行,调试体验和运行时一致。

对比 Rust const fn:Rust 限制极多(不能分配、不能循环复杂逻辑);Zig 几乎图灵完备(配额保护下)。

Zig 设计哲学小彩蛋:Sema 自己就是用 Zig 写的解释器——编译器在用自己的语言解释自己,这才是真正的自举之美!还有 ComptimeReason 和 LazySrcLoc 这些小细节,让报错既毒舌又精准(还记得我们第30篇吗?)。

6. 小结

Zig 的“编译即执行”灵魂,就是Sema 把 ZIR 当作脚本直接解释——源码即执行,执行即常量。

好了,第33篇到此结束。

下篇我们或许聊聊 ZIR 到底是怎么生成的,或者 incremental compilation 如何让 comptime 更快~

如果你也被 Zig 的 comptime 虐过或者惊艳到,欢迎评论区贴出你的代码/报错,我们一起扒源码~

Zachel | Zig源码学习系列第33篇
我们下篇见!🚀