Chapter 01Boot Basics

启动与基础

概述

Zig将每个源文件都视作一个带命名空间的模块,其编译模型围绕着使用@import显式地将这些单元连接在一起,从而使得依赖关系和程序边界一目了然,具体如#编译模型中所述。本章通过展示根模块、stdbuiltin如何协作,从单个文件生成一个可运行程序,同时保留对目标和优化模式的显式控制,从而构建了这段旅程的第一英里。

我们还为数据和执行建立了基本规则:constvar如何指导可变性,为什么像void {}这样的字面量对API设计至关重要,Zig如何处理默认溢出,以及如何为任务选择正确的打印界面,具体如#值中所述。在此过程中,我们预览了你将在后续章节中依赖的发布模式变体和缓冲输出助手;参见#构建模式

学习目标

  • 解释Zig如何通过@import解析模块以及根命名空间的角色。
  • 描述std.start如何发现main,以及为什么入口点通常返回!void,具体如#入口点中所述。
  • 使用constvar和像void {}这样的字面量形式来表达关于可变性和单元值的意图。
  • 根据输出通道和性能需求,在std.debug.print、无缓冲写入器和缓冲stdout之间进行选择。

从单个源文件开始

在Zig中,最快在屏幕上显示内容的方法是依赖默认的模块图:你编译的根文件成为规范的命名空间,而@import让你能够从标准库到编译器元数据的一切。你将不断使用这些钩子来使运行时行为与构建时决策保持一致。

入口点选择

Zig编译器根据目标平台、链接模式和用户声明导出不同的入口点符号。这个选择发生在编译时,位于lib/std/start.zig:28-104

入口点符号表

平台链接模式条件导出的符号处理函数
POSIX/Linux可执行文件默认_start_start()
POSIX/Linux可执行文件链接libcmainmain()
Windows可执行文件默认wWinMainCRTStartupWinStartup() / wWinMainCRTStartup()
Windows动态链接库默认_DllMainCRTStartup_DllMainCRTStartup()
UEFI可执行文件默认EfiMainEfiMain()
WASI可执行文件 (command)默认_startwasi_start()
WASI可执行文件 (reactor)默认_initializewasi_start()
WebAssembly独立式默认_startwasm_freestanding_start()
WebAssembly链接libc默认__main_argc_argvmainWithoutEnv()
OpenCL/Vulkan内核默认mainspirvMain2()
MIPS任何默认__start(同_start)

编译时入口点逻辑

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

模块和导入

根模块就是你的顶级文件,因此任何你标记为pub的声明都可以立即通过@import("root")重新导入。将它与@import("builtin")配对,以检查当前编译器调用选择的目标,具体如#内置函数中所述。

Zig
// File: chapters-data/code/01__boot-basics/imports.zig

// Import the standard library for I/O, memory management, and core utilities
// 导入标准库用于I/O、内存管理和核心工具
const std = @import("std");
// Import builtin to access compile-time information about the build environment
// 导入builtin以访问有关构建环境的编译时信息
const builtin = @import("builtin");
// Import root to access declarations from the root source file
// 导入root以访问根源文件中的声明
// In this case, we reference app_name which is defined in this file
// 在这种情况下,我们引用在此文件中定义的app_name
const root = @import("root");

// Public constant that can be accessed by other modules importing this file
// 其他模块导入此文件时可以访问的公共常量
pub const app_name = "Boot Basics Tour";

