Chapter 20Concept Primer Modules Vs Programs Vs Packages Vs Libraries

概念入门

概述

第19章映射了编译器的模块图;本章将命名这些模块可以扮演的角色,以便你了解文件何时仅仅是助手,何时升级为程序,以及何时成为可重用包或库的核心。

我们还将预览Zig CLI如何为消费者注册模块,为第21章build.zig中的构建图编写奠定基础。

学习目标

  • 区分模块、程序、包和库,并解释Zig在编译期间如何处理每个。
  • 使用--dep-M标志(及其构建图等效项)为消费者注册命名模块。
  • 在启动新工件或重构现有工件时,应用一个实用的清单来选择正确的单元。19

构建共享词汇表

在连接构建脚本或注册依赖项之前,请确定一致的语言:在Zig中,模块@import返回的任何编译单元,程序是具有入口点的模块图,捆绑了模块和元数据,而是一个旨在重用且没有根main的包。 start.zig

实践中的模块和程序

这个演示从一个根模块开始,该模块导出一个库的清单,但也声明了main,因此运行时将该图视为一个程序,而助手模块则内省公共符号以保持术语的真实性。19

Zig

// 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();
}
运行
Shell
$ zig run module_role_map.zig
输出
Shell
== 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可执行文件链接libcmainmain()
Windows可执行文件默认wWinMainCRTStartupWinStartup() / wWinMainCRTStartup()
Windows动态库默认_DllMainCRTStartup_DllMainCRTStartup()
UEFI可执行文件默认EfiMainEfiMain()
WASI可执行文件(命令)默认_startwasi_start()
WASI可执行文件(响应器)默认_initializewasi_start()
WebAssembly独立式默认_startwasm_freestanding_start()
WebAssembly链接libc默认__main_argc_argvmainWithoutEnv()
OpenCL/Vulkan内核默认mainspirvMain2()
MIPS任何默认__start(与_start相同)

来源:start.zig

编译时入口点逻辑

在编译时,std.start根据builtin.output_modebuiltin.oslink_libc和目标架构运行一个小的决策树,以精确地导出上述符号中的一个:

graph TB Start["comptime 块<br/>(start.zig:28)"] CheckMode["检查 builtin.output_mode"] CheckSimplified["simplified_logic?<br/>(stage2 后端)"] CheckLinkC["link_libc 或<br/>object_format == .c?"] CheckWindows["builtin.os == .windows?"] CheckUEFI["builtin.os == .uefi?"] CheckWASI["builtin.os == .wasi?"] CheckWasm["arch.isWasm() &&<br/>os == .freestanding?"] ExportMain["@export(&main, 'main')"] ExportWinMain["@export(&WinStartup,<br/>'wWinMainCRTStartup')"] ExportStart["@export(&_start, '_start')"] ExportEfi["@export(&EfiMain, 'EfiMain')"] ExportWasi["@export(&wasi_start,<br/>wasm_start_sym)"] ExportWasmStart["@export(&wasm_freestanding_start,<br/>'_start')"] Start --> CheckMode CheckMode -->|".Exe 或有 main"| CheckSimplified CheckSimplified -->|"true"| Simple["简化逻辑<br/>(第 33-51 行)"] CheckSimplified -->|"false"| CheckLinkC CheckLinkC -->|"是"| ExportMain CheckLinkC -->|"否"| CheckWindows CheckWindows -->|"是"| ExportWinMain CheckWindows -->|"否"| CheckUEFI CheckUEFI -->|"是"| ExportEfi CheckUEFI -->|"否"| CheckWASI CheckWASI -->|"是"| ExportWasi CheckWASI -->|"否"| CheckWasm CheckWasm -->|"是"| ExportWasmStart CheckWasm -->|"否"| ExportStart

来源:lib/std/start.zig:28-100

库清单和内部重用

记录在pkg/manifest.zig中的清单模型最终会成为包元数据:名称、语义版本、模块列表,以及一个明确的声明,即不导出任何入口点。

包作为分发契约

包是生产者和消费者之间的协议:生产者注册模块名称并公开元数据;消费者导入这些名称,而无需触及文件系统路径,信任构建图提供正确的代码。

使用-M和--dep注册模块

Zig 0.15.2用-M(模块定义)和--dep(导入表条目)取代了旧的--pkg-begin/--pkg-end语法,这与std.build在连接工作区时所做的事情类似(参见Build.zig)。

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;
}
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;
}
运行
Shell
$ zig build-exe --dep overlay -Mroot=package_overlay_demo.zig -Moverlay=overlay_widget.zig -femit-bin=overlay_demo && ./overlay_demo
输出
Shell
== 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.Buildstd.Build.Module节点的形式在构建图中重新出现。下图总结了本机构建系统的入口点如何连接编译器编译、测试、文档和安装。

