概述
第19章映射了编译器的模块图;本章命名这些模块可以扮演的角色,以便您知道文件何时仅仅是助手,何时升级为程序,以及何时成为可重用包或库的核心。
学习目标
- 区分模块、程序、包和库,并解释Zig在编译过程中如何对待每种类型。
- 使用
--dep和-M标志(及其构建图等价物)为消费者注册命名模块。 - 在开始新构件或重构现有构件时,应用实用检查表选择正确的单元。19
建立共享词汇表
在编写构建脚本或注册依赖项之前,请确定一致的语言:在Zig中,模块是由@import返回的任何编译单元,程序是具有入口点的模块图,包捆绑模块加元数据,而库是旨在重用而没有根main的包。
start.zig
实践中的模块和程序
此演示从一个根模块开始,该模块导出库的清单但也声明main,因此运行时将图视为程序,而助手模块内省公共符号以保持术语诚实。19
// This module demonstrates how Zig's module system distinguishes between different roles:
// programs (with main), libraries (exposing public APIs), and hybrid modules.
// It showcases introspection of module characteristics and role-based decision making.
const std = @import("std");
const roles = @import("role_checks.zig");
const manifest_pkg = @import("pkg/manifest.zig");
// List of public declarations intentionally exported by the root module.
// This array defines the public API surface that other modules can rely on.
// It serves as documentation and can be used for validation or tooling.
pub const PublicSurface = [_][]const u8{
"main",
"libraryManifest",
"PublicSurface",
};
// Provide 一个 canonical manifest describing 库 surface 该 此 module exposes.
// 其他模块导入此辅助函数以推理包级API。
// Returns a Manifest struct containing metadata about the library's public interface.
pub fn libraryManifest() manifest_pkg.Manifest {
// Delegate to the manifest package to construct a sample library descriptor
return manifest_pkg.sampleLibrary();
}
// Entry point demonstrating module role classification and vocabulary.
// Analyzes both the root module and a library module, printing their characteristics:
// - Whether they export a main function (indicating program vs library intent)
// - Public symbol counts (API surface area)
// - Role recommendations based on module structure
pub fn main() !void {
// Use a fixed-size stack buffer for stdout to avoid heap allocation
var stdout_buffer: [768]u8 = undefined;
var file_writer = std.fs.File.stdout().writer(&stdout_buffer);
const stdout = &file_writer.interface;
// Capture snapshots of module characteristics for analysis
const root_snapshot = roles.rootSnapshot();
const library_snapshot = roles.librarySnapshot();
// Retrieve role-based decision guidance
const decisions = roles.decisions();
try stdout.print("== Module vocabulary demo ==\n", .{});
// Display root module role determination based on main export
try stdout.print(
"root exports main? {s} → treat as {s}\n",
.{
if (root_snapshot.exports_main) "yes" else "no",
root_snapshot.role,
},
);
// Show the number of public declarations in the root module
try stdout.print(
"root public surface: {d} declarations\n",
.{root_snapshot.public_symbol_count},
);
// Display library module metadata: name, version, and main export status
try stdout.print(
"library '{s}' v{s} exports main? {s}\n",
.{
library_snapshot.name,
library_snapshot.version,
if (library_snapshot.exports_main) "yes" else "no",
},
);
// Show the count of public modules or symbols in the library
try stdout.print(
"library modules listed: {d}\n",
.{library_snapshot.public_symbol_count},
);
// Print architectural guidance for different module design goals
try stdout.print("intent cheat sheet:\n", .{});
for (decisions) |entry| {
try stdout.print(" - {s} → {s}\n", .{ entry.goal, entry.recommendation });
}
// Flush buffered output to ensure all content is written
try stdout.flush();
}
$ zig run module_role_map.zig== Module vocabulary demo ==
root exports main? yes → treat as program
root public surface: 3 declarations
library 'widgetlib' v0.1.0 exports main? no
library modules listed: 2
intent cheat sheet:
- ship a CLI entry point → program
- publish reusable code → package + library
- share type definitions inside a workspace → module保持根导出最小化,并在一个地方(此处为PublicSurface)记录它们,以便助手模块可以在不依赖未记录的全局变量的情况下推理意图。
底层原理:入口点和程序
模块图表现为程序还是库取决于它最终是否导出入口点符号。std.start根据平台、链接模式和几个builtin字段决定导出哪个符号,因此main的存在只是故事的一部分。
入口点符号表
| 平台 | 链接模式 | 条件 | 导出符号 | 处理函数 |
|---|---|---|---|---|
| POSIX/Linux | 可执行文件 | 默认 | _start | _start() |
| POSIX/Linux | 可执行文件 | 链接libc | main | main() |
| Windows | 可执行文件 | 默认 | wWinMainCRTStartup | WinStartup() / wWinMainCRTStartup() |
| Windows | 动态库 | 默认 | _DllMainCRTStartup | _DllMainCRTStartup() |
| UEFI | 可执行文件 | 默认 | EfiMain | EfiMain() |
| WASI | 可执行文件(命令) | 默认 | _start | wasi_start() |
| WASI | 可执行文件(反应器) | 默认 | _initialize | wasi_start() |
| WebAssembly | 独立环境 | 默认 | _start | wasm_freestanding_start() |
| WebAssembly | 链接libc | 默认 | __main_argc_argv | mainWithoutEnv() |
| OpenCL/Vulkan | 内核 | 默认 | main | spirvMain2() |
| MIPS | 任意 | 默认 | __start | (与_start相同) |
来源:start.zig
编译时入口点逻辑
在编译时,std.start在builtin.output_mode、builtin.os、link_libc和目标架构上运行一个小决策树,以导出上述符号中的一个:
库清单和内部重用
记录在pkg/manifest.zig中的清单模拟了最终成为包元数据的内容:名称、语义版本、模块列表以及明确声明不导出入口点。
包作为分发契约
包是生产者和消费者之间的协议:生产者注册模块名称并暴露元数据;消费者导入这些名称而不接触文件系统路径,信任构建图提供正确的代码。
使用 -M 和 --dep 注册模块
Zig 0.15.2 用 -M(模块定义)和 --dep(导入表条目)替换了旧的 --pkg-begin/--pkg-end 语法,反映了 std.build 在连接工作区时的做法(参见 Build.zig)。
// Import the standard library for common utilities and types
const std = @import("std");
// Import builtin module to access compile-time information about the build
const builtin = @import("builtin");
// Import the overlay module by name as it will be registered via --dep/-M on the CLI
const overlay = @import("overlay");
// / Entry point for the package overlay demonstration program.
// / Demonstrates how to use the overlay_widget library to display package information
// / including build mode and target operating system details.
pub fn main() !void {
// Allocate a fixed-size buffer on the stack for stdout operations
// This avoids heap allocation for simple output scenarios
var stdout_buffer: [512]u8 = undefined;
// Create a buffered writer for stdout to improve performance by batching writes
var file_writer = std.fs.File.stdout().writer(&stdout_buffer);
const stdout = &file_writer.interface;
// Populate package details structure with information about the current package
// This includes compile-time information like optimization mode and target OS
const details = overlay.PackageDetails{
.package_name = "overlay",
.role = "library package",
// Extract the optimization mode name (e.g., Debug, ReleaseFast) at compile time
.optimize_mode = @tagName(builtin.mode),
// Extract the target OS name (e.g., linux, windows) at compile time
.target_os = @tagName(builtin.target.os.tag),
};
// Render the package summary to stdout using the overlay library
try overlay.renderSummary(stdout, details);
// Ensure all buffered output is written to the terminal
try stdout.flush();
}
const std = @import("std");
// Summary of a package registration as seen from the consumer invoking `--pkg-begin`.
// 从调用 `--pkg-begin` 的消费者视角看到的包注册摘要。
pub const PackageDetails = struct {
package_name: []const u8,
role: []const u8,
optimize_mode: []const u8,
target_os: []const u8,
};
// Render a formatted summary that demonstrates how package registration exposes modules by name.
// 渲染格式化摘要,演示包注册如何按名称公开模块。
pub fn renderSummary(writer: anytype, details: PackageDetails) !void {
try writer.print("registered package: {s}\n", .{details.package_name});
try writer.print("role advertised: {s}\n", .{details.role});
try writer.print("optimize mode: {s}\n", .{details.optimize_mode});
try writer.print("target os: {s}\n", .{details.target_os});
try writer.print(
"resolved module namespace: overlay → pub decls: {d}\n",
.{moduleDeclCount()},
);
}
fn moduleDeclCount() usize {
// Enumerate the declarations exported by this module to simulate API surface reporting.
// 枚举此模块导出的声明以模拟API表面报告。
return std.meta.declarations(@This()).len;
}
$ zig build-exe --dep overlay -Mroot=package_overlay_demo.zig -Moverlay=overlay_widget.zig -femit-bin=overlay_demo && ./overlay_demoregistered package: overlay
role advertised: library package
optimize mode: Debug
target os: linux
resolved module namespace: overlay → pub decls: 2--dep overlay 必须在使用它的模块声明之前;否则导入表将保持为空,编译器无法解析 @import("overlay")。
案例研究:编译器引导命令
Zig 编译器本身也是使用相同的 -M/--dep 机制构建的。在从 zig1 引导到 zig2 的过程中,命令行连接多个命名模块及其依赖项:
zig1 <lib-dir> build-exe -ofmt=c -lc -OReleaseSmall \ --name zig2 \ -femit-bin=zig2.c \ -target <host-triple> \ --dep build_options \ --dep aro \ -Mroot=src/main.zig \ -Mbuild_options=config.zig \ -Maro=lib/compiler/aro/aro.zig
在这里,每个 --dep 行都会为下一个 -M 模块声明排队依赖项,就像在小型覆盖演示中一样,但规模是编译器级别的。
从CLI标志到构建图
一旦您从临时 zig build-exe 命令转移到 build.zig 文件,相同的概念会重新出现在构建图中作为 std.Build 和 std.Build.Module 节点。下图总结了原生构建系统的入口点如何连接编译器编译、测试、文档和安装。
记录包意图
除了CLI标志之外,意图存在于文档中:描述哪些模块是公共的,您是否期望下游入口点,以及包应如何被其他构建图消费(参见 Module.zig)。
快速选择正确的单元
在决定接下来要创建什么时,请使用下面的速查表;它故意带有观点,以便团队形成共享的默认值。19
| 您想要… | 首选 | 理由 |
|---|---|---|
| 发布没有入口点的可重用算法 | 包 + 库 | 捆绑模块和元数据,以便消费者可以通过名称导入,并与路径解耦。 |
| 发布命令行工具 | 程序 | 导出main(或_start),除非您打算共享它们,否则保持助手模块私有。 |
| 在单个仓库内的文件之间共享类型 | 模块 | 使用普通的@import暴露命名空间,而不会过早耦合构建元数据。19 |
工件类型概览
编译器的output_mode和link_mode选择决定了支持每个概念角色的具体工件形式。程序通常构建为可执行文件,而库使用可以是静态或动态的Lib输出。
来源:Config.zig, main.zig, builtin.zig
您可以将本章的词汇表与这些工件类型通过简单的映射结合起来:
| 角色 | 典型工件 | 说明 |
|---|---|---|
| 程序 | output_mode: Exe (静态或动态) | 暴露入口点;也可以在内部导出助手模块。 |
| 库包 | output_mode: Lib (静态或共享) | 旨在重用;没有根main,消费者通过名称导入模块。 |
| 内部模块 | 取决于上下文 | 通常作为可执行文件或库的一部分编译;通过@import暴露,而不是独立的工件。 |