// Main entry point of the program
// 程序的主入口点
// Returns an error union to propagate any I/O errors during execution
// 返回错误联合类型以在执行期间传播任何I/O错误
pub fn main() !void {
    // Allocate a fixed-size buffer on the stack for stdout operations
    // 在栈上为stdout操作分配固定大小的缓冲区
    // This buffer batches write operations to reduce syscalls
    // 该缓冲区批量处理写入操作以减少系统调用
    var stdout_buffer: [256]u8 = undefined;
    // Create a buffered writer wrapping stdout
    // 创建包装stdout的缓冲写入器
    var stdout_writer = std.fs.File.stdout().writer(&stdout_buffer);
    // Get the generic writer interface for polymorphic I/O operations
    // 获取用于多态I/O操作的通用写入器接口
    const stdout = &stdout_writer.interface;

    // Print the application name by referencing the root module's declaration
    // 通过引用根模块的声明打印应用程序名称
    // Demonstrates how @import("root") allows access to the entry file's public declarations
    // 演示@import("root")如何允许访问入口文件的公共声明
    try stdout.print("app: {s}\n", .{root.app_name});

    // Print the optimization mode (Debug, ReleaseSafe, ReleaseFast, or ReleaseSmall)
    // 打印优化模式(Debug、ReleaseSafe、ReleaseFast或ReleaseSmall)
    // @tagName converts the enum value to its string representation
    // @tagName将枚举值转换为其字符串表示
    try stdout.print("optimize mode: {s}\n", .{@tagName(builtin.mode)});

    // Print the target triple showing CPU architecture, OS, and ABI
    // 打印显示CPU架构、操作系统和ABI的目标三元组
    // Each component is extracted from builtin.target and converted to a string
    // 每个组件从builtin.target中提取并转换为字符串
    try stdout.print(
        "target: {s}-{s}-{s}\n",
        .{
            @tagName(builtin.target.cpu.arch),
            @tagName(builtin.target.os.tag),
            @tagName(builtin.target.abi),
        },
    );

    // Flush the buffer to ensure all accumulated output is written to stdout
    // 刷新缓冲区以确保所有累积的输出都写入stdout
    try stdout.flush();
}
运行
Shell
$ zig run imports.zig
输出
Shell
app: Boot Basics Tour
optimize mode: Debug
target: x86_64-linux-gnu

实际的目标标识符取决于你的主机三元组;重要的是看到@tagName如何暴露每个枚举,以便你以后可以对它们进行分支。

因为缓冲的stdout写入器会批量处理数据,所以在退出前一定要调用flush(),这样终端才能接收到最后一行。

使用@import("root")来暴露配置常量,而不在你的命名空间中添加额外的全局变量。

入口点和早期错误

Zig的运行时粘合代码(std.start)会寻找一个pub fn main,转发命令行状态,并将错误返回视为带有诊断信息的中止信号。因为main通常执行I/O操作,所以给它!void返回类型可以使错误传播保持显式。

Zig
// File: chapters-data/code/01__boot-basics/entry_point.zig

// Import the standard library for I/O and utility functions
// 导入标准库用于I/O和工具函数
const std = @import("std");
// Import builtin to access compile-time information like build mode
// 导入builtin以访问像构建模式这样的编译时信息
const builtin = @import("builtin");

// Define a custom error type for build mode violations
// 定义用于构建模式违规的自定义错误类型
const ModeError = error{ReleaseOnly};

// Main entry point of the program
// 程序的主入口点
// Returns an error union to propagate any errors that occur during execution
// 返回错误联合类型以传播执行期间发生的任何错误
pub fn main() !void {
    // Attempt to enforce debug mode requirement
    // 尝试强制执行调试模式要求
    // If it fails, catch the error and print a warning instead of terminating
    // 如果失败,捕获错误并打印警告而不是终止
    requireDebugSafety() catch |err| {
        std.debug.print("warning: {s}\n", .{@errorName(err)});
    };

    // Print startup message to stdout
    // 打印启动消息到stdout
    try announceStartup();
}

// Validates that the program is running in Debug mode
// 验证程序是否在调试模式下运行
// Returns an error if compiled in Release mode to demonstrate error handling
// 如果在发布模式下编译则返回错误以演示错误处理
fn requireDebugSafety() ModeError!void {
    // Check compile-time build mode
    // 检查编译时构建模式
    if (builtin.mode == .Debug) return;
    // Return error if not in Debug mode
    // 如果不在调试模式则返回错误
    return ModeError.ReleaseOnly;
}