graph TB subgraph "构建入口点" BUILD_FN["build(b: *std.Build)"] --> OPTIONS["解析构建选项"] OPTIONS --> COMPILER["addCompilerStep()"] OPTIONS --> TEST_SETUP["测试套件设置"] OPTIONS --> DOCS["文档步骤"] end subgraph "编译器编译" COMPILER --> EXE["std.Build.CompileStep<br/>(zig 可执行文件)"] EXE --> COMPILER_MOD["addCompilerMod()"] EXE --> BUILD_OPTIONS["build_options<br/>(生成配置)"] EXE --> LLVM_INTEGRATION["LLVM/Clang/LLD<br/>链接"] end subgraph "测试步骤" TEST_SETUP --> TEST_CASES["test-cases<br/>tests.addCases()"] TEST_SETUP --> TEST_MODULES["test-modules<br/>tests.addModuleTests()"] TEST_SETUP --> TEST_UNIT["test-unit<br/>编译器单元测试"] TEST_SETUP --> TEST_STANDALONE["test-standalone"] TEST_SETUP --> TEST_CLI["test-cli"] end subgraph "文档" DOCS --> LANGREF_GEN["generateLangRef()<br/>(tools/docgen.zig)"] DOCS --> STD_DOCS["autodoc_test<br/>(lib/std/std.zig)"] end subgraph "安装" EXE --> INSTALL_BIN["stage3/bin/zig"] INSTALL_LIB_DIR["lib/目录"] --> INSTALL_LIB_TARGET["stage3/lib/zig/"] LANGREF_GEN --> INSTALL_LANGREF["stage3/doc/langref.html"] STD_DOCS --> INSTALL_STD_DOCS["stage3/doc/std/"] end

记录包意图

除了CLI标志,意图还存在于文档中:描述哪些模块是公共的,你是否期望下游入口点,以及其他构建图应如何使用该包(参见Module.zig)。

快速选择正确的单元

在决定下一步创建什么时,请使用下面的备忘单;它是有意为之,以便团队形成共享的默认值。19

你想…优先选择理由
发布没有入口点的可重用算法包 + 库将模块与元数据捆绑,以便消费者可以通过名称导入并与路径解耦。
发布命令行工具程序导出一个main(或_start)并保持助手模块私有,除非你打算共享它们。
在同一个仓库中跨文件共享类型模块使用普通的@import来公开命名空间,而不会过早地耦合构建元数据。19

工件类型一览

编译器的output_modelink_mode选择决定了支持每个概念角色的具体工件形式。程序通常构建为可执行文件,而库则使用可以是静态或动态的Lib输出。

graph LR subgraph "输出模式 + 链接模式 = 工件类型" Exe_static["output_mode: Exe<br/>link_mode: static"] --> ExeStatic["静态可执行文件"] Exe_dynamic["output_mode: Exe<br/>link_mode: dynamic"] --> ExeDynamic["动态可执行文件"] Lib_static["output_mode: Lib<br/>link_mode: static"] --> LibStatic["静态库 (.a)"] Lib_dynamic["output_mode: Lib<br/>link_mode: dynamic"] --> LibDynamic["共享库 (.so/.dll)"] Obj["output_mode: Obj<br/>link_mode: N/A"] --> ObjFile["目标文件 (.o)"] end

来源:Config.zig, main.zig, builtin.zig

你可以使用简单的映射将本章的词汇与这些工件类型结合起来:

角色典型工件备注
程序output_mode: Exe(静态或动态)公开一个入口点;也可能在内部导出助手模块。
库包output_mode: Lib(静态或共享)旨在重用;没有根main,消费者按名称导入模块。
内部模块取决于上下文通常作为可执行文件或库的一部分编译;通过@import而不是独立工件公开。

注意与警告

  • 即使在临时模块中也要记录类似清单的数据,以便以后晋升为包是机械化的。
  • 当将程序转换为库时,删除或保护入口点;否则消费者会得到冲突的根。19
  • -M/--dep工作流是std.build.Module的一个薄薄的封装,因此一旦你的项目超过单个二进制文件,就优先使用构建图。21

练习

  • 扩展module_role_map.zig,使备忘单由从JSON清单加载的数据驱动,然后将人体工程学与直接的Zig结构体进行比较。12json.zig
  • 修改覆盖演示以注册两个外部模块并发出它们的声明计数,以加强--dep如何将多个导入排队。
  • 草拟一个zig build脚本来包装覆盖示例,验证CLI标志是否干净地映射到b.addModulemodule.addImport21

注意事项、替代方案、边缘情况

  • 交叉编译包可能会公开target特定的模块;记录条件导入以防止意外的名称解析失败。
  • 如果你在同一个构建图中两次注册模块名称,zig CLI会报告冲突——将其视为重构的信号,而不是依赖排序。19
  • 一些工具仍然期望使用已弃用的--pkg-begin语法;与编译器同步升级脚本,以保持依赖注册的一致性。v0.15.2

Help make this chapter better.

Found a typo, rough edge, or missing explanation? Open an issue or propose a small improvement on GitHub.