Chapter 07

comptime:编译期计算与元编程

Zig 最强大的特性——用运行时代码的语法编写编译期逻辑

什么是 comptime?

comptime 是 Zig 的元编程机制,允许在编译期执行任意 Zig 代码。与 C 的预处理器宏(文本替换,难以调试)或 Rust 的过程宏(复杂 API)不同,Zig 的 comptime 就是普通的 Zig 代码在编译阶段运行。

comptime 的核心用途

comptime 变量与表达式

// comptime 变量:只在编译期存在
comptime var count: usize = 0;
comptime count += 1;
comptime count += 1;
// 运行时 count 不存在,它只是编译期常量 2

// comptime 表达式
const page_size = comptime blk: {
    const base = 4096;
    break :blk base * 2;  // 编译期计算 8192
};

// 编译期断言
comptime {
    if (@sizeOf(usize) != 8) {
        @compileError("需要 64 位平台");
    }
}

// @compileLog:编译期打印(调试用)
comptime {
    @compileLog("page_size =", page_size);
}

泛型:comptime 类型参数

泛型函数

// comptime T: type 表示类型参数
fn identity(comptime T: type, value: T) T {
    return value;
}

const x = identity(i32, 42);       // T = i32
const s = identity([]const u8, "hi");  // T = []const u8
_ = x; _ = s;

// 泛型 max 函数(支持任何可比较类型)
fn max(comptime T: type, a: T, b: T) T {
    return if (a > b) a else b;
}
std.debug.print("max(3,5)={d}\n", .{max(i32, 3, 5)});

泛型数据结构

// Zig 用返回 type 的函数实现泛型类型
fn Stack(comptime T: type) type {
    return struct {
        items: std.ArrayList(T),

        const Self = @This();

        pub fn init(allocator: std.mem.Allocator) Self {
            return .{ .items = std.ArrayList(T).init(allocator) };
        }

        pub fn deinit(self: *Self) void {
            self.items.deinit();
        }

        pub fn push(self: *Self, item: T) !void {
            try self.items.append(item);
        }

        pub fn pop(self: *Self) ?T {
            return self.items.popOrNull();
        }

        pub fn peek(self: Self) ?T {
            if (self.items.items.len == 0) return null;
            return self.items.items[self.items.items.len - 1];
        }
    };
}

pub fn main() !void {
    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
    defer _ = gpa.deinit();

    var stack = Stack(i32).init(gpa.allocator());
    defer stack.deinit();

    try stack.push(1);
    try stack.push(2);
    try stack.push(3);
    std.debug.print("pop: {?d}\n", .{stack.pop()});  // 3
}

@TypeOf、@typeName、@typeInfo

// @TypeOf:获取表达式的类型
const x: i32 = 42;
const T = @TypeOf(x);          // i32
const T2 = @TypeOf(x, 3.14);   // 公共类型(会编译错误:i32 和 f64 不兼容)
_ = T; _ = T2;

// @typeName:获取类型名称字符串(编译期)
std.debug.print("type: {s}\n", .{@typeName(i32)});   // "i32"
std.debug.print("type: {s}\n", .{@typeName(bool)});  // "bool"

// @typeInfo:获取类型的详细结构信息
const info = @typeInfo(i32);
switch (info) {
    .Int => |int_info| {
        std.debug.print("bits={d}, signed={}\n",
            .{ int_info.bits, int_info.signedness == .signed });
    },
    else => {},
}

类型反射与代码生成

// 使用 typeInfo 在编译期生成针对任意结构体的打印函数
fn printStruct(comptime T: type, value: T) void {
    const info = @typeInfo(T);
    if (info != .Struct) @compileError("只支持结构体");

    std.debug.print("{s} {{\n", .{@typeName(T)});
    inline for (info.Struct.fields) |field| {
        // inline for 在编译期展开循环
        std.debug.print("  .{s} = {any}\n",
            .{ field.name, @field(value, field.name) });
    }
    std.debug.print("}}\n", .{});
}

