Chapter 37Illegal Behavior And Safety Modes

非法行为与安全模式

概述

完成风格调整后,我们清楚地认识到,除非不变量在失败时大声报错,否则它们毫无价值(36)。本章解释 Zig 如何将这些失败正式化为非法行为,以及工具链如何在它们破坏状态之前捕获大多数问题。#illegal behavior

接下来我们将深入研究命令行工具,因此我们希望在脚本代表我们切换优化模式之前,先建立运行时防护栏。38

学习目标

  • 区分非法行为的安全检查和非检查类别。
  • 检查活动优化模式并推理 Zig 将发出哪些运行时检查。
  • 围绕 @setRuntimeSafetyunreachablestd.debug.assert 构建契约,以在每次构建中保持不变量可证明。

参考: 4

Zig 中的非法行为

非法行为是 Zig 的总称,指语言拒绝定义的操作,从整数溢出到解引用无效指针。我们已经依赖对切片和可选类型的边界检查;本节整合这些规则,以便即将到来的 CLI 工作继承可预测的失败故事。3

安全检查路径 vs 非检查路径

安全检查的非法行为涵盖编译器可以在运行时检测的情况(溢出、哨兵不匹配、错误联合字段访问),而非检查情况对安全检测保持不可见(通过错误指针类型进行别名、来自外部代码的布局违规)。

Debug 和 ReleaseSafe 默认保持防护开启。ReleaseFast 和 ReleaseSmall 假设您用这些陷阱换取了性能,因此任何通过不变量溜过去的东西在实践中变得未定义。

示例:守护非检查算术

以下辅助函数使用 @addWithOverflow 证明加法安全,然后禁用最终 + 的运行时安全以避免冗余检查,同时将病态输入饱和到类型的最大值。#setruntimesafety

Zig
const std = @import("std");

/// Performs addition with overflow detection and saturation.
/// If overflow occurs, returns the maximum u8 value instead of wrapping.
/// Uses @setRuntimeSafety(false) in the non-overflow path for performance.
fn guardedUncheckedAdd(a: u8, b: u8) u8 {
    // Check if addition would overflow using builtin overflow detection
    const sum = @addWithOverflow(a, b);
    const overflow = sum[1] == 1;
    // Saturate to max value on overflow
    if (overflow) return std.math.maxInt(u8);

    // Safe path: disable runtime safety checks for this addition
    // since we've already verified no overflow will occur
    return blk: {
        @setRuntimeSafety(false);
        break :blk a + b;
    };
}

/// Performs addition without runtime safety checks.
/// This allows the operation to wrap on overflow (undefined behavior in safe mode).
/// Demonstrates completely disabling safety for a function scope.
fn wrappingAddUnsafe(a: u8, b: u8) u8 {
    // Disable all runtime safety checks for this entire function
    @setRuntimeSafety(false);
    return a + b;
}

// Verifies that guardedUncheckedAdd correctly handles both normal addition
// and overflow saturation scenarios.
test "guarded unchecked addition saturates on overflow" {
    // Normal case: 120 + 80 = 200 (no overflow)
    try std.testing.expectEqual(@as(u8, 200), guardedUncheckedAdd(120, 80));
    // Overflow case: 240 + 30 = 270 > 255, should saturate to 255
    try std.testing.expectEqual(std.math.maxInt(u8), guardedUncheckedAdd(240, 30));
}

// Demonstrates that wrappingAddUnsafe produces the same wrapped result
// as @addWithOverflow when overflow occurs.
test "wrapping addition mirrors overflow tuple" {
    // @addWithOverflow returns [wrapped_result, overflow_bit]
    const sum = @addWithOverflow(@as(u8, 250), @as(u8, 10));
    // Verify overflow occurred (250 + 10 = 260 > 255)
    try std.testing.expect(sum[1] == 1);
    // Verify wrapped result matches unchecked addition (260 % 256 = 4)
    try std.testing.expectEqual(sum[0], wrappingAddUnsafe(250, 10));
}
运行
Shell
$ zig test 01_guarded_runtime_safety.zig
输出
Shell
All 2 tests passed.

使用 -OReleaseFast 运行相同测试可验证,即使在全局运行时安全缺失时,防护仍继续饱和而不是恐慌。