// Writes a startup announcement message to standard output
// 向标准输出写入启动公告消息
// Demonstrates buffered I/O operations in Zig
// 演示Zig中的缓冲I/O操作
fn announceStartup() !void {
    // Allocate a fixed-size buffer on the stack for stdout operations
    // 在栈上为stdout操作分配固定大小的缓冲区
    var stdout_buffer: [128]u8 = undefined;
    // Create a buffered writer wrapping stdout
    // 创建包装stdout的缓冲写入器
    var stdout_writer = std.fs.File.stdout().writer(&stdout_buffer);
    // Get the generic writer interface for polymorphic I/O
    // 获取用于多态I/O的通用写入器接口
    const stdout = &stdout_writer.interface;
    // Write formatted message to the buffer
    // 将格式化消息写入缓冲区
    try stdout.print("Zig entry point reporting in.\n", .{});
    // Flush the buffer to ensure message is written to stdout
    // 刷新缓冲区以确保消息写入stdout
    try stdout.flush();
}
运行
Shell
$ zig run entry_point.zig
输出
Shell
Zig entry point reporting in.

在发布模式下(zig run -OReleaseFast …​),ModeError.ReleaseOnly分支会被触发,警告会在程序继续之前出现,这很好地演示了catch如何将错误转换成面向用户的诊断信息,而不会抑制后续的工作。

如何处理的返回类型

Zig在std.start中的启动代码会在编译时检查你的main()函数的返回类型,并生成适当的处理逻辑。这种灵活性允许你选择最适合你程序需求的签名——无论你是想要简单的成功/失败语义的!void,显式退出码的u8,还是无限事件循环的noreturncallMain()函数协调这个分派,确保错误被记录下来,退出码正确地传播到操作系统。

callMain返回类型处理

callMain()函数处理来自用户main()的不同返回类型签名:

graph TB Start["callMain()"] GetRetType["ReturnType = @TypeOf(root.main)<br/>.return_type"] CheckType["switch ReturnType"] Void["void"] CallVoid["root.main()<br/>return 0"] NoReturn["noreturn"] CallNoReturn["return root.main()"] U8["u8"] CallU8["return root.main()"] ErrorUnion["error union"] CheckInner["@TypeOf(result)?"] InnerVoid["void"] ReturnZero["return 0"] InnerU8["u8"] ReturnResult["return result"] Invalid["@compileError"] CallCatch["result = root.main()<br/>catch |err|"] LogError["Log error name<br/>and stack trace<br/>(lines 707-712)"] ReturnOne["return 1"] Start --> GetRetType GetRetType --> CheckType CheckType --> Void CheckType --> NoReturn CheckType --> U8 CheckType --> ErrorUnion CheckType --> Invalid Void --> CallVoid NoReturn --> CallNoReturn U8 --> CallU8 ErrorUnion --> CallCatch CallCatch --> CheckInner CallCatch --> LogError LogError --> ReturnOne CheckInner --> InnerVoid CheckInner --> InnerU8 CheckInner --> Invalid InnerVoid --> ReturnZero InnerU8 --> ReturnResult

来自main()的有效返回类型:

  • void - 返回退出码0
  • noreturn - 永不返回(无限循环或显式退出)
  • u8 - 直接返回退出码
  • !void - 成功时返回0,错误时返回1(记录错误及堆栈跟踪)
  • !u8 - 成功时返回退出码,错误时返回1(记录错误及堆栈跟踪)

我们示例中使用的!void签名提供了最佳的平衡:显式错误处理,自动记录日志以及适当的退出码。

命名和作用域预览

变量遵循词法作用域:每个代码块都引入一个新的区域,你可以在其中遮蔽或扩展绑定,而constvar则分别表示不可变和可变,并帮助编译器推理安全性,具体如#代码块中所述。Zig将关于样式和遮蔽的更深入讨论推迟到第38章,但请记住,在顶层进行深思熟虑的命名(通常通过pub const)是在文件之间共享配置的惯用方式;参见#变量

使用值和构建

一旦你有了入口点,下一站就是数据:数值类型有明确大小的种类(iNuNfN),字面量从上下文中推断其类型,除非你选择包装或饱和运算符,否则Zig使用调试安全检查来捕获溢出。构建模式(-O标志)决定了哪些检查保留以及编译器优化的积极程度。

