概述
本章聚焦于包注册模块之后发生的事情——名称如何成为具体导入、编译器何时打开文件以及哪些钩子控制发现(参见build_runner.zig)。我们将建模模块图,阐明文件系统路径和注册命名空间之间的区别,并展示如何保护可选助手而无需散布脆弱的#ifdef样式逻辑。
在此过程中,我们将探索编译时导入、测试特定发现以及使用@hasDecl的安全探测,强化Zig 0.15.2中引入的writer API更改,使每个示例都成为正确stdout用法的参考(参见v0.15.2和File.zig)。
学习目标
- 跟踪构建运行器如何将注册的模块名称扩展为依赖感知的模块图。24
- 区分文件系统相对导入和构建注册模块,并预测在模糊情况下哪个会获胜(参见Build.zig和22)。
- 识别触发模块发现的每个机制:直接导入、
comptime块、test声明、导出和入口点探测(参见start.zig和testing.zig)。 - 应用编译时防护,使可选工具从发布构件中消失,同时保持调试构建的丰富检测(参见19和builtin.zig)。
- 使用
@hasDecl和相关反射助手来检测能力,而不依赖有损的字符串比较或未经检查的假设(参见meta.zig和15)。 - 记录和测试发现策略,以便协作者理解构建图何时会包含额外模块。13
模块图映射
编译器将每个翻译单元转换为类似结构体的命名空间。导入对应于该图中的边,构建运行器为其提供预注册命名空间列表,因此即使磁盘上不存在具有该名称的文件,模块也能确定性解析。
在底层,这些命名空间与内部池、文件和分析工作队列一起存在于Zcu编译状态中:
模块解析在评估@import边时遍历此命名空间图,使用与增量编译和符号解析相同的Zcu和InternPool机制。
Root, , and namespaces
根模块是编译器视为入口点的任何文件。从该根模块,你可以通过@import("root")检查自身,通过@import("std")访问捆绑的标准库,并通过@import("builtin")访问编译器提供的元数据。以下探测打印每个命名空间暴露的内容,并演示基于文件系统的导入(extras.zig)参与相同的图。19
const std = @import("std");
const builtin = @import("builtin");
const root = @import("root");
const extras = @import("extras.zig");
pub fn helperSymbol() void {}
pub fn main() !void {
var stdout_buffer: [512]u8 = undefined;
var file_writer = std.fs.File.stdout().writer(&stdout_buffer);
const out = &file_writer.interface;
try out.print("root has main(): {}\n", .{@hasDecl(root, "main")});
try out.print("root has helperSymbol(): {}\n", .{@hasDecl(root, "helperSymbol")});
try out.print("std namespace type: {s}\n", .{@typeName(@TypeOf(@import("std")))});
try out.print("current build mode: {s}\n", .{@tagName(builtin.mode)});
try out.print("extras.greet(): {s}\n", .{extras.greet()});
try out.flush();
}
$ zig run 01_root_namespace.zigroot has main(): true
root has helperSymbol(): true
std namespace type: type
current build mode: Debug
extras.greet(): extras namespace discovered via file path对std.fs.File.stdout().writer(&buffer)的调用反映了0.15.2的writer API:我们缓冲、打印和刷新以避免截断输出,同时保持无分配器。v0.15.2
Names registered by the 构建图
当你调用b.createModule或exe.addModule时,你注册一个命名空间名称(例如"logging")和一个根源文件。该构建图中的任何@import("logging")都指向已注册的模块,即使logging.zig文件位于调用者旁边。只有当找不到已注册的命名空间时,编译器才会回退到相对于导入文件的基于路径的解析。这就是通过build.zig.zon获取的依赖项如何暴露其模块的方式:构建脚本在用户代码执行之前很久就构建了图。24
编译器强制要求给定文件恰好属于一个模块。编译错误测试套件包含一个情况,其中同一文件既作为已注册模块又作为直接文件路径导入,这会被拒绝:
const case = ctx.obj("file in multiple modules", b.graph.host);
case.addDepModule("foo", "foo.zig");
case.addError(
\\comptime {
\\ _ = @import("foo");
\\ _ = @import("foo.zig");
\\}
, &[_][]const u8{
":1:1: error: file exists in modules 'foo' and 'root'",
":1:1: note: files must belong to only one module",
":1:1: note: file is the root of module 'foo'",
":3:17: note: file is imported here by the root of module 'root'",
});这演示了一个文件可以是已注册模块的根,也可以通过基于路径的导入成为根模块的一部分,但不能同时两者兼得。
发现触发器和时机
模块发现从导入字符串在编译时已知的那一刻开始。编译器以波的形式解析依赖图,一旦在comptime上下文中评估导入,就排队新模块。15
导入、和评估顺序
comptime块在语义分析期间运行。如果它包含_ = @import("tooling.zig");,构建运行器会立即解析和解析该模块——即使运行时从不引用它。使用显式策略(标志、优化模式或构建选项),使此类导入可预测而非令人惊讶。
抵制在@import内联字符串连接的诱惑;Zig无论如何都需要导入目标是编译时已知的字符串,因此首选记录意图的单个常量。
测试、导出和入口探测
test块和pub export声明也会触发发现。当你运行zig test时,编译器导入每个包含测试的模块,注入一个合成主函数,并调用std.testing测试框架助手。类似地,std.start检查根模块的main、_start和平台特定的入口点,沿途拉入这些声明引用的任何模块。这就是为什么即使是休眠的测试助手也必须位于comptime防护之后;否则它们会仅仅因为存在test声明而泄漏到生产构件中。19
在Zig编译器自身的构建中,从测试声明通过到测试运行器和命令的路径如下所示:
这清楚地表明,添加声明不仅会拉入,还会将你的模块连接到由驱动的测试构建和执行管道中。
条件发现模式
可选工具不应要求单独的代码库分支。相反,应从编译时数据驱动发现,并通过反射命名空间来决定激活什么。15
使用优化模式门控模块
优化模式内置于builtin.mode中。使用它仅在构建Debug时导入昂贵的诊断工具。下面的示例在Debug构建期间连接debug_tools.zig,在ReleaseFast时跳过它,同时演示Zig 0.15.2所需的缓冲写入器模式。
const std = @import("std");
const builtin = @import("builtin");
pub fn main() !void {
comptime {
if (builtin.mode == .Debug) {
_ = @import("debug_tools.zig");
}
}
var stdout_buffer: [512]u8 = undefined;
var file_writer = std.fs.File.stdout().writer(&stdout_buffer);
const out = &file_writer.interface;
try out.print("build mode: {s}\n", .{@tagName(builtin.mode)});
if (comptime builtin.mode == .Debug) {
const debug = @import("debug_tools.zig");
try out.print("{s}\n", .{debug.banner});
} else {
try out.print("no debug tooling imported\n", .{});
}
try out.flush();
}
$ zig run 02_conditional_import.zigbuild mode: Debug
debug tooling wired at comptime$ zig run -OReleaseFast 02_conditional_import.zigbuild mode: ReleaseFast
no debug tooling imported因为@import("debug_tools.zig")位于comptime条件之后,ReleaseFast二进制文件甚至不会解析助手,保护构建不会意外依赖于仅调试的全局变量。
使用进行安全探测
与其假设模块导出特定函数,不如探测它。这里我们暴露一个plugins命名空间,它要么转发到plugins_enabled.zig,要么返回空结构体。@hasDecl在编译时告诉我们可选的install钩子是否存在,启用一个在每个构建模式下都能工作的安全运行时分支。15
const std = @import("std");
const plugins = @import("plugins.zig");
pub fn main() !void {
var stdout_buffer: [512]u8 = undefined;
var file_writer = std.fs.File.stdout().writer(&stdout_buffer);
const out = &file_writer.interface;
if (comptime @hasDecl(plugins.namespace, "install")) {
try out.print("plugin discovered: {s}\n", .{plugins.namespace.install()});
} else {
try out.print("no plugin available; continuing safely\n", .{});
}
try out.flush();
}
$ zig run 03_safe_probe.zigplugin discovered: Diagnostics overlay instrumentation active$ zig run -OReleaseFast 03_safe_probe.zigno plugin available; continuing safely注意我们在命名空间类型本身(plugins.namespace)上测试声明。这使根模块对插件的内部结构不可知,并避免基于字符串类型的特性切换。19
操作指南
注意事项
练习
- 扩展
01_root_namespace.zig,使其迭代@typeInfo(@import("root")).Struct.decls,打印一个排序的符号表以及每个符号所在的模块。15 - 修改
02_conditional_import.zig,将调试工具门控在构建选项布尔值之后(例如-Ddev-inspect=true),并记录构建脚本如何通过第22章中的b.addOptions来传递该选项。22 - 创建一个兄弟模块,仅在
builtin.mode == .Debug时使用comptime { _ = @import("helper.zig"); },然后编写一个测试,断言助手在ReleaseFast中永远不会编译。13