const Person = struct { name: []const u8, age: u32 };
printStruct(Person, .{ .name = "Alice", .age = 30 });
// 输出:
// Person {
//   .name = Alice
//   .age = 30
// }

编译期断言与约束

// 约束泛型参数必须实现某个"接口"
fn sum(comptime T: type, items: []const T) T {
    // 编译期检查 T 是否为数值类型
    comptime {
        const info = @typeInfo(T);
        if (info != .Int and info != .Float) {
            @compileError("sum 只支持整数或浮点类型,得到: " ++ @typeName(T));
        }
    }
    var total: T = 0;
    for (items) |item| total += item;
    return total;
}

const nums = [_]i32{ 1, 2, 3, 4, 5 };
std.debug.print("sum = {d}\n", .{sum(i32, &nums)});  // 15
// sum(bool, &[_]bool{true, false});  // 编译错误!
comptime vs 宏的优势

comptime 代码使用完整的 Zig 语言(可以调用函数、使用循环、访问文件系统),而不是 C 预处理器那样的文本替换。这意味着 comptime 错误有完整的类型信息和行号,调试起来和普通代码一样直观。

comptime 的工作机制

Zig 的编译分两个阶段

理解 comptime 的关键是理解 Zig 编译过程:Zig 编译器在生成机器码之前,会先执行一个"编译期求值"阶段:

编译期(Comptime Phase)
在这个阶段,所有 comptime 标记的表达式、函数和块都会被执行。执行环境是 Zig 内置的 ZIR(Zig Intermediate Representation)解释器——一个完整的 Zig 子集运行时,但仅在编译器内部运行。这个阶段会展开泛型(生成特化版本)、求值常量、执行 @compileError/@compileLog。
类型是第一等公民(Types as First-Class Values)
在 Zig 中,类型本身是 type 类型的 comptime 值(comptime 常量),可以像整数一样传递、存储、操作。const T = i32; 在 comptime 中是合法的——T 是一个 type 类型的变量。这是泛型(泛型参数 comptime T: type)和类型反射(@typeInfo(T))的基础。
运行期(Runtime Phase)
编译期求值完成后,代码中的所有 comptime 表达式已经被结果替换,泛型函数已经被特化(每个不同的类型参数生成一份代码),剩余的代码通过 LLVM 生成机器码。运行期代码完全不感知"这里曾经有 comptime"。

comptime 的典型应用模式

const std = @import("std");

// 模式1:编译期生成 LUT(查找表)
// 比运行时计算快,结果直接编译进二进制
const SIN_TABLE = blk: {
    var table: [360]f32 = undefined;
    var i: usize = 0;
    while (i < 360) : (i += 1) {
        const rad = @as(f32, @floatFromInt(i)) * std.math.pi / 180.0;
        table[i] = std.math.sin(rad);
    }
    break :blk table;
};

// 模式2:编译期选择实现(替代 if-else 条件编译)
fn atomicAdd(comptime T: type, ptr: *T, val: T) T {
    // 编译期检查类型大小,选择对应的原子指令
    return switch (@sizeOf(T)) {
        4 => @atomicRmw(T, ptr, .Add, val, .SeqCst),
        8 => @atomicRmw(T, ptr, .Add, val, .SeqCst),
        else => @compileError("atomicAdd 只支持 4/8 字节类型"),
    };
}

// 模式3:编译期解析配置(替代 build flags)
const build_options = @import("build_options");  // 由 build.zig 生成

const MAX_CONNECTIONS = if (build_options.is_production)
    10000
else
    100;  // 编译期选择,零运行时开销

comptime 的限制与边界条件

不能在 comptime 中做的事
在 comptime 阶段,以下操作是禁止的:调用运行时不确定的函数(读取文件、网络请求、用户输入);对运行时变量取地址(地址在运行时才确定);使用 @extern 声明的外部符号(链接在运行时发生)。尝试这些操作会在编译时报错。
comptime 代码的调试
使用 @compileLog(value) 在编译期打印值(类似 print,但输出在编译时),帮助调试 comptime 逻辑。注意:@compileLog 会让编译失败(设计如此),用完要删除。
第7章小结