按优化模式的安全默认值

当前优化模式通过 @import("builtin").mode 公开,这使得在不查阅构建脚本的情况下,轻松了解给定产物中存在哪些运行时检查。#compile variables 下表总结了在您开始手动选择加入或退出检查之前,每个模式提供的默认契约。

模式运行时安全典型意图
Debug启用具有最大诊断和堆栈跟踪的开发构建。
ReleaseSafe启用仍然更喜欢可预测陷阱而不是静默损坏的生产构建。
ReleaseFast禁用假设不变量已在其他地方证明的高性能二进制文件。
ReleaseSmall禁用大小受限的交付物,每个注入的陷阱都是负担。

在运行时检测安全

此探针打印活动模式和隐含的安全默认值,然后比较检查的加法和未检查的加法,以便您可以看到在检查消失时什么会存活。

Zig
const std = @import("std");
const builtin = @import("builtin");

// Extract the compile-time type of the optimization mode enum
const ModeType = @TypeOf(builtin.mode);

/// Captures both the active optimization mode and its default safety behavior
const ModeInfo = struct {
    mode: ModeType,
    safety_default: bool,
};

/// Determines whether runtime safety checks are enabled by default for a given mode.
/// Debug and ReleaseSafe modes retain safety checks; ReleaseFast and ReleaseSmall disable them.
fn defaultSafety(mode: ModeType) bool {
    return switch (mode) {
        // These modes prioritize correctness with runtime checks
        .Debug, .ReleaseSafe => true,
        // These modes prioritize performance/size by removing checks
        .ReleaseFast, .ReleaseSmall => false,
    };
}

/// Performs checked addition that detects overflow without panicking.
/// Returns both the wrapped result and an overflow flag.
fn sampleAdd(a: u8, b: u8) struct { result: u8, overflowed: bool } {
    // @addWithOverflow returns a tuple: [wrapped_result, overflow_bit]
    const pair = @addWithOverflow(a, b);
    return .{ .result = pair[0], .overflowed = pair[1] == 1 };
}

/// Performs unchecked addition by explicitly disabling runtime safety.
/// In Debug/ReleaseSafe, this avoids the panic on overflow.
/// In ReleaseFast/ReleaseSmall, the safety was already off, so this is redundant but harmless.
fn uncheckedAddStable(a: u8, b: u8) u8 {
    return blk: {
        // Temporarily disable runtime safety for this block only
        @setRuntimeSafety(false);
        // Raw addition without overflow checks; wraps silently on overflow
        break :blk a + b;
    };
}

pub fn main() void {
    // Capture the current build mode and its implied safety setting
    const info = ModeInfo{
        .mode = builtin.mode,
        .safety_default = defaultSafety(builtin.mode),
    };

    // Report which optimization mode the binary was compiled with
    std.debug.print("optimize-mode: {s}\n", .{@tagName(info.mode)});
    // Show whether runtime safety is on by default in this mode
    std.debug.print("runtime-safety-default: {}\n", .{info.safety_default});

    // Demonstrate checked addition that reports overflow without crashing
    const checked = sampleAdd(200, 80);
    std.debug.print("checked-add result={d} overflowed={}\n", .{ checked.result, checked.overflowed });

    // Demonstrate unchecked addition that wraps silently (24 = (200+80) % 256)
    const unchecked = uncheckedAddStable(200, 80);
    std.debug.print("unchecked-add result={d}\n", .{unchecked});
}
运行
Shell
$ zig run 02_mode_probe.zig
输出
Shell
optimize-mode: Debug
runtime-safety-default: true
checked-add result=24 overflowed=true
unchecked-add result=24

使用 -OReleaseFast 重新运行探针,以观察默认安全标志翻转为 false,而检查路径仍报告溢出,帮助您记录在发布构建中可能需要的功能标志或遥测。

契约、恐慌和恢复

当您在启用安全的构建中触发 unreachable 时,堆栈跟踪令人冷静地恐惧。在断言和错误联合已经耗尽优雅退出之后,将它们视为最后防线。#reaching unreachable code

将这种纪律与早期章节的错误处理技术配对,可以保持失败模式可调试而不牺牲确定性。4

示例:断言数字转换