优化模式

Zig提供了四种优化模式,它们控制着代码速度、二进制文件大小和安全检查之间的权衡:

模式优先级安全检查速度二进制大小用例
Debug安全+调试信息✓ 全部启用最慢最大开发和调试
ReleaseSafe速度+安全✓ 全部启用带安全的生产环境
ReleaseFast最高速度✗ 禁用最快中等性能关键的生产环境
ReleaseSmall最小尺寸✗ 禁用最小嵌入式系统,尺寸受限

优化模式通过-O标志指定,并影响:

  • 运行时安全检查(溢出、边界检查、空指针检查)
  • 堆栈跟踪和调试信息生成
  • LLVM优化级别(当使用LLVM后端时)
  • 内联启发式和代码生成策略
graph TB subgraph "优化模式效果" OptMode["optimize_mode: OptimizeMode"] OptMode --> SafetyChecks["运行时安全检查"] OptMode --> DebugInfo["调试信息"] OptMode --> CodegenStrategy["代码生成策略"] OptMode --> LLVMOpt["LLVM优化级别"] SafetyChecks --> Overflow["整数溢出检查"] SafetyChecks --> Bounds["边界检查"] SafetyChecks --> Null["空指针检查"] SafetyChecks --> Unreachable["不可达断言"] DebugInfo --> StackTraces["堆栈跟踪"] DebugInfo --> DWARF["DWARF调试信息"] DebugInfo --> LineInfo["源代码行信息"] CodegenStrategy --> Inlining["内联启发式"] CodegenStrategy --> Unrolling["循环展开"] CodegenStrategy --> Vectorization["SIMD向量化"] LLVMOpt --> O0["Debug: -O0"] LLVMOpt --> O2Safe["ReleaseSafe: -O2 + safety"] LLVMOpt --> O3["ReleaseFast: -O3"] LLVMOpt --> Oz["ReleaseSmall: -Oz"] end

在本章中,我们使用Debug(默认)进行开发,并预览ReleaseFast来演示优化选择如何影响行为和二进制特性。

值、字面量和调试打印

std.debug.print写入到stderr,非常适合早期实验;它接受你抛出的任何值,揭示了@TypeOf和朋友们如何对字面量进行反射。

Zig
// File: chapters-data/code/01__boot-basics/values_and_literals.zig
const std = @import("std");

pub fn main() !void {
    // Declare a mutable variable with explicit type annotation
    // 声明带显式类型注解的可变变量
    // u32 is an unsigned 32-bit integer, initialized to 1
    // u32是一个无符号32位整数,初始化为1
    var counter: u32 = 1;

    // Declare an immutable constant with inferred type (comptime_int)
    // 声明具有推断类型的不可变常量(comptime_int)
    // The compiler infers the type from the literal value 2
    // 编译器从字面量值2推断类型
    const increment = 2;

    // Declare a constant with explicit floating-point type
    // 声明具有显式浮点类型的常量
    // f64 is a 64-bit floating-point number
    // f64是64位浮点数
    const ratio: f64 = 0.5;

    // Boolean constant with inferred type
    // 具有推断类型的布尔常量
    // Demonstrates Zig's type inference for simple literals
    // 演示Zig对简单字面量的类型推断
    const flag = true;

    // Character literal representing a newline
    // 表示换行的字符字面量
    // Single-byte characters are u8 values in Zig
    // 单字节字符在Zig中是u8值
    const newline: u8 = '\n';

    // The unit type value, analogous to () in other languages
    // 单位类型值,类似于其他语言中的()
    // Represents "no value" or "nothing" explicitly
    // 明确表示"无值"或"空"
    const unit_value = void{};

    // Mutate the counter by adding the increment
    // 通过增加增量来改变计数器
    // Only var declarations can be modified
    // 只有var声明可以被修改
    counter += increment;

    // Print formatted output showing different value types
    // 打印显示不同值类型的格式化输出
    // {} is a generic format specifier that works with any type
    // {}是适用于任何类型的通用格式说明符
    std.debug.print("counter={} ratio={} safety={}\n", .{ counter, ratio, flag });

    // Cast the newline byte to u32 for display as its ASCII decimal value
    // 将换行字节强制转换为u32以显示其ASCII十进制值
    // @as performs explicit type coercion
    // @as执行显式类型强制转换
    std.debug.print("newline byte={} (ASCII)\n", .{@as(u32, newline)});

    // Use compile-time reflection to print the type name of unit_value
    // 使用编译时反射打印unit_value的类型名称
    // @TypeOf gets the type, @typeName converts it to a string
    // @TypeOf获取类型,@typeName将其转换为字符串
    std.debug.print("unit literal has type {s}\n", .{@typeName(@TypeOf(unit_value))});
}
运行
Shell
$ zig run values_and_literals.zig
输出
Shell
counter=3 ratio=0.5 safety=true
newline byte=10 (ASCII)
unit literal has type void

