首页
/ 深入理解 Rust 编译器 E0133 错误:为什么 Unsafe 操作必须位于 `unsafe` 块中

深入理解 Rust 编译器 E0133 错误:为什么 Unsafe 操作必须位于 `unsafe` 块中

2026-09-06 16:06:42作者:齐添朝

本文围绕 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!
}

实践中的两个要点:

  1. 最小化 unsafe 区域unsafe 块应只包裹真正执行 unsafe 操作的那几句语句,而不是整个函数体,这样后续审计和 unused_unsafe 检查(见第六节)都能精确定位问题;
  2. 块内仍然要遵守 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! 声明给出了三个关键事实:

  1. 默认级别为 Allowpub UNSAFE_OP_IN_UNSAFE_FN, Allow, "unsafe operations in unsafe functions without an explicit unsafe block are deprecated");
  2. edition 覆盖规则 @edition Edition2024 => Warn——即 2024 edition 自动升为 warn
  3. 标记为 @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 错误 */ }
    }
}

逐分支解读:

  1. BuiltinUnsafeBlock:编译器内部生成的隐式 unsafe 块(如部分运行时机制),直接放行,不产生任何提示;
  2. UnsafeBlock:显式 unsafe { } 块内部,放行并把该块标记为"已使用"(*used = true)——源码注释特别强调,即使在 unsafe fn 内这个块"技术上冗余"也要标记为已使用,"because we want to eventually enable unsafe_op_in_unsafe_fn by default"。这解释了为什么现在就在 unsafe fn 里写 unsafe { } 永远不会收到"多余"警告;
  3. UnsafeFn 且 lint 允许:即 2021 及更早 edition(或手动 #![allow(unsafe_op_in_unsafe_fn)])下,隐式权限仍然生效,不报错也不警告;
  4. UnsafeFn 且 lint 不允许(2024 edition 默认):不是硬错误,而是发出 unsafe_op_in_unsafe_fn lint 警告emit_unsafe_op_in_unsafe_fn_lint),并附带"把整个函数体包进 unsafe 块"的一次性建议;
  5. 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_errcheck_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_inheritedL847-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_unsafeUnsafeFn/Safe 两个分支进入报错之前,还有一步 emit_deprecated_safe_fn_callL90 起):若目标函数带有 RustcDeprecatedSafe2024 属性(即某函数在 2024 edition 中从"看似安全"降级为必须 unsafe 调用),且调用方代码来自 2024 之前的 edition,编译器不直接报错,而是发 DEPRECATED_SAFE_2024 警告,并用 CallToDeprecatedSafeFnRequiresUnsafe 诊断建议插入一行 // FIXME: Audit that {guarantee} 注释——提示你在升级 edition 前人工审计该调用所依赖的安全保证。这说明 E0133 所在的检查器同时承担着 edition 演进中"unsafe 语义收紧"的过渡通知职责,理解它能帮你提前排查跨 edition 升级时的编译告警。

八、排查与验证清单

结合上述文档与源码,遇到或预防 E0133 时可以按以下清单操作:

  1. 读错误正文确定操作类别:错误消息本身(第二节的完整表格)已经指明是哪一类 unsafe 操作,并按 need 提示了潜在 UB 原因;
  2. 看是否附加了 "not inherited" note:若有,说明你在 unsafe fn 内写代码而期望继承权限——检查你的 edition:2024 下必须显式加块,或显式 #![allow(unsafe_op_in_unsafe_fn)](不推荐);
  3. 确认 edition 与 lint 级别UNSAFE_OP_IN_UNSAFE_FN 的默认级别由 edition 决定(2021 及以前 allow、2024 warn,见 builtin.rs L2646-L2653),项目中还可能有 #![deny(unsafe_op_in_unsafe_fn)] 等属性进一步提高严格度——建议直接开启该 lint 并在 CI 中以 deny 运行,提前暴露 2024 edition 问题;
  4. 应用建议修复:优先采纳编译器的 unsafe { ... } 包裹建议,并保持块最小化,让 unused_unsafe 帮你回收空块;
  5. 查阅错误码文档:每个错误码在 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 警告保证这些块始终是最小且必要的。

登录后查看全文
热门项目推荐
相关项目推荐