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 一个 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();
}
运行
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.startbuiltin.output_modebuiltin.oslink_libc和目标架构上运行一个小决策树,以导出上述符号中的一个:

graph TB Start["comptime block<br/>(start.zig:28)"] CheckMode["Check builtin.output_mode"] CheckSimplified["simplified_logic?<br/>(stage2 backends)"] CheckLinkC["link_libc or<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 or has main"| CheckSimplified CheckSimplified -->|"true"| Simple["Simplified logic<br/>(lines 33-51)"] CheckSimplified -->|"false"| CheckLinkC CheckLinkC -->|"yes"| ExportMain CheckLinkC -->|"no"| CheckWindows CheckWindows -->|"yes"| ExportWinMain CheckWindows -->|"no"| CheckUEFI CheckUEFI -->|"yes"| ExportEfi CheckUEFI -->|"no"| CheckWASI CheckWASI -->|"yes"| ExportWasi CheckWASI -->|"no"| CheckWasm CheckWasm -->|"yes"| ExportWasmStart CheckWasm -->|"no"| 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
// 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();
}
Zig
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;
}
运行
Shell
$ zig build-exe --dep overlay -Mroot=package_overlay_demo.zig -Moverlay=overlay_widget.zig -femit-bin=overlay_demo && ./overlay_demo
输出
Shell
registered 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.Buildstd.Build.Module 节点。下图总结了原生构建系统的入口点如何连接编译器编译、测试、文档和安装。

graph TB subgraph "Build Entry Point" BUILD_FN["build(b: *std.Build)"] --> OPTIONS["Parse Build Options"] OPTIONS --> COMPILER["addCompilerStep()"] OPTIONS --> TEST_SETUP["Test Suite Setup"] OPTIONS --> DOCS["Documentation Steps"] end subgraph "Compiler Compilation" COMPILER --> EXE["std.Build.CompileStep<br/>(zig executable)"] EXE --> COMPILER_MOD["addCompilerMod()"] EXE --> BUILD_OPTIONS["build_options<br/>(generated config)"] EXE --> LLVM_INTEGRATION["LLVM/Clang/LLD<br/>linking"] end subgraph "Test Steps" 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/>compiler unit tests"] TEST_SETUP --> TEST_STANDALONE["test-standalone"] TEST_SETUP --> TEST_CLI["test-cli"] end subgraph "Documentation" DOCS --> LANGREF_GEN["generateLangRef()<br/>(tools/docgen.zig)"] DOCS --> STD_DOCS["autodoc_test<br/>(lib/std/std.zig)"] end subgraph "Installation" EXE --> INSTALL_BIN["stage3/bin/zig"] INSTALL_LIB_DIR["lib/ directory"] --> 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 "Output Mode + Link Mode = Artifact Type" Exe_static["output_mode: Exe<br/>link_mode: static"] --> ExeStatic["Static executable"] Exe_dynamic["output_mode: Exe<br/>link_mode: dynamic"] --> ExeDynamic["Dynamic executable"] Lib_static["output_mode: Lib<br/>link_mode: static"] --> LibStatic["Static library (.a)"] Lib_dynamic["output_mode: Lib<br/>link_mode: dynamic"] --> LibDynamic["Shared library (.so/.dll)"] Obj["output_mode: Obj<br/>link_mode: N/A"] --> ObjFile["Object file (.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结构体的人体工程学。12, json.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.