这里我们以两种方式记录 ASCII 数字契约:一次使用解锁非检查数学的断言,一次使用对调用者友好的验证的错误联合。debug.zig

Zig

// This file demonstrates different safety modes in Zig and how to handle
// conversions with varying levels of runtime checking.

const std = @import("std");

/// Converts an ASCII digit character to its numeric value without runtime safety checks.
/// This function uses an assert to document the precondition that the input must be
/// a valid ASCII digit ('0'-'9'). The @setRuntimeSafety(false) directive disables
/// runtime integer overflow checks for the subtraction and cast operations.
/// 
/// Precondition: byte must be in the range ['0', '9']
/// Returns: The numeric value (0-9) as a u4
pub fn asciiDigitToValueUnchecked(byte: u8) u4 {
    // Assert documents the contract: caller must provide a valid ASCII digit
    std.debug.assert(byte >= '0' and byte <= '9');
    
    // Block with runtime safety disabled for performance-critical paths
    return blk: {
        // Disable runtime overflow/underflow checks for this conversion
        @setRuntimeSafety(false);
        // Safe cast because precondition guarantees result fits in u4 (0-9)
        break :blk @intCast(byte - '0');
    };
}

/// Converts an ASCII digit character to its numeric value with error handling.
/// This function validates the input at runtime and returns an error if the
/// byte is not a valid ASCII digit, making it safe to use with untrusted input.
/// 
/// Returns: The numeric value (0-9) as a u4, or error.InvalidDigit if invalid
pub fn asciiDigitToValue(byte: u8) !u4 {
    // Validate input is within valid ASCII digit range
    if (byte < '0' or byte > '9') return error.InvalidDigit;
    // Safe cast: validation ensures result is in range 0-9
    return @intCast(byte - '0');
}

// Verifies that the unchecked conversion produces correct results for all valid inputs.
// Tests all ASCII digits to ensure the assert-backed function maintains correctness
// even when runtime safety is disabled internally.
test "assert-backed conversion stays safe across modes" {
    // Iterate over all valid ASCII digit characters at compile time
    inline for ("0123456789") |ch| {
        // Verify unchecked function produces same result as direct conversion
        try std.testing.expectEqual(@as(u4, @intCast(ch - '0')), asciiDigitToValueUnchecked(ch));
    }
}

// Verifies that the error-returning conversion properly rejects invalid input.
// Ensures that error handling path works correctly and provides meaningful diagnostics.
test "error path preserves diagnosability" {
    // Verify that non-digit characters return the expected error
    try std.testing.expectError(error.InvalidDigit, asciiDigitToValue('z'));
}
运行
Shell
$ zig test 03_unreachable_contract.zig
输出
Shell
All 2 tests passed.

断言支持的路径在 ReleaseFast 中编译为单个减法,但如果您传递非数字,它在 Debug 中仍会恐慌。为不受信任的数据保留一个防御性的错误返回变体。

注意事项与警告

  • 即使在 Debug 模式下,一些基于指针的错误仍保持未检查。当您需要边界强制时,优先使用基于切片的 API。
  • @setRuntimeSafety(false) 的范围缩小到尽可能小的块,并在切换之前证明前提条件。
  • 在开发中捕获恐慌堆栈跟踪,如果您希望稍后分类 ReleaseSafe 崩溃,请交付符号文件。

练习

  • 扩展 guardedUncheckedAdd 以在哨兵终止的切片溢出目标缓冲区时发出诊断,然后测量安全开启和安全关闭构建之间的差异。#sentinel terminated arrays
  • 编写一个基准测试工具,循环执行数百万次安全加法,每次迭代切换 @setRuntimeSafety,以确认每个模式下防护的成本。
  • 增强模式探针以在即将到来的 CLI 项目中记录构建元数据,以便脚本可以在 ReleaseFast 二进制文件省略陷阱时发出警告。38

替代方案与边缘情况

  • 在 ReleaseFast 中未能从 + 切换到 @addWithOverflow,会冒着静默回绕的风险,这种回绕只在罕见的负载模式下表现出来。
  • 运行时安全不能防御并发数据竞争。将这些工具与本书后面介绍的同步原语配对。
  • 调用 C 代码时,请记住 Zig 的检查在 FFI 边界停止。在信任不变量之前验证外部输入。33

Help make this chapter better.

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