概述
完成风格调整后,我们清楚地认识到,除非不变量在失败时大声报错,否则它们毫无价值(36)。本章解释 Zig 如何将这些失败正式化为非法行为,以及工具链如何在它们破坏状态之前捕获大多数问题。#illegal behavior
接下来我们将深入研究命令行工具,因此我们希望在脚本代表我们切换优化模式之前,先建立运行时防护栏。38
学习目标
- 区分非法行为的安全检查和非检查类别。
- 检查活动优化模式并推理 Zig 将发出哪些运行时检查。
- 围绕
@setRuntimeSafety、unreachable和std.debug.assert构建契约,以在每次构建中保持不变量可证明。
参考: 4
Zig 中的非法行为
非法行为是 Zig 的总称,指语言拒绝定义的操作,从整数溢出到解引用无效指针。我们已经依赖对切片和可选类型的边界检查;本节整合这些规则,以便即将到来的 CLI 工作继承可预测的失败故事。3
安全检查路径 vs 非检查路径
安全检查的非法行为涵盖编译器可以在运行时检测的情况(溢出、哨兵不匹配、错误联合字段访问),而非检查情况对安全检测保持不可见(通过错误指针类型进行别名、来自外部代码的布局违规)。
Debug 和 ReleaseSafe 默认保持防护开启。ReleaseFast 和 ReleaseSmall 假设您用这些陷阱换取了性能,因此任何通过不变量溜过去的东西在实践中变得未定义。
示例:守护非检查算术
以下辅助函数使用 @addWithOverflow 证明加法安全,然后禁用最终 + 的运行时安全以避免冗余检查,同时将病态输入饱和到类型的最大值。#setruntimesafety
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));
}
$ zig test 01_guarded_runtime_safety.zigAll 2 tests passed.使用 -OReleaseFast 运行相同测试可验证,即使在全局运行时安全缺失时,防护仍继续饱和而不是恐慌。
按优化模式的安全默认值
当前优化模式通过 @import("builtin").mode 公开,这使得在不查阅构建脚本的情况下,轻松了解给定产物中存在哪些运行时检查。#compile variables 下表总结了在您开始手动选择加入或退出检查之前,每个模式提供的默认契约。
| 模式 | 运行时安全 | 典型意图 |
|---|---|---|
| Debug | 启用 | 具有最大诊断和堆栈跟踪的开发构建。 |
| ReleaseSafe | 启用 | 仍然更喜欢可预测陷阱而不是静默损坏的生产构建。 |
| ReleaseFast | 禁用 | 假设不变量已在其他地方证明的高性能二进制文件。 |
| ReleaseSmall | 禁用 | 大小受限的交付物,每个注入的陷阱都是负担。 |
在运行时检测安全
此探针打印活动模式和隐含的安全默认值,然后比较检查的加法和未检查的加法,以便您可以看到在检查消失时什么会存活。
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});
}
$ zig run 02_mode_probe.zigoptimize-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
// 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'));
}
$ zig test 03_unreachable_contract.zigAll 2 tests passed.断言支持的路径在 ReleaseFast 中编译为单个减法,但如果您传递非数字,它在 Debug 中仍会恐慌。为不受信任的数据保留一个防御性的错误返回变体。
注意事项与警告
- 即使在 Debug 模式下,一些基于指针的错误仍保持未检查。当您需要边界强制时,优先使用基于切片的 API。
- 将
@setRuntimeSafety(false)的范围缩小到尽可能小的块,并在切换之前证明前提条件。 - 在开发中捕获恐慌堆栈跟踪,如果您希望稍后分类 ReleaseSafe 崩溃,请交付符号文件。
练习
- 扩展
guardedUncheckedAdd以在哨兵终止的切片溢出目标缓冲区时发出诊断,然后测量安全开启和安全关闭构建之间的差异。#sentinel terminated arrays - 编写一个基准测试工具,循环执行数百万次安全加法,每次迭代切换
@setRuntimeSafety,以确认每个模式下防护的成本。 - 增强模式探针以在即将到来的 CLI 项目中记录构建元数据,以便脚本可以在 ReleaseFast 二进制文件省略陷阱时发出警告。38
替代方案与边缘情况
- 在 ReleaseFast 中未能从
+切换到@addWithOverflow,会冒着静默回绕的风险,这种回绕只在罕见的负载模式下表现出来。 - 运行时安全不能防御并发数据竞争。将这些工具与本书后面介绍的同步原语配对。
- 调用 C 代码时,请记住 Zig 的检查在 FFI 边界停止。在信任不变量之前验证外部输入。33