概述
第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 a canonical manifest describing the library surface that this module exposes.
/// Other modules import this helper to reason about the package-level 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)。
const std = @import("std");
/// Summary of a package registration as seen from the consumer invoking `--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.
return std.meta.declarations(@This()).len;
}
const std = @import("std");
/// Summary of a package registration as seen from the consumer invoking `--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.
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_demo== 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--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而不是独立工件公开。 |