void {}视为一个传达“无需配置”的字面量,并记住调试打印默认为stderr,因此它们永远不会干扰stdout管道。

缓冲stdout和构建模式

当你想要确定性的stdout输出且系统调用更少时,借用一个缓冲区并刷新一次——尤其是在吞吐量很重要的发布配置中。下面的例子展示了如何围绕std.fs.File.stdout()设置一个缓冲写入器,并突出了不同构建模式之间的差异。

Zig
// File: chapters-data/code/01__boot-basics/buffered_stdout.zig
const std = @import("std");

pub fn main() !void {
    // Allocate a 256-byte buffer on the stack for output batching
    // 在栈上分配256字节的缓冲区用于输出批处理
    // This buffer accumulates write operations to minimize syscalls
    // 该缓冲区累积写入操作以最小化系统调用
    var stdout_buffer: [256]u8 = undefined;

    // Create a buffered writer wrapping stdout
    // 创建包装stdout的缓冲写入器
    // The writer batches output into stdout_buffer before making syscalls
    // 写入器在执行系统调用前将输出批处理到stdout_buffer
    var writer_state = std.fs.File.stdout().writer(&stdout_buffer);
    const stdout = &writer_state.interface;

    // These print calls write to the buffer, not directly to the terminal
    // 这些打印调用写入缓冲区,而不是直接写入终端
    // No syscalls occur yet—data accumulates in stdout_buffer
    // 尚未发生系统调用—数据在stdout_buffer中累积
    try stdout.print("Buffering saves syscalls.\n", .{});
    try stdout.print("Flush once at the end.\n", .{});

    // Explicitly flush the buffer to write all accumulated data at once
    // 显式刷新缓冲区以一次性写入所有累积的数据
    // This triggers a single syscall instead of one per print operation
    // 这将触发单个系统调用,而不是每次打印操作一次
    try stdout.flush();
}
运行
Shell
$ zig build-exe buffered_stdout.zig -OReleaseFast
$
$ ./buffered_stdout
输出
Shell
Buffering saves syscalls.
Flush once at the end.

使用缓冲写入器反映了标准库自己的初始化模板,并保持写入的内聚性;在退出前总是刷新,以保证操作系统看到你的最终消息。

注意和警告

  • std.debug.print目标是stderr并绕过stdout缓冲,所以即使在简单的工具中也应保留它用于诊断。
  • 当你故意想跳过溢出陷阱时,可以使用包装(%`)和饱和(`|)算术;默认运算符在调试模式下仍然会因捕捉早期错误而恐慌,如#运算符中所述。
  • std.fs.File.stdout().writer(&buffer)反映了zig init使用的模式,并且需要显式的flush()来向下游推送缓冲的字节。

练习

  • 扩展imports.zig以打印由@sizeOf(usize)报告的指针大小,并通过在命令行上切换-Dtarget值来比较目标。
  • 重构entry_point.zig,使requireDebugSafety返回一个描述性的错误联合(error{ReleaseOnly}![]const u8),并让main在重新抛出错误之前将消息写入stdout。
  • 使用-OReleaseSafe-OReleaseSmall构建buffered_stdout.zig,测量二进制文件大小,看看优化选择如何影响部署占用空间。

Help make this chapter better.

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