深入理解 Rust 编译器 E0133 错误:为什么 Unsafe 操作必须位于 `unsafe` 块中
本文围绕 Rust 编译器错误码 E0133("Unsafe code was used outside of an unsafe block")展开:先完整说明该错误的触发场景与标准修复方式,再结合 rustc 编译器源码(rustc_mir_build 的 unsafety 检查器与 rustc_lint_defs 的 lint 定义),剖析编译器是如何对每个 unsafe 操作做"安全性上下文"判定、为何 unsafe fn 体内的隐式权限正在被逐步废弃,以及 2024 edition 中 unsafe_op_in_unsafe_fn lint 的默认行为变化。读完本文,你将能够准确定位任何一条 E0133 诊断的来源,理解"requires unsafe function or block"与"requires unsafe block"两种措辞的差异,并按照当前编译器推荐的写法收敛代码中的 unsafe 区域。
一、E0133 是什么:错误示例与错误信息
E0133 是 Rust 编译器对"在 unsafe 块之外使用了 unsafe 代码"的硬性错误(error,而非 lint 警告)。编译器错误码文档 E0133 给出的原始错误示例是:
// compile_fail, E0133
unsafe fn f() { return; } // 这是一段 unsafe 代码
fn main() {
f(); // error: call to unsafe function requires unsafe function or block
}
调用了一个 unsafe fn,却既没有处于 unsafe fn 之内、也没有被 unsafe { } 块包裹,编译器就会拒绝编译并报 E0133。
文档同时指出:使用 unsafe 功能在语义上是危险的,因此被安全(safety)检查所禁止。典型的 unsafe 操作包括:
- 解引用裸指针(raw pointers);
- 通过 FFI 调用外部函数;
- 调用被标记为
unsafe的函数。
而修复手段只有一条:把这些 unsafe 指令用 unsafe 块包裹起来。对应的正确写法是:
unsafe fn f() { return; }
fn main() {
unsafe { f(); } // ok!
}
值得注意的是,当前编译器实际输出的错误措辞比文档示例中的措辞更精确。在 诊断定义文件 中,最基础的一条 E0133 诊断模板为:
call to unsafe function
{$function}is unsafe and requires unsafe block
即编译器会具体给出被调用的 unsafe 函数名。此外,同一文件中还定义了一组措辞变体,例如 L226 处的 call to unsafe function ... requires unsafe function or block。这两种措辞的差异背后,正是"当前 unsafe fn 体内是否还允许隐式执行 unsafe 操作"这一状态(后文第六节详述)。
二、哪些操作会触发 E0133:完整的操作类别清单
官方错误码文档只列举了三类示例,但从编译器源码可以完整枚举出所有会触发 E0133 的 unsafe 操作类别。check_unsafety.rs 中定义的 UnsafeOpKind 枚举与 diagnostics.rs 中逐一对应的 #[diag(..., code = E0133)] 结构体,构成了完整清单。按源码中出现顺序整理如下:
| Unsafe 操作类别 | 诊断措辞(编译错误正文) | 附带的 note 说明 |
|---|---|---|
| 调用 unsafe 函数 | call to unsafe function ... is unsafe and requires unsafe block | consult the function's documentation ... to avoid undefined behavior |
| 使用内联汇编 | use of inline assembly is unsafe and requires unsafe block | inline assembly is entirely unchecked and can cause undefined behavior |
| 初始化含 unsafe 字段的类型 | initializing type with an unsafe field is unsafe and requires unsafe block | unsafe fields may carry library invariants |
| 使用可变 static | use of mutable static is unsafe and requires unsafe block | mutable statics can be mutated by multiple threads ... |
| 使用 extern static | use of extern static is unsafe and requires unsafe block | extern statics are not controlled by the Rust type system ... |
| 访问 unsafe 字段 | use of unsafe field is unsafe and requires unsafe block | unsafe fields may carry library invariants |
| 解引用裸指针 | dereference of raw pointer is unsafe and requires unsafe block | raw pointers may be null, dangling or unaligned ... |
| 访问 union 字段 | access to union field is unsafe and requires unsafe block | the field may not be properly initialized ... |
| 修改布局受限字段(layout constrained field) | mutation of layout constrained field is unsafe and requires unsafe block | mutating layout constrained fields cannot statically be checked for valid values |
| 以内部可变性借用布局受限字段 | borrow of layout constrained field with interior mutability is unsafe and requires unsafe block | — |
| unsafe binder cast | unsafe binder cast is unsafe and requires unsafe block ... | — |
调用带 #[target_feature] 的函数 |
call to function ... with #[target_feature] is unsafe and requires unsafe block |
还会提示当前构建上下文缺少哪些 target feature |
这些诊断结构体(如 UnsafeOpInUnsafeFnDerefOfRawPointerRequiresUnsafe)还都携带一个可选的 unsafe_not_inherited_note 子诊断——当报错位置的外层恰好是一个 unsafe fn 时,编译器会附加一条 note 指向该 unsafe fn 的函数头,解释"unsafety 不会从外层 unsafe fn 继承进来"。这是帮助开发者理解错误的关键提示:即使在 unsafe fn 内部,只要 unsafe_op_in_unsafe_fn lint 不允许隐式 unsafe 操作,你仍然需要显式的 unsafe { } 块。
此外,编译器还为最常见的修复场景提供了机器建议:SuggUnsafeBlock 子诊断会通过 #[suggestion_part(code = "unsafe {{ ")] 与 #[suggestion_part(code = " }}")] 两个片段,建议把出错的语句包裹进 unsafe { ... },配合 --fix 等工具可自动完成大部分修复。
三、修复方式详解:unsafe 块的两种正确用法
3.1 在安全上下文中:显式 unsafe 块
这是 E0133 文档给出的标准修复。对于文档中列举的三类典型操作(裸指针解引用、FFI 调用、调用 unsafe 函数),统一做法是把具体执行 unsafe 操作的语句包进 unsafe 块:
unsafe fn f() { return; }
fn main() {
unsafe { f(); } // ok!
}
实践中的两个要点:
- 最小化 unsafe 区域。
unsafe块应只包裹真正执行 unsafe 操作的那几句语句,而不是整个函数体,这样后续审计和unused_unsafe检查(见第六节)都能精确定位问题; - 块内仍然要遵守 unsafe 契约。编译器只是"放行"了语法层面,并不替你验证指针是否悬空、static 是否数据竞争——每条 E0133 诊断附带的 note(例如 raw pointer 那条"may be null, dangling or unaligned")就是在提醒你需要人工保证的不变式。
3.2 在 unsafe 函数内部:推荐显式块,隐式权限被逐步淘汰
E0133 文档中"Unsafe code in functions"一节说明了当前编译器对 unsafe fn 体内代码的特殊待遇:
Unsafe code is currently accepted in unsafe functions, but that is being phased out in favor of requiring unsafe blocks here too.
也就是说,历史上 unsafe fn 的整个函数体都被视为一个隐式 unsafe 区域,其内部可以直接再调用其他 unsafe 函数;但这一语义正在被淘汰,未来将要求 unsafe fn 内部同样使用显式 unsafe 块。文档给出的对照示例:
unsafe fn f() { return; }
unsafe fn g() {
f(); // 当前被接受,但不再推荐
unsafe { f(); } // 推荐写法
}
控制这条"淘汰进度"的,是 unsafe_op_in_unsafe_fn lint。E0133 文档明确说明:该 lint 在 2024 edition 中默认 warn,在更早的 edition(2015/2018/2021)中默认 allow。
这一说法与 lint 的源码定义完全一致。builtin.rs 中的 declare_lint! 声明给出了三个关键事实:
- 默认级别为
Allow(pub UNSAFE_OP_IN_UNSAFE_FN, Allow, "unsafe operations in unsafe functions without an explicit unsafe block are deprecated"); - edition 覆盖规则
@edition Edition2024 => Warn——即 2024 edition 自动升为warn; - 标记为
@future_incompatible = FutureIncompatibleInfo { reason: fcw!(EditionError 2024 "unsafe-op-in-unsafe-fn") }——表明这是一个面向未来不兼容(future-incompatible)的演进项:它目前只是警告,但按 edition 升级机制预留了升级为 error 的路径,开发者若继续依赖隐式权限,跨 edition 升级时会收到明确的 future-incompatibility 报告。
lint 文档注释还解释了动机:unsafe fn 允许任意 unsafe 操作会扩大需要逐行审查的 unsafe 代码面,而显式 unsafe 块能把"到底哪些行在做 unsafe 操作"这件事变得一目了然(注释中提及了 RFC 2585 与跟踪 issue #71668,可用这两个编号在语言 RFC 仓库中进一步查证)。
四、源码纵深:编译器如何判定"需要 unsafe"——SafetyContext 状态机
E0133 的检查实现在 rustc_mir_build 的 check_unsafety.rs。入口是 check_unsafety(tcx, def)(L1053):编译器为每个函数体构建 UnsafetyVisitor,在 MIR 构建阶段自顶向下遍历 THIR(类型化 HIR),对每个表达式记录当前所处的安全性上下文(safety_context 字段,L27-L29 注释明确写道:"This notably tracks whether we are in an unsafe block, and whether it has been used")。
SafetyContext 有四种状态,从 requires_unsafe 函数的 match 分支可以完整读出判定逻辑:
fn requires_unsafe(&mut self, span: Span, kind: UnsafeOpKind) {
let unsafe_op_in_unsafe_fn_allowed = self.unsafe_op_in_unsafe_fn_allowed();
match self.safety_context {
SafetyContext::BuiltinUnsafeBlock => {}
SafetyContext::UnsafeBlock { ref mut used, .. } => { *used = true; }
SafetyContext::UnsafeFn if unsafe_op_in_unsafe_fn_allowed => {}
SafetyContext::UnsafeFn => { /* 触发 unsafe_op_in_unsafe_fn lint */ }
SafetyContext::Safe => { /* 触发 E0133 错误 */ }
}
}
逐分支解读:
BuiltinUnsafeBlock:编译器内部生成的隐式 unsafe 块(如部分运行时机制),直接放行,不产生任何提示;UnsafeBlock:显式unsafe { }块内部,放行并把该块标记为"已使用"(*used = true)——源码注释特别强调,即使在unsafe fn内这个块"技术上冗余"也要标记为已使用,"because we want to eventually enableunsafe_op_in_unsafe_fnby default"。这解释了为什么现在就在unsafe fn里写unsafe { }永远不会收到"多余"警告;UnsafeFn且 lint 允许:即 2021 及更早 edition(或手动#![allow(unsafe_op_in_unsafe_fn)])下,隐式权限仍然生效,不报错也不警告;UnsafeFn且 lint 不允许(2024 edition 默认):不是硬错误,而是发出unsafe_op_in_unsafe_fnlint 警告(emit_unsafe_op_in_unsafe_fn_lint),并附带"把整个函数体包进 unsafe 块"的一次性建议;Safe:普通安全上下文,直接调用emit_requires_unsafe_err产生 E0133 硬错误——这正是本文开头示例中f();报错的路径。
判定"lint 是否允许"的实现极其直接(L175-L178):
/// Whether the `unsafe_op_in_unsafe_fn` lint is `allow`ed at the current HIR node.
fn unsafe_op_in_unsafe_fn_allowed(&self) -> bool {
self.tcx.lint_level_spec_at_node(UNSAFE_OP_IN_UNSAFE_FN, self.hir_context).is_allow()
}
即按当前 HIR 节点查询 lint 级别——这意味着 #[allow]/#[warn]/#[deny] 属性、#![forbid] 等 lint 控制机制在任意嵌套作用域内都能生效,而 2024 edition 的默认 warn 正通过 lint 系统的 edition 机制注入。
五、错误措辞的两种变体:"requires unsafe block" vs "requires unsafe function or block"
阅读源码可以发现,emit_requires_unsafe_err(check_unsafety.rs L840 起)对每一种 unsafe 操作都按 unsafe_op_in_unsafe_fn_allowed 分发到成对的两套诊断模板:
- lint 被允许时:
CallToUnsafeFunctionRequiresUnsafeUnsafeOpInUnsafeFnAllowed,措辞为 "... is unsafe and requires unsafe function or block"; - lint 不被允许时:
CallToUnsafeFunctionRequiresUnsafe,措辞为 "... is unsafe and requires unsafe block"。
这个看似细微的措辞差异有实际含义:它如实反映了"你当前还剩下哪些出路"——2021 edition 下你可以把调用搬进一个 unsafe fn 或包进 unsafe 块二选一;而在 2024 edition 下(或把 lint 提为 deny 的项目中),unsafe fn 不再是一个可接受的替代方案,只应使用显式 unsafe 块。同时该函数在报错前会先向上查找外层节点(note_non_inherited,L847-L860):若直接外层是一个 unsafe 块或 unsafe fn,就生成 UnsafeNotInheritedNote 指向它,提醒"外层的 unsafe 不会传递进来"——这是开发者最容易产生的直觉误区("我在 unsafe fn 里为什么还报错?"),编译器用这条 note 正面回答了它。
六、另一面:unused_unsafe —— 空 unsafe 块的告警
理解了"unsafe 操作缺少块"报 E0133,就要理解其镜像问题:有块但没有 unsafe 操作会怎样。同一检查器在 in_safety_context 中处理离开 unsafe 块时的逻辑:若一个 unsafe 块在整个生命周期内从未被任何 unsafe 操作"使用"(used == false),就调用 warn_unused_unsafe 发出 unused_unsafe 警告;若外层块已"使用"而嵌套的某块没用,还会附带 UnusedUnsafeEnclosing::Block 提示指认外层块。
这对实践的直接启示是双向的:
- 把大函数体整个塞进一个巨型
unsafe块会稀释审查价值,且 lint 检查无法区分块内哪些行真正 unsafe; - 保持块的最小化,
unused_unsafe会在你删掉了块内唯一 unsafe 调用后自动提醒你"这个块可以退役了"。
源码注释中还提到一个特例:即使处于 unsafe fn 内部,显式 unsafe 块也被无条件标记为"已使用"(见第四节第 2 条),因此向 2024 edition 收敛的迁移过程中,提前给 unsafe fn 补块不会产生任何新警告——这正是文档"Recommended way to write this"示例的底层保障。
七、一个相邻机制:deprecated safe fn 与 2024 edition 过渡
在 requires_unsafe 的 UnsafeFn/Safe 两个分支进入报错之前,还有一步 emit_deprecated_safe_fn_call(L90 起):若目标函数带有 RustcDeprecatedSafe2024 属性(即某函数在 2024 edition 中从"看似安全"降级为必须 unsafe 调用),且调用方代码来自 2024 之前的 edition,编译器不直接报错,而是发 DEPRECATED_SAFE_2024 警告,并用 CallToDeprecatedSafeFnRequiresUnsafe 诊断建议插入一行 // FIXME: Audit that {guarantee} 注释——提示你在升级 edition 前人工审计该调用所依赖的安全保证。这说明 E0133 所在的检查器同时承担着 edition 演进中"unsafe 语义收紧"的过渡通知职责,理解它能帮你提前排查跨 edition 升级时的编译告警。
八、排查与验证清单
结合上述文档与源码,遇到或预防 E0133 时可以按以下清单操作:
- 读错误正文确定操作类别:错误消息本身(第二节的完整表格)已经指明是哪一类 unsafe 操作,并按 need 提示了潜在 UB 原因;
- 看是否附加了 "not inherited" note:若有,说明你在
unsafe fn内写代码而期望继承权限——检查你的 edition:2024 下必须显式加块,或显式#; - 确认 edition 与 lint 级别:
UNSAFE_OP_IN_UNSAFE_FN的默认级别由 edition 决定(2021 及以前allow、2024warn,见 builtin.rs L2646-L2653),项目中还可能有#![deny(unsafe_op_in_unsafe_fn)]等属性进一步提高严格度——建议直接开启该 lint 并在 CI 中以deny运行,提前暴露 2024 edition 问题; - 应用建议修复:优先采纳编译器的
unsafe { ... }包裹建议,并保持块最小化,让unused_unsafe帮你回收空块; - 查阅错误码文档:每个错误码在 compiler/rustc_error_codes/src/error_codes/ 下都有独立文档,E0133 的原始说明、错误/正确示例及 lint 背景都收录其中,可作为团队内训材料直接使用。
九、小结
E0133 表面上只是"忘了写 unsafe 块"的语法错误,但其背后是 Rust 安全模型的一条核心原则:unsafe 操作的合法区域必须显式可审计。从编译器源码看,UnsafetyVisitor 通过 SafetyContext 四态机(Safe / UnsafeFn / UnsafeBlock / BuiltinUnsafeBlock)逐表达式判定每个操作所处上下文,配合 unsafe_op_in_unsafe_fn 这个按 edition 升级的 future-incompatible lint,正在把"unsafe fn 内隐式权限"这一历史遗留逐步淘汰。对 Rust 2024 edition 用户而言,正确姿势只有一条:无论身处 fn 还是 unsafe fn,unsafe 操作一律显式 unsafe { } 包裹,并让 unused_unsafe 警告保证这些块始终是最小且必要的。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0626
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00