《Rust 编译器原理》完整目录
- 前言
- 第1章 编译管线全景:从源码到机器码的完整旅程
- 第2章 所有权系统:编译期内存管理的核心机制
- 第3章 借用检查器:编译器如何证明内存安全
- 第4章 生命周期:编译器如何推断引用的有效范围
- 第5章 内存布局:编译器如何排列数据
- 第6章 单态化:泛型的编译期展开
- 第7章 Trait 静态分发:零成本抽象的编译器实现
- 第8章 Trait Object 与虚表:运行时多态的内存布局
- 第9章 async/await:状态机的编译器变换
- 第10章 Pin、Waker 与 Future:异步运行时的三大支柱
- 第11章 闭包:匿名函数的编译器实现
- 第12章 unsafe:安全抽象的逃生舱(当前)
- 第13章 FFI:与 C 世界的桥梁
- 第14章 宏系统:编译期的元编程引擎
- 第15章 MIR 优化:编译器的中间表示与优化管线
- 第16章 LLVM 代码生成:从 MIR 到机器码
- 第17章 增量编译:让重编译只做必要的事
- 第18章 设计哲学与架构决策
第12章 unsafe:安全抽象的逃生舱
"unsafe 不意味着代码是错误的。它意味着编译器不再为你检查正确性------你必须自己保证。" ------ Ralf Jung
Rust 的安全模型是编程语言设计史上最激进的实验之一:它试图在编译期证明程序不会出现内存错误。但任何静态分析都有其极限------当你需要直接操作硬件、调用外部 C 函数、实现零成本抽象的底层数据结构时,编译器的证明能力就不够了。这就是 unsafe 存在的意义:它不是安全模型的漏洞,而是安全模型的完备性补充。
本章将从编译器实现的角度,彻底剖析 unsafe 的每一个维度。我们会深入 rustc_mir_build 中的 UnsafetyVisitor,看它如何在 THIR(Typed High-level IR)上追踪安全上下文;我们会研究标准库如何在 unsafe 之上构建 Vec、String、HashMap 等看似安全的抽象;我们会理解 UnsafeCell 为什么是整个内部可变性体系的基石;我们还会学习如何使用 Miri 在测试阶段捕获未定义行为。
:::tip 本章要点
unsafe块精确解锁五种特定操作:解引用裸指针、调用 unsafe 函数、访问可变静态变量、实现 unsafe trait、访问 union 字段unsafe不会关闭所有权检查、借用检查、类型检查------这些在 unsafe 块中仍然完全有效- 编译器通过
UnsafetyVisitor在 THIR 上遍历每个表达式,维护SafetyContext状态机来判断 unsafe 操作是否合法 UnsafeCell<T>是 Rust 中唯一允许通过共享引用获取可变指针的类型,是Cell、RefCell、Mutex等所有内部可变性类型的基石- 标准库中
Vec、String、HashMap等核心数据结构的内部实现大量使用 unsafe,但对外暴露完全安全的 API - Miri 作为 MIR 解释器,能在测试阶段检测 unsafe 代码中的未定义行为,包括别名违规、悬垂指针、数据竞争等
- 编写 unsafe 代码的核心原则是:最小化 unsafe 表面积、文档化不变量、使用 Miri 测试 :::
12.1 五种 unsafe 超能力:精确的能力解锁
unsafe 关键字不是一个"关闭所有检查"的开关。它是一把极其精确的钥匙,只打开五扇特定的门。理解这五种超能力的边界,是编写正确 unsafe 代码的前提。
12.1.1 解引用裸指针
裸指针 *const T 和 *mut T 的创建是安全的,但解引用(读取或写入指针指向的值)必须在 unsafe 块中进行:
rust
fn raw_pointer_demo() {
let value = 42;
// 创建裸指针是安全的------编译器允许
let ptr: *const i32 = &value as *const i32;
let mut_ptr: *mut i32 = &value as *const i32 as *mut i32;
// 指针比较、转换、算术都是安全的
let addr = ptr as usize;
let is_null = ptr.is_null();
let is_aligned = (ptr as usize) % std::mem::align_of::<i32>() == 0;
// 但解引用必须在 unsafe 块中
unsafe {
let val = *ptr; // 读取
assert_eq!(val, 42);
// 指针算术后解引用
let arr = [10, 20, 30];
let arr_ptr = arr.as_ptr();
let second = *arr_ptr.add(1); // 偏移后读取
assert_eq!(second, 20);
}
}
为什么解引用是 unsafe 的?因为编译器无法在编译期验证:
- 指针指向的内存是否已经被释放(悬垂指针)
- 指针是否为 null
- 指针是否正确对齐
- 指向的内存是否包含类型
T的有效值 - 是否存在别名违规(同时有
&T和&mut T指向同一位置)
12.1.2 调用 unsafe 函数
标记为 unsafe fn 的函数有额外的前置条件(precondition),编译器无法自动验证,因此调用者必须在 unsafe 块中调用并手动保证这些条件成立:
rust
use std::alloc::{alloc, dealloc, Layout};
fn allocation_demo() {
// std::alloc::alloc 是 unsafe fn,因为:
// 1. Layout 必须非零大小(否则行为未定义)
// 2. 返回的指针可能为 null(分配失败)
// 3. 返回的内存未初始化
let layout = Layout::new::<[u8; 1024]>();
unsafe {
let ptr = alloc(layout);
if ptr.is_null() {
std::alloc::handle_alloc_error(layout);
}
// 写入数据
std::ptr::write(ptr as *mut u64, 0xDEADBEEF);
// 必须使用相同的 Layout 释放
dealloc(ptr, layout);
// 此后 ptr 是悬垂指针,任何解引用都是 UB
}
}
// 自定义 unsafe 函数------调用者必须保证 index < slice.len()
/// # Safety
/// `index` must be less than `slice.len()`.
unsafe fn get_unchecked_demo(slice: &[i32], index: usize) -> i32 {
// 不做边界检查,直接通过指针访问
*slice.as_ptr().add(index)
}
注意 unsafe fn 的语义是:这个函数有编译器无法验证的前置条件。不是说函数体内的代码一定会出错,而是说调用者有责任满足文档中列出的安全约束。
12.1.3 访问或修改可变静态变量
static mut 变量可以被任何线程在任何时刻访问,没有同步机制保护,因此读写都是 unsafe 的:
rust
static mut REQUEST_COUNT: u64 = 0;
static mut CONFIG_BUFFER: [u8; 4096] = [0; 4096];
fn static_mut_demo() {
unsafe {
// 读和写都需要 unsafe
REQUEST_COUNT += 1;
let count = REQUEST_COUNT;
// 即使是单线程,编译器也无法证明没有别名违规
CONFIG_BUFFER[0] = 0xFF;
}
}
static mut 之所以 unsafe,核心原因是它违反了 Rust 的排他性访问 原则。在多线程环境下,多个线程同时写入 static mut 构成数据竞争(data race),这是未定义行为。即使在单线程中,编译器也无法确保你没有同时持有 static mut 的多个可变引用。
最佳实践 :几乎所有场景都应该用 static + AtomicU64 / Mutex<T> / OnceLock<T> 替代 static mut:
rust
use std::sync::atomic::{AtomicU64, Ordering};
// 安全的替代方案
static REQUEST_COUNT_SAFE: AtomicU64 = AtomicU64::new(0);
fn safe_counter() {
REQUEST_COUNT_SAFE.fetch_add(1, Ordering::Relaxed);
let count = REQUEST_COUNT_SAFE.load(Ordering::Relaxed);
}
12.1.4 访问 union 的字段
Rust 的 union 类型类似于 C 的 union------所有字段共享同一块内存。读取 union 字段是 unsafe 的,因为编译器不知道当前存储的是哪个变体:
rust
#[repr(C)]
union FloatBits {
f: f32,
bits: u32,
}
fn union_demo() {
let fb = FloatBits { f: 1.0 };
// 读取 union 字段是 unsafe 的
unsafe {
// 将 f32 的位模式重新解释为 u32
let bits = fb.bits;
assert_eq!(bits, 0x3F800000); // IEEE 754: 1.0f32
// 也可以读回 f 字段
let f = fb.f;
assert_eq!(f, 1.0);
}
// 但写入 union 字段是安全的(只是覆盖内存)
let mut fb2 = FloatBits { bits: 0 };
fb2.bits = 0x40000000; // 安全:只是写入
unsafe {
assert_eq!(fb2.f, 2.0); // unsafe:读取需要确认类型解释合法
}
}
读取 union 字段之所以 unsafe,是因为编译器无法保证内存中的位模式对于你要读取的类型是合法的值。如果 union 存储了一个 u32 值 2,然后你把它作为 bool 读取(只有 0 和 1 是合法的 bool 值),就会产生未定义行为。
12.1.5 实现 unsafe trait
unsafe trait 表示该 trait 有编译器无法验证的语义约束。实现它需要 unsafe impl,表示实现者承诺满足这些约束:
rust
/// 一个 unsafe trait 的例子
/// # Safety
/// 实现者必须保证:
/// 1. 类型的所有位模式都是合法的(可以被 zeroed)
/// 2. 类型不包含引用或指针(因为 null 引用是 UB)
unsafe trait ZeroValid {}
// 基本整数类型可以安全地全零初始化
unsafe impl ZeroValid for u8 {}
unsafe impl ZeroValid for u32 {}
unsafe impl ZeroValid for i64 {}
unsafe impl ZeroValid for usize {}
// bool 不能实现------0 是 false,但全零之外的位模式对 bool 无意义
// unsafe impl ZeroValid for bool {} // 不正确,但编译器不会阻止
// 标准库中最重要的 unsafe trait
// Send: 类型可以安全地在线程间转移所有权
// Sync: 类型可以安全地在线程间共享引用
// 编译器自动为大多数类型实现,但也允许手动 unsafe impl
struct MyWrapper(*mut u8);
unsafe impl Send for MyWrapper {} // 程序员保证这个裸指针可以跨线程
unsafe impl Sync for MyWrapper {} // 程序员保证通过共享引用访问是安全的
Send 和 Sync 是 Rust 并发安全的基石,编译器会根据类型的组成自动推导。但有时自动推导过于保守(如裸指针不是 Send),需要手动 unsafe impl。
12.1.6 汇总:五种超能力的全景图
值只有一个所有者"] B["借用检查器
&T 和 &mut T 不共存"] C["类型系统
类型必须匹配"] D["生命周期检查
引用不能超过被引用对象"] E["边界检查
数组索引运行时检查"] end subgraph "unsafe 精确解锁的五种操作" F["解引用裸指针
*const T / *mut T"] G["调用 unsafe fn
前置条件由调用者保证"] H["访问 static mut
无同步保护的全局状态"] I["读取 union 字段
位模式重新解释"] J["实现 unsafe trait
语义约束由实现者保证"] end style A fill:#10b981,color:#fff,stroke:none style B fill:#10b981,color:#fff,stroke:none style C fill:#10b981,color:#fff,stroke:none style D fill:#10b981,color:#fff,stroke:none style E fill:#10b981,color:#fff,stroke:none style F fill:#ef4444,color:#fff,stroke:none style G fill:#ef4444,color:#fff,stroke:none style H fill:#ef4444,color:#fff,stroke:none style I fill:#ef4444,color:#fff,stroke:none style J fill:#ef4444,color:#fff,stroke:none
12.2 安全契约:unsafe 不会关闭借用检查器
这是理解 unsafe 最关键的认知------unsafe 块中的所有权系统、借用检查器、类型系统仍然完全有效。unsafe 只是在这些检查之上额外解锁了五种操作,而不是替换或关闭任何现有检查。
rust
fn safety_still_enforced() {
unsafe {
// 所有权检查------仍然生效
let s = String::from("hello");
let t = s; // s 被 move
// println!("{}", s); // 编译错误!即使在 unsafe 块中
// 借用检查------仍然生效
let mut v = vec![1, 2, 3];
let r = &v;
// v.push(4); // 编译错误!不能在共享借用存在时可变借用
println!("{:?}", r);
// 类型检查------仍然生效
let x: i32 = 42;
// let y: String = x; // 编译错误!类型不匹配
// 生命周期检查------仍然生效
let r2;
{
let local = 42;
r2 = &local;
}
// println!("{}", r2); // 编译错误!local 已经被销毁
}
}
| 安全机制 | unsafe 块中是否生效 | 说明 |
|---|---|---|
| 所有权与 Move 语义 | 完全生效 | 值仍然只能有一个所有者,move 后原变量不可用 |
| 借用检查器 | 完全生效 | &T 和 &mut T 不能共存,可变借用独占 |
| 生命周期检查 | 完全生效 | 引用不能超过被引用对象的生命周期 |
| 类型系统 | 完全生效 | 赋值和传参必须类型匹配 |
| 边界检查 | 完全生效 | slice[index] 仍有运行时边界检查 |
| 裸指针解引用 | 解除限制 | 可以通过 *ptr 读写裸指针指向的内存 |
| 调用 unsafe fn | 解除限制 | 可以调用标记为 unsafe 的函数 |
| 可变静态变量 | 解除限制 | 可以读写 static mut |
| union 字段访问 | 解除限制 | 可以读取 union 的任意字段 |
| unsafe trait 实现 | 解除限制 | 可以为类型实现 unsafe trait |
这意味着 unsafe 代码仍然享受大部分 Rust 安全保证。你不能在 unsafe 块中创建两个 &mut 引用指向同一个局部变量(借用检查器会阻止你),但你可以通过裸指针创建两个指向同一位置的可变指针------此时编译器不再保护你,正确性完全由你负责。
unsafe 的信任边界模型
unsafe 的核心哲学可以用一句话概括:编译器说"我无法验证这个操作的安全性",程序员说"我保证它是安全的"。 这形成了一个信任契约:
(用户代码)"] -->|"调用安全 API"| B["安全的公开接口
fn push(&mut self, val: T)"] B -->|"内部委托"| C["unsafe 代码块
ptr::write, ptr.add"] C -->|"维护不变量"| D["安全不变量
1. 指针有效
2. 无别名违规
3. 对齐正确
4. 值已初始化"] E["编译器"] -.->|"自动证明"| A F["库作者"] -.->|"手动保证"| D style A fill:#10b981,color:#fff,stroke:none style B fill:#3b82f6,color:#fff,stroke:none style C fill:#ef4444,color:#fff,stroke:none style D fill:#f59e0b,color:#000,stroke:none style E fill:#10b981,color:#fff,stroke:none style F fill:#ef4444,color:#fff,stroke:none
这个模型的关键洞见是:unsafe 不会传染 。一个正确封装的 unsafe 抽象,对外暴露的 API 是完全安全的。使用 Vec::push 的代码不需要任何 unsafe,因为 Vec 的实现者已经保证了内部 unsafe 操作的正确性。
12.3 编译器如何检查 unsafe:UnsafetyVisitor 深度解析
Rust 编译器对 unsafe 的检查不是在 MIR 层面进行的,而是在 THIR(Typed High-level IR)层面,通过 rustc_mir_build crate 中的 UnsafetyVisitor 实现。这是一个精心设计的访问者模式(Visitor Pattern),它遍历整个函数的 THIR 表示,追踪当前的"安全上下文",并在发现 unsafe 操作位于 safe 上下文时报错。
12.3.1 SafetyContext 状态机
编译器用一个枚举 SafetyContext 来表示当前代码所处的安全上下文:
rust
// 源码位于 compiler/rustc_mir_build/src/check_unsafety.rs
#[derive(Clone)]
enum SafetyContext {
Safe, // 安全上下文------unsafe 操作不允许
BuiltinUnsafeBlock, // 编译器生成的 unsafe 块(如 derive 宏)
UnsafeFn, // unsafe fn 内部------隐式 unsafe 上下文
UnsafeBlock { // 显式 unsafe {} 块
span: Span, // 块的源码位置
hir_id: HirId, // HIR 节点 ID
used: bool, // 是否包含了 unsafe 操作
nested_used_blocks: Vec<NestedUsedBlock>, // 嵌套的已使用 unsafe 块
},
}
这四种状态构成了一个状态机:
12.3.2 UnsafetyVisitor 的核心结构
rust
// 源码位于 compiler/rustc_mir_build/src/check_unsafety.rs
struct UnsafetyVisitor<'a, 'tcx> {
tcx: TyCtxt<'tcx>, // 类型上下文------访问所有编译信息
thir: &'a Thir<'tcx>, // 当前函数的 THIR 表示
hir_context: HirId, // 当前 HIR 作用域(用于 lint 级别)
safety_context: SafetyContext, // 当前安全上下文
body_target_features: &'tcx [TargetFeature], // 函数的 target_feature 属性
assignment_info: Option<Ty<'tcx>>, // 赋值左侧信息(union 字段处理)
in_union_destructure: bool, // 是否在 union 解构模式中
typing_env: ty::TypingEnv<'tcx>, // 类型环境
inside_adt: bool, // 是否在布局约束的 ADT 内部
warnings: &'a mut Vec<UnusedUnsafeWarning>, // 未使用的 unsafe 块警告
suggest_unsafe_block: bool, // 是否建议包装 unsafe 块
}
12.3.3 requires_unsafe:核心判断逻辑
当 UnsafetyVisitor 遇到一个需要 unsafe 的操作时,它调用 requires_unsafe 方法。这个方法根据当前 SafetyContext 决定是允许操作、标记 unsafe 块为已使用,还是报错:
rust
// 源码位于 compiler/rustc_mir_build/src/check_unsafety.rs(简化)
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 {
// 编译器生成的 unsafe 块------总是允许
SafetyContext::BuiltinUnsafeBlock => {}
// 显式 unsafe 块------标记为已使用
SafetyContext::UnsafeBlock { ref mut used, .. } => {
*used = true; // 关键:标记这个 unsafe 块确实包含了 unsafe 操作
}
// unsafe fn 内部,且 unsafe_op_in_unsafe_fn lint 为 allow------允许
SafetyContext::UnsafeFn if unsafe_op_in_unsafe_fn_allowed => {}
// unsafe fn 内部,但 lint 不允许------发出 lint 警告
SafetyContext::UnsafeFn => {
kind.emit_unsafe_op_in_unsafe_fn_lint(
self.tcx, self.hir_context, span, self.suggest_unsafe_block,
);
self.suggest_unsafe_block = false;
}
// 安全上下文------这是一个错误!
SafetyContext::Safe => {
kind.emit_requires_unsafe_err(
self.tcx, span, self.hir_context, unsafe_op_in_unsafe_fn_allowed,
);
}
}
}
这段代码揭示了一个重要的编译器行为:unsafe 块的"已使用"标记 。如果一个 unsafe {} 块内部没有执行任何 unsafe 操作,编译器会产生 unused_unsafe 警告。这是通过 used 字段追踪的------每次 requires_unsafe 在 UnsafeBlock 上下文中被调用,就会将 used 设为 true。
12.3.4 UnsafeOpKind:所有 unsafe 操作的分类
编译器将所有需要 unsafe 的操作分类为一个枚举,每种操作会产生不同的错误消息:
rust
// 源码位于 compiler/rustc_mir_build/src/check_unsafety.rs
#[derive(Clone, PartialEq)]
enum UnsafeOpKind {
CallToUnsafeFunction(Option<DefId>), // 调用 unsafe 函数
UseOfInlineAssembly, // 使用内联汇编 asm!
InitializingTypeWith, // 初始化布局约束类型
InitializingTypeWithUnsafeField, // 初始化含 unsafe 字段的类型
UseOfMutableStatic, // 使用 static mut
UseOfExternStatic, // 使用 extern static
UseOfUnsafeField, // 访问 unsafe 字段
DerefOfRawPointer, // 解引用裸指针
AccessToUnionField, // 访问 union 字段
MutationOfLayoutConstrainedField, // 修改布局约束字段
BorrowOfLayoutConstrainedField, // 借用布局约束字段
CallToFunctionWith { // 调用带 target_feature 的函数
function: DefId,
missing: Vec<Symbol>,
build_enabled: Vec<Symbol>,
},
UnsafeBinderCast, // unsafe binder 转换
}
注意这个枚举比"五种超能力"更细------它区分了 UseOfMutableStatic 和 UseOfExternStatic,也包含了 UseOfInlineAssembly(通常被归入"调用 unsafe 函数"的范畴,但编译器内部单独处理),以及 CallToFunctionWith(调用需要特定 CPU 特性的函数)。
12.3.5 visit_expr:表达式级别的 unsafe 检查
UnsafetyVisitor 实现了 Visitor trait,在遍历 THIR 的每个表达式时进行检查。以下是关键的匹配逻辑(从真实编译器源码提取):
rust
// 源码位于 compiler/rustc_mir_build/src/check_unsafety.rs(简化关键分支)
fn visit_expr(&mut self, expr: &'a Expr<'tcx>) {
match expr.kind {
// 函数调用------检查是否是 unsafe fn
ExprKind::Call { fun, .. } => {
let fn_ty = self.thir[fun].ty;
let sig = fn_ty.fn_sig(self.tcx);
if sig.safety().is_unsafe() {
let func_id = if let ty::FnDef(func_id, _) = fn_ty.kind() {
Some(*func_id)
} else {
None
};
self.requires_unsafe(expr.span, CallToUnsafeFunction(func_id));
}
}
// 解引用------检查是否是裸指针
ExprKind::Deref { arg } => {
if let ExprKind::StaticRef { def_id, .. } = self.thir[arg].kind {
if self.tcx.is_mutable_static(def_id) {
self.requires_unsafe(expr.span, UseOfMutableStatic);
}
} else if self.thir[arg].ty.is_raw_ptr() {
self.requires_unsafe(expr.span, DerefOfRawPointer);
}
}
// 字段访问------检查是否是 union 字段或 unsafe 字段
ExprKind::Field { lhs, variant_index, name } => {
let lhs = &self.thir[lhs];
if let ty::Adt(adt_def, _) = lhs.ty.kind() {
if adt_def.variant(variant_index).fields[name].safety.is_unsafe() {
self.requires_unsafe(expr.span, UseOfUnsafeField);
} else if adt_def.is_union() {
if self.assignment_info.is_none() {
self.requires_unsafe(expr.span, AccessToUnionField);
}
}
}
}
// 内联汇编------需要 unsafe
ExprKind::InlineAsm(box InlineAsmExpr {
asm_macro: AsmMacro::Asm, ..
}) => {
self.requires_unsafe(expr.span, UseOfInlineAssembly);
}
_ => {}
}
visit::walk_expr(self, expr);
}
12.3.6 visit_block:unsafe 块的进入和退出
当访问者遇到一个 unsafe {} 块时,它会切换安全上下文:
rust
// 源码位于 compiler/rustc_mir_build/src/check_unsafety.rs
fn visit_block(&mut self, block: &'a Block) {
match block.safety_mode {
// 编译器生成的 unsafe 块------不影响外层 unsafe 块的"已使用"状态
BlockSafety::BuiltinUnsafe => {
self.in_safety_context(SafetyContext::BuiltinUnsafeBlock, |this| {
visit::walk_block(this, block)
});
}
// 显式 unsafe 块
BlockSafety::ExplicitUnsafe(hir_id) => {
self.in_safety_context(
SafetyContext::UnsafeBlock {
span: block.span,
hir_id,
used: false, // 初始为未使用
nested_used_blocks: Vec::new(),
},
|this| visit::walk_block(this, block),
);
}
// 普通的安全块
BlockSafety::Safe => {
visit::walk_block(self, block);
}
}
}
in_safety_context 方法使用了一个精巧的模式:保存旧的上下文,设置新的上下文,执行闭包(遍历块内容),然后恢复旧的上下文。在恢复时,如果内部的 unsafe 块没有被使用(used == false),就记录一个未使用 unsafe 的警告。
12.3.7 嵌套 unsafe 块的检测
编译器还会检测冗余的嵌套 unsafe 块:
rust
fn nested_unsafe_example() {
unsafe {
// 这个 unsafe 块包含了 unsafe 操作------有用
let ptr: *const i32 = &42;
let _ = *ptr;
unsafe {
// 这个内层 unsafe 块是冗余的------外层已经提供了 unsafe 上下文
// 编译器会发出 unused_unsafe 警告
let _ = *ptr;
}
}
}
编译器通过 nested_used_blocks 追踪嵌套的 unsafe 块。当外层 unsafe 块被标记为已使用时,所有嵌套的已使用 unsafe 块会被报告为冗余,因为它们的 unsafe 操作已经被外层块覆盖。
12.4 裸指针体系:*const T 和 *mut T
裸指针是 unsafe Rust 的核心工具。理解裸指针的创建、操作和约束,是编写正确 unsafe 代码的基础。
12.4.1 创建裸指针(安全操作)
rust
fn creating_raw_pointers() {
// 从引用创建------最常见的方式
let x = 42;
let ptr_const: *const i32 = &x; // 从 &T 到 *const T
let ptr_mut: *mut i32 = &x as *const i32 as *mut i32; // 需要两步转换
// 从可变引用创建
let mut y = 42;
let ptr_mut2: *mut i32 = &mut y; // 从 &mut T 到 *mut T
let ptr_const2: *const i32 = &mut y; // 从 &mut T 到 *const T
// 从地址创建(无 provenance)
let addr: usize = 0xDEAD_BEEF;
let ptr_from_addr: *const i32 = std::ptr::without_provenance(addr);
// 注意:这个指针不能用于读写(没有 provenance),只能用于比较或标记
// 空指针
let null_ptr: *const i32 = std::ptr::null();
let null_mut_ptr: *mut i32 = std::ptr::null_mut();
// 从 Box/Vec 等获取
let boxed = Box::new(100);
let ptr_from_box: *const i32 = &*boxed; // 或 Box::into_raw(boxed)
}
关键点:创建裸指针本身是安全的。裸指针只是一个数字(内存地址),持有它不会违反任何安全不变量。只有当你解引用它(读取或写入指向的内存)时,才进入 unsafe 领域。
12.4.2 指针算术
Rust 提供了一组指针算术方法,有些是 safe 的,有些是 unsafe 的:
rust
fn pointer_arithmetic() {
let arr = [10i32, 20, 30, 40, 50];
let base: *const i32 = arr.as_ptr();
// add/sub 是 unsafe 的------可能产生越界指针
unsafe {
let second = *base.add(1); // 偏移 1 个 i32 = 4 字节
assert_eq!(second, 20);
let fourth = *base.add(3);
assert_eq!(fourth, 40);
// offset 接受 isize,可以负偏移
let third_ptr = base.add(4);
let second_from_end = *third_ptr.offset(-2);
assert_eq!(second_from_end, 30);
}
// wrapping_add/wrapping_sub 是安全的------但产生的指针不一定可解引用
let wrapped = base.wrapping_add(100); // 安全:不解引用就没问题
// unsafe { *wrapped } // UB:指针指向分配之外的内存
// 指针比较是安全的
let p1 = &arr[0] as *const i32;
let p2 = &arr[2] as *const i32;
assert!(p1 < p2);
// 计算两个指针之间的偏移(unsafe)
unsafe {
let offset = p2.offset_from(p1);
assert_eq!(offset, 2);
}
}
add 和 offset 是 unsafe 的原因很微妙:即使你不解引用结果指针,如果偏移结果超出了原始分配的范围,行为就是未定义的。这是因为 LLVM 的优化器假设 getelementptr inbounds(add 编译到的 LLVM 指令)不会产生越界指针。wrapping_add 使用的是不带 inbounds 标记的地址算术,所以是安全的------但它产生的指针可能没有有效的 provenance,不能用于内存访问。
12.4.3 ptr::read 和 ptr::write
标准库的 core::ptr 模块提供了通过裸指针读写内存的函数。让我们看看它们的真实实现:
rust
// 源码位于 library/core/src/ptr/mod.rs
pub const unsafe fn read<T>(src: *const T) -> T {
// 早期实现通过 copy_nonoverlapping + MaybeUninit
// 现在直接使用 intrinsic,在 MIR 中降级为 _0 = *src
// 这样 LLVM 可以获得更多类型元数据(!range, !nonnull, !noundef)
unsafe {
#[cfg(debug_assertions)]
ub_checks::assert_unsafe_precondition!(
check_language_ub,
"ptr::read requires that the pointer argument is aligned and non-null",
(
addr: *const () = src as *const (),
align: usize = align_of::<T>(),
is_zst: bool = T::IS_ZST,
) => ub_checks::maybe_is_aligned_and_not_null(addr, align, is_zst)
);
crate::intrinsics::read_via_copy(src)
}
}
pub const unsafe fn write<T>(dst: *mut T, src: T) {
// 直接使用 intrinsic,在 MIR 中降级为 *dst = move src
unsafe {
#[cfg(debug_assertions)]
ub_checks::assert_unsafe_precondition!(
check_language_ub,
"ptr::write requires that the pointer argument is aligned and non-null",
(
addr: *mut () = dst as *mut (),
align: usize = align_of::<T>(),
is_zst: bool = T::IS_ZST,
) => ub_checks::maybe_is_aligned_and_not_null(addr, align, is_zst)
);
intrinsics::write_via_move(dst, src)
}
}
注意两个细节:
- debug_assertions 下的前置条件检查 :在 debug 模式下,
ptr::read和ptr::write会检查指针的对齐和非空性。这不是完整的安全检查(不检查 provenance 或内存是否已释放),但能捕获最常见的错误。 - 直接使用 intrinsic :编译器将这些操作直接降级为 MIR 原语,避免了通过
MaybeUninit+copy_nonoverlapping的间接路径,使得 LLVM 能产生更优的代码。
ptr::read 和 ptr::write 的安全前置条件:
| 条件 | read |
write |
|---|---|---|
| 指针非空 | 必须 | 必须 |
| 指针正确对齐 | 必须 | 必须 |
| 指针指向有效的已分配内存 | 必须 | 必须 |
| 指向的内存已初始化为 T 的合法值 | 必须 | 不需要 |
| 不违反别名规则 | 必须 | 必须 |
还有两个变体不要求对齐:read_unaligned 和 write_unaligned,它们在处理打包结构体(#[repr(packed)])或网络协议解析时很有用。
12.4.4 Provenance(来源)系统
Rust 的指针模型不仅包含地址,还包含 provenance(来源信息)。这是理解为什么某些看似合理的指针操作是 UB 的关键。
rust
fn provenance_demo() {
let a = [1, 2, 3];
let b = [4, 5, 6];
let ptr_a = a.as_ptr();
let ptr_b = b.as_ptr();
// 即使 ptr_a.add(3) 恰好等于 ptr_b(栈上相邻分配)
// 通过 ptr_a.add(3) 读取 b[0] 仍然是 UB
// 因为 ptr_a 的 provenance 只覆盖数组 a 的内存范围
// 正确的做法:使用 ptr_b 读取 b 的数据
unsafe {
let val = *ptr_b; // 正确:ptr_b 有 b 的 provenance
assert_eq!(val, 4);
}
}
Provenance 有三个维度:
- 空间维度(Spatial):指针有权访问的内存地址范围
- 时间维度(Temporal):指针有权访问的时间跨度(分配存活期间)
- 可变性维度(Mutability):指针是只读还是可读写
标准库提供了两套 provenance API:
rust
fn provenance_apis() {
let x = 42u32;
let ptr = &x as *const u32;
// Strict Provenance(推荐)
let addr = ptr.addr(); // 提取地址,不暴露 provenance
let new_ptr = ptr.with_addr(addr | 1); // 创建标记指针,保留 provenance
let new_ptr = ptr.map_addr(|a| a & !1); // 地址变换,保留 provenance
// Exposed Provenance(兼容旧代码)
let addr = ptr.expose_provenance(); // 暴露 provenance 到全局列表
let ptr2: *const u32 = std::ptr::with_exposed_provenance(addr); // 从暴露列表恢复
}
12.5 unsafe fn vs unsafe block:不同的含义
unsafe 关键字在两个位置出现,含义完全不同:
12.5.1 unsafe 块:调用者声明"我已验证安全性"
rust
fn safe_wrapper(ptr: *const i32) -> Option<i32> {
if ptr.is_null() {
return None;
}
// unsafe 块说:我(代码作者)已经验证了安全前置条件
// 这里检查了非空,但仍有其他风险(对齐、provenance 等)
Some(unsafe { *ptr })
}
12.5.2 unsafe fn:函数声明"调用者必须保证安全性"
rust
/// 从裸指针创建切片引用。
///
/// # Safety
/// - `ptr` 必须指向连续的 `len` 个已初始化的 `T` 值
/// - 返回的引用的整个生命周期内,指向的内存不能被修改
/// - `len * size_of::<T>()` 不能超过 `isize::MAX`
unsafe fn slice_from_raw<'a, T>(ptr: *const T, len: usize) -> &'a [T] {
// 整个函数体是隐式的 unsafe 上下文
std::slice::from_raw_parts(ptr, len)
}
12.5.3 Rust 2024 Edition 的变化
从 Rust 2024 edition 开始,unsafe fn 内部的 unsafe 操作也需要显式的 unsafe {} 块。这是一个重大的语义演进:
rust
// Rust 2021 及之前------unsafe fn 内部隐式 unsafe
unsafe fn old_style(ptr: *const i32) -> i32 {
*ptr // 整个函数体是 unsafe 上下文,不需要 unsafe 块
}
// Rust 2024------unsafe fn 内部需要显式 unsafe 块
unsafe fn new_style(ptr: *const i32) -> i32 {
unsafe { *ptr } // 必须显式标记
}
这个变化的动机是提高代码审查的精确度。在大型 unsafe fn 中,哪些操作真正需要 unsafe、哪些只是普通的安全代码,过去很难区分。新规则强制开发者精确标记每个 unsafe 操作点。
编译器通过 UNSAFE_OP_IN_UNSAFE_FN lint 控制此行为:
rust
// 编译器源码中的检查逻辑
SafetyContext::UnsafeFn if unsafe_op_in_unsafe_fn_allowed => {}
SafetyContext::UnsafeFn => {
// lint 不允许------发出警告,建议添加 unsafe 块
kind.emit_unsafe_op_in_unsafe_fn_lint(
self.tcx, self.hir_context, span, self.suggest_unsafe_block,
);
}
12.6 构建安全抽象:标准库的 unsafe 实践
Rust 标准库是 unsafe 安全抽象的最佳范例。Vec、String、HashMap 等核心类型的内部充满了 unsafe 代码,但对外暴露的 API 完全安全。让我们深入研究这些实现。
12.6.1 Vec::push_mut 的真实实现
rust
// 源码位于 library/alloc/src/vec/mod.rs
impl<T> Vec<T> {
pub fn push(&mut self, value: T) {
let _ = self.push_mut(value);
}
pub fn push_mut(&mut self, value: T) -> &mut T {
// 保存长度------告诉编译器 len 在 grow_one() 前后不变
let len = self.len;
// 如果容量不足,扩容
if len == self.buf.capacity() {
self.buf.grow_one();
}
unsafe {
// 1. 计算写入位置:当前末尾之后的一个位置
let end = self.as_mut_ptr().add(len);
// 2. 写入值(不会读取或 drop 目标位置的旧值)
ptr::write(end, value);
// 3. 更新长度
self.len = len + 1;
// 4. 返回刚写入值的可变引用
&mut *end
}
}
}
这段代码中 unsafe 的正确性依赖于以下不变量:
self.buf管理的内存至少有capacity个T的空间self.len <= self.capacity,所以add(len)不会越界- 位置
len处没有已初始化的值(因为len之后的空间是"未使用"的),所以ptr::write不会泄漏旧值 - 写入后立即更新
self.len,维护了Vec的不变量
如果任何一个不变量被破坏------比如某个 bug 导致 self.len > self.capacity------ptr::write 就会写入未分配的内存,造成堆破坏。
12.6.2 String::from_utf8_unchecked
rust
// 源码位于 library/alloc/src/string.rs
pub unsafe fn from_utf8_unchecked(bytes: Vec<u8>) -> String {
// 就这么简单:直接用 bytes 构造 String
String { vec: bytes }
}
这可能是标准库中最简短的 unsafe 函数之一。它的安全前置条件只有一个:bytes 必须是合法的 UTF-8 序列 。如果违反这个条件,后续任何操作 String 的代码都可能产生未定义行为------因为 String 的所有方法都假设内部数据是有效的 UTF-8。
对比安全版本 String::from_utf8:
rust
pub fn from_utf8(vec: Vec<u8>) -> Result<String, FromUtf8Error> {
match str::from_utf8(&vec) {
Ok(..) => Ok(String { vec }), // 验证通过,安全构造
Err(e) => Err(FromUtf8Error { bytes: vec, error: e }),
}
}
安全版本做了 O(n) 的 UTF-8 验证。当你已经确信数据是合法 UTF-8(比如从已知安全的源获取),可以用 from_utf8_unchecked 跳过检查,获得 O(1) 的性能。这就是 unsafe 的价值所在:在安全性已由其他方式保证时,移除冗余的运行时检查。
12.6.3 HashMap 的 unsafe 使用
HashMap 的底层实现 hashbrown 大量使用 unsafe 来管理内存布局和哈希表探测:
rust
// hashbrown 的 RawTable 使用 unsafe 的典型模式(概念简化)
struct RawTable<T> {
// 控制字节数组------每个槽位一个字节,标记空/满/已删除
ctrl: *mut u8,
// 数据区------紧跟在控制字节之后
data: *mut T,
// 桶的数量(总是 2 的幂)
bucket_mask: usize,
// 已存储的元素数量
items: usize,
// 增长阈值
growth_left: usize,
}
impl<T> RawTable<T> {
// 通过哈希值探测到一个已知存在的元素
unsafe fn find(&self, hash: u64) -> Option<*mut T> {
// 使用 SIMD 指令批量比较控制字节
// 这是 unsafe 的,因为:
// 1. 需要解引用 ctrl 指针
// 2. 需要解引用 data 指针
// 3. 假设 bucket_mask + 1 个槽位都已分配
let index = (hash as usize) & self.bucket_mask;
let ctrl_byte = *self.ctrl.add(index);
if ctrl_byte == hash_to_ctrl(hash) {
Some(self.data.add(index))
} else {
// 线性探测...
None
}
}
}
HashMap 对外的 API(insert、get、remove)完全不需要 unsafe------所有的指针操作都被封装在内部,由 RawTable 的不变量保证正确性。
12.6.4 安全抽象的设计模式
从上述标准库实例中,我们可以提炼出安全抽象的通用模式:
指针有效"] K["内容是合法 UTF-8"] L["控制字节与数据一致
桶数为 2 的幂"] end A --> D --> G --> J B --> E --> H --> K C --> F --> I --> L style A fill:#10b981,color:#fff,stroke:none style B fill:#10b981,color:#fff,stroke:none style C fill:#10b981,color:#fff,stroke:none style D fill:#3b82f6,color:#fff,stroke:none style E fill:#3b82f6,color:#fff,stroke:none style F fill:#3b82f6,color:#fff,stroke:none style G fill:#ef4444,color:#fff,stroke:none style H fill:#ef4444,color:#fff,stroke:none style I fill:#ef4444,color:#fff,stroke:none style J fill:#f59e0b,color:#000,stroke:none style K fill:#f59e0b,color:#000,stroke:none style L fill:#f59e0b,color:#000,stroke:none
核心设计原则:
- 最小化 unsafe 表面积:只在真正需要的地方使用 unsafe,尽可能将安全检查放在 unsafe 之前
- 验证在前,unsafe 在后:先做完所有安全检查(边界、容量、格式验证等),再进入 unsafe 区域
- 不变量文档化 :每个使用 unsafe 的地方都应该有
// SAFETY:注释,说明为什么这个操作是安全的 - 封装而非暴露:unsafe 操作应该被封装在安全的公开 API 后面,不让不安全性泄漏到调用者
12.7 内部可变性的基石:UnsafeCell
UnsafeCell<T> 是 Rust 类型系统中最特殊的类型。它是唯一一个允许通过共享引用(&T)获取可变指针(*mut T)的类型,是整个内部可变性(interior mutability)体系的基石。
12.7.1 UnsafeCell 的定义
rust
// 源码位于 library/core/src/cell.rs
#[repr(transparent)]
pub struct UnsafeCell<T: ?Sized> {
value: T,
}
// 关键:UnsafeCell 不是 Sync
// 这意味着 &UnsafeCell<T> 不能在线程间共享(除非 T: Sync 且有额外同步)
impl<T: ?Sized> !Sync for UnsafeCell<T> {}
impl<T> UnsafeCell<T> {
pub const fn new(value: T) -> UnsafeCell<T> {
UnsafeCell { value }
}
pub const fn into_inner(self) -> T {
self.value
}
}
impl<T: ?Sized> UnsafeCell<T> {
// 这是 UnsafeCell 的核心:从 &self 获取 *mut T
pub const fn get(&self) -> *mut T {
self as *const UnsafeCell<T> as *const T as *mut T
}
// 获取可变引用(需要 &mut self,所以是安全的)
pub const fn get_mut(&mut self) -> &mut T {
&mut self.value
}
}
UnsafeCell::get 方法是 safe 的------它只返回一个裸指针,不做解引用。但通过这个指针修改值是 unsafe 的。
12.7.2 为什么 UnsafeCell 是特殊的
Rust 的别名规则(aliasing rules)规定:通过共享引用 &T 访问的内存,在引用存活期间不能被修改 。编译器基于此规则进行优化------它可以假设 &T 指向的值不会改变,从而缓存读取结果、消除冗余加载等。
UnsafeCell<T> 是这个规则的唯一例外。编译器知道 &UnsafeCell<T> 内部的值可能被修改 ,因此不会对通过 UnsafeCell 访问的内存做上述优化。
在编译器的 LLVM IR 生成中,这体现为:
&T(非UnsafeCell)生成带noalias和readonly标记的参数&UnsafeCell<T>不会生成这些标记
12.7.3 Cell 和 RefCell:UnsafeCell 的安全包装
标准库基于 UnsafeCell 构建了一系列安全的内部可变性类型:
rust
// Cell<T>------适用于 Copy 类型的内部可变性
use std::cell::Cell;
struct Counter {
count: Cell<u32>, // 即使通过 &Counter 也能修改
}
impl Counter {
fn increment(&self) { // 注意:&self,不是 &mut self
self.count.set(self.count.get() + 1);
}
}
// Cell 的简化实现
pub struct SimpleCell<T> {
value: UnsafeCell<T>,
}
impl<T: Copy> SimpleCell<T> {
pub fn get(&self) -> T {
// SAFETY: Cell 只用于 Copy 类型,不会产生悬垂引用
unsafe { *self.value.get() }
}
pub fn set(&self, val: T) {
// SAFETY:
// 1. 通过 UnsafeCell::get 获取 *mut T
// 2. T: Copy,所以不需要 drop 旧值
// 3. Cell 不是 Sync,所以不会有数据竞争
unsafe { *self.value.get() = val; }
}
}
rust
// RefCell<T>------运行时借用检查
use std::cell::RefCell;
struct Document {
content: RefCell<String>,
edit_count: Cell<u32>,
}
impl Document {
fn append(&self, text: &str) {
// borrow_mut() 在运行时检查是否有其他借用
let mut content = self.content.borrow_mut();
content.push_str(text);
self.edit_count.set(self.edit_count.get() + 1);
}
fn read(&self) -> String {
// borrow() 返回 Ref<String>,运行时检查无可变借用
self.content.borrow().clone()
}
}
// RefCell 的简化内部结构
pub struct SimpleRefCell<T> {
borrow_flag: Cell<isize>, // >0 表示有 n 个共享借用,-1 表示有一个可变借用
value: UnsafeCell<T>,
}
impl<T> SimpleRefCell<T> {
pub fn borrow(&self) -> &T {
let flag = self.borrow_flag.get();
if flag < 0 {
panic!("already mutably borrowed");
}
self.borrow_flag.set(flag + 1);
// SAFETY: 没有可变借用存在(我们刚检查过)
unsafe { &*self.value.get() }
}
pub fn borrow_mut(&self) -> &mut T {
let flag = self.borrow_flag.get();
if flag != 0 {
panic!("already borrowed");
}
self.borrow_flag.set(-1);
// SAFETY: 没有任何借用存在(我们刚检查过)
unsafe { &mut *self.value.get() }
}
}
12.7.4 Mutex 和 RwLock:线程安全的内部可变性
对于多线程场景,标准库提供了基于操作系统原语的同步类型,它们的核心仍然是 UnsafeCell:
rust
// Mutex 的简化结构
pub struct SimpleMutex<T> {
locked: AtomicBool, // 原子锁标志
data: UnsafeCell<T>, // 被保护的数据
}
// Mutex<T> 是 Sync 的(即使 T 不是 Sync),因为 Mutex 保证了排他访问
unsafe impl<T: Send> Sync for SimpleMutex<T> {}
impl<T> SimpleMutex<T> {
pub fn lock(&self) -> MutexGuard<'_, T> {
// 自旋等待获取锁
while self.locked.compare_exchange(
false, true,
Ordering::Acquire, Ordering::Relaxed
).is_err() {
std::hint::spin_loop();
}
MutexGuard { mutex: self }
}
}
struct MutexGuard<'a, T> {
mutex: &'a SimpleMutex<T>,
}
impl<T> std::ops::Deref for MutexGuard<'_, T> {
type Target = T;
fn deref(&self) -> &T {
// SAFETY: 我们持有锁,保证了排他访问
unsafe { &*self.mutex.data.get() }
}
}
impl<T> std::ops::DerefMut for MutexGuard<'_, T> {
fn deref_mut(&mut self) -> &mut T {
// SAFETY: 我们持有锁,保证了排他访问
unsafe { &mut *self.mutex.data.get() }
}
}
impl<T> Drop for MutexGuard<'_, T> {
fn drop(&mut self) {
self.mutex.locked.store(false, Ordering::Release);
}
}
12.7.5 内部可变性的类型层次
swift
UnsafeCell<T>(基石------提供通过 &self 获取 *mut T 的能力)
├── Cell<T>(单线程,Copy 类型,零开销)
├── RefCell<T>(单线程,任意类型,运行时借用检查)
├── Mutex<T>(多线程,操作系统互斥锁)
├── RwLock<T>(多线程,读写锁)
├── AtomicT(多线程,原子操作,无锁)
└── OnceCell<T> / OnceLock<T>(一次性初始化)
所有这些类型都通过不同的策略------编译时限制(Cell 只允许 Copy)、运行时检查(RefCell 的借用计数)、操作系统同步原语(Mutex)------来保证通过 UnsafeCell::get() 获取的可变指针被正确使用。
12.8 常见 unsafe 模式
12.8.1 transmute:位模式重解释
std::mem::transmute 是 Rust 中最强大也最危险的 unsafe 操作之一。它将一种类型的位模式直接重新解释为另一种类型:
rust
use std::mem;
fn transmute_examples() {
// 合法:f32 和 u32 大小相同,任何 f32 位模式都是合法 u32
let float_bits: u32 = unsafe { mem::transmute(1.0f32) };
assert_eq!(float_bits, 0x3F800000);
// 合法:查看 enum 的判别值
#[repr(u8)]
enum Color { Red = 0, Green = 1, Blue = 2 }
let discriminant: u8 = unsafe { mem::transmute(Color::Green) };
assert_eq!(discriminant, 1);
// 合法:&T 和 *const T 有相同的布局
let x = 42;
let ptr: *const i32 = unsafe { mem::transmute(&x) };
// 危险但合法:&[T] 和 (*const T, usize) 有相同布局
let slice: &[i32] = &[1, 2, 3];
let (ptr, len): (*const i32, usize) = unsafe { mem::transmute(slice) };
assert_eq!(len, 3);
}
transmute 的限制和替代:
rust
fn transmute_alternatives() {
// transmute 要求大小相同------否则编译错误
// let x: u64 = unsafe { mem::transmute(42u32) }; // 编译错误
// 更安全的替代方案:
// 1. 用 as 做数值转换(而非 transmute)
let x: u64 = 42u32 as u64;
// 2. 用 from_ne_bytes/to_ne_bytes 做整数/浮点转换
let float: f32 = f32::from_bits(0x3F800000);
let bits: u32 = float.to_bits();
// 3. 用 transmute_copy 处理不同大小的类型(更危险)
let big: u64 = 0xDEADBEEF_CAFEBABE;
let small: u32 = unsafe { mem::transmute_copy(&big) }; // 只复制前 4 字节
// 4. 用指针强转代替引用 transmute
let x: u32 = 42;
let ptr = &x as *const u32 as *const f32;
// 但注意:通过 ptr 读取仍然是 UB(违反别名规则),除非用 read_unaligned
}
transmute 导致 UB 的经典案例:
rust
fn transmute_ub_examples() {
// UB:创建非法的 bool 值
// let invalid_bool: bool = unsafe { mem::transmute(2u8) };
// bool 只有 0 (false) 和 1 (true) 两个合法值
// UB:创建非法的 enum 值
// #[repr(u8)]
// enum Tristate { A = 0, B = 1, C = 2 }
// let invalid: Tristate = unsafe { mem::transmute(42u8) };
// 42 不是任何变体的判别值
// UB:创建非法的引用
// let null_ref: &i32 = unsafe { mem::transmute(0usize) };
// 引用不能为 null
// UB:'static 引用指向局部变量
// let x = 42;
// let forever: &'static i32 = unsafe { mem::transmute(&x) };
// x 会在作用域结束时销毁,forever 变成悬垂引用
}
12.8.2 ManuallyDrop:抑制析构
ManuallyDrop<T> 允许你阻止 Rust 自动调用类型的析构函数:
rust
use std::mem::ManuallyDrop;
fn manually_drop_demo() {
// 基本用法:阻止 drop
let mut s = ManuallyDrop::new(String::from("hello"));
// 可以正常使用内部值
s.push_str(" world");
println!("{}", *s); // Deref 到 String
// 当 s 离开作用域时,String 不会被 drop
// 内存泄漏!除非你手动 drop
unsafe {
ManuallyDrop::drop(&mut s); // 手动调用析构函数
}
// 此后不能再使用 s------值已被销毁
}
// ManuallyDrop 在实现数据结构时很有用
struct MyVec<T> {
ptr: *mut T,
len: usize,
cap: usize,
}
impl<T> Drop for MyVec<T> {
fn drop(&mut self) {
// 需要 drop 每个元素,但不能"移出" *ptr.add(i)
// 因为 MyVec 拥有原始内存
unsafe {
// 使用 ptr::drop_in_place 原地析构每个元素
for i in 0..self.len {
std::ptr::drop_in_place(self.ptr.add(i));
}
// 然后释放内存
if self.cap > 0 {
let layout = std::alloc::Layout::array::<T>(self.cap).unwrap();
std::alloc::dealloc(self.ptr as *mut u8, layout);
}
}
}
}
12.8.3 MaybeUninit:安全地处理未初始化内存
MaybeUninit<T> 表示一块可能未初始化的内存。它是处理未初始化值的安全接口,替代了过去直接使用 mem::uninitialized()(已废弃)的方式:
rust
use std::mem::MaybeUninit;
fn maybe_uninit_demo() {
// 创建未初始化的值
let mut x: MaybeUninit<i32> = MaybeUninit::uninit();
// 写入值
x.write(42);
// 现在可以安全地读取
let val = unsafe { x.assume_init() };
assert_eq!(val, 42);
// 常见模式:初始化数组
let mut arr: [MaybeUninit<String>; 3] = [
MaybeUninit::uninit(),
MaybeUninit::uninit(),
MaybeUninit::uninit(),
];
arr[0].write(String::from("hello"));
arr[1].write(String::from("world"));
arr[2].write(String::from("!"));
// 将 [MaybeUninit<String>; 3] 转换为 [String; 3]
// SAFETY: 所有元素都已初始化
let arr: [String; 3] = unsafe {
// 使用 array_assume_init 或 transmute
let ptr = &arr as *const [MaybeUninit<String>; 3] as *const [String; 3];
std::ptr::read(ptr)
};
// 避免 double-free:原始 MaybeUninit 数组被遗忘
// (MaybeUninit 没有 Drop impl,所以没关系)
}
// MaybeUninit 的一个重要用途:避免初始化开销
fn init_large_buffer() -> Box<[u8; 1048576]> { // 1MB
// 错误方式:先全零初始化,再填充------浪费
// let mut buf = Box::new([0u8; 1048576]);
// 正确方式:分配未初始化内存
let mut buf: Box<MaybeUninit<[u8; 1048576]>> = Box::new_uninit();
// 填充数据
let slice = unsafe {
let ptr = buf.as_mut_ptr() as *mut u8;
std::slice::from_raw_parts_mut(ptr, 1048576)
};
for (i, byte) in slice.iter_mut().enumerate() {
*byte = (i % 256) as u8;
}
// 标记为已初始化
unsafe { buf.assume_init() }
}
12.8.4 指针类型转换
裸指针之间的转换是安全的(只是改变类型标签),但通过转换后的指针读写是 unsafe 的:
rust
fn pointer_casting() {
let x: u32 = 0x41424344;
let ptr_u32: *const u32 = &x;
// 指针转换是安全的
let ptr_u8: *const u8 = ptr_u32 as *const u8;
let ptr_void: *const std::ffi::c_void = ptr_u32 as *const std::ffi::c_void;
// 通过转换后的指针读取------unsafe
unsafe {
// 读取 u32 的第一个字节
let first_byte = *ptr_u8;
// 在小端系统上:0x44,在大端系统上:0x41
// 读取 4 个字节
let bytes: [u8; 4] = [
*ptr_u8,
*ptr_u8.add(1),
*ptr_u8.add(2),
*ptr_u8.add(3),
];
}
// 常见模式:trait object 的虚表指针提取
let s: &dyn std::fmt::Debug = &42i32;
// fat pointer = (data_ptr, vtable_ptr)
let (data, vtable): (*const (), *const ()) = unsafe {
std::mem::transmute(s)
};
}
12.9 Miri:MIR 解释器与 UB 检测
Miri 是 Rust 项目官方维护的 MIR 解释器。它逐条执行 MIR 指令,精确追踪每个内存分配、每次指针操作、每个借用的创建和销毁。当检测到未定义行为时,它会报告精确的错误信息。
12.9.1 Miri 的能力
bash
# 安装 Miri
rustup +nightly component add miri
# 运行项目的测试
cargo +nightly miri test
# 运行单个二进制
cargo +nightly miri run
Miri 能检测的 UB 类别:
| 类别 | 说明 | 示例 |
|---|---|---|
| 悬垂指针解引用 | 访问已释放的内存 | use-after-free |
| 未初始化内存读取 | 读取未写入的内存 | MaybeUninit::assume_init 前读取 |
| 空指针解引用 | 通过 null 指针读写 | *std::ptr::null() |
| 越界访问 | 指针偏移超出分配范围 | ptr.add(len + 1) |
| 别名违规 | 违反 Stacked Borrows / Tree Borrows | &T 和 &mut T 同时存在 |
| 无效值创建 | 创建类型不允许的值 | transmute::<u8, bool>(2) |
| 对齐错误 | 指针未正确对齐 | 将 *const u8 当 *const u64 读取 |
| 数据竞争 | 无同步的并发读写 | 多线程写 static mut |
| 内存泄漏 | 分配未释放的内存 | 忘记 dealloc |
| 双重释放 | 同一内存释放两次 | 两次 dealloc 同一指针 |
12.9.2 Miri 实战示例
rust
// 示例 1:悬垂指针------Miri 会捕获
fn dangling_pointer() {
let ptr = {
let x = 42;
&x as *const i32
// x 在这里被销毁
};
// Miri 报错:dereferencing pointer to `x` which was deallocated
// unsafe { println!("{}", *ptr); }
}
// 示例 2:别名违规------Miri 会捕获
fn aliasing_violation() {
let mut x = 42;
let ptr = &mut x as *mut i32;
let ref_x = &x; // 创建共享引用
unsafe {
*ptr = 100; // 通过裸指针修改
// Miri (Stacked Borrows): 共享引用的 "borrow tag" 已被 invalidated
}
// println!("{}", ref_x); // 使用已失效的共享引用
}
// 示例 3:无效 bool 值------Miri 会捕获
fn invalid_bool() {
let x: u8 = 2;
// Miri 报错:constructing invalid value: encountered 0x02, but expected a boolean
// let b: bool = unsafe { std::mem::transmute(x) };
}
// 示例 4:Miri 不会捕获的场景
fn miri_limitations() {
// Miri 只检测实际执行路径上的 UB
// 如果有未覆盖的代码路径包含 UB,Miri 检测不到
let condition = false;
if condition {
// 这段代码包含 UB,但 Miri 不会检测到(未执行)
unsafe { *std::ptr::null::<i32>() };
}
// Miri 的执行速度比原生代码慢 10-100 倍
// 不适合在 CI 中运行大型集成测试
}
12.9.3 Stacked Borrows 与 Tree Borrows
Miri 使用两种内存模型来检测别名违规:
Stacked Borrows(默认模型):每个内存位置维护一个"借用栈"。每次创建引用或使用指针时,会在栈上压入一个标签。当通过某个标签访问内存时,栈上该标签之上的所有标签都被 invalidated。如果后续使用了被 invalidated 的标签,Miri 报告 UB。
rust
fn stacked_borrows_demo() {
let mut x = 42;
// 借用栈状态(从底到顶):
// [Unique(x)]
let ptr = &mut x as *mut i32;
// [Unique(x), Unique(ptr)]
let ref_x = &x;
// [Unique(x), Unique(ptr), SharedRO(ref_x)]
unsafe { *ptr = 100; }
// 通过 ptr 写入------invalidate ptr 之上的所有标签
// [Unique(x), Unique(ptr)] ← ref_x 被弹出
// 现在使用 ref_x 会报 UB
// println!("{}", ref_x); // Stacked Borrows violation
}
Tree Borrows (实验性模型):比 Stacked Borrows 更宽松,使用树形结构而非栈来追踪借用关系。某些 Stacked Borrows 认为是 UB 的代码在 Tree Borrows 下是合法的。可以通过 -Zmiri-tree-borrows 标志启用。
12.9.4 在 CI 中集成 Miri
yaml
# GitHub Actions 配置
name: Miri
on: [push, pull_request]
jobs:
miri:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@nightly
with:
components: miri
- name: Run Miri
run: cargo miri test
env:
MIRIFLAGS: "-Zmiri-strict-provenance"
推荐的 Miri 标志:
-Zmiri-strict-provenance:启用严格 provenance 检查-Zmiri-symbolic-alignment-check:更严格的对齐检查-Zmiri-tree-borrows:使用 Tree Borrows 模型(实验性)-Zmiri-disable-isolation:允许访问文件系统和环境变量
12.10 未定义行为(UB)的完整分类
未定义行为(Undefined Behavior)是 unsafe 代码的终极禁区。当 UB 发生时,编译器的所有优化假设都可能失效,程序的行为变得完全不可预测------它可能看起来正常工作,可能崩溃,可能产生错误结果,也可能暴露安全漏洞。
12.10.1 数据竞争(Data Races)
数据竞争的三个条件同时满足时发生:
- 两个或更多线程并发访问同一内存位置
- 至少一个访问是写入
- 没有同步机制保护
rust
use std::thread;
fn data_race_example() {
static mut COUNTER: u64 = 0;
// UB:两个线程同时写入 static mut,没有同步
let t1 = thread::spawn(|| {
for _ in 0..1000 {
unsafe { COUNTER += 1; } // UB!
}
});
let t2 = thread::spawn(|| {
for _ in 0..1000 {
unsafe { COUNTER += 1; } // UB!
}
});
t1.join().unwrap();
t2.join().unwrap();
// 结果不确定:可能是 2000,也可能是其他值
// 更糟的是:编译器可能将循环优化为单次 += 1000
// 因为它假设没有其他线程在修改 COUNTER
}
// 正确做法:使用原子操作
use std::sync::atomic::{AtomicU64, Ordering};
static SAFE_COUNTER: AtomicU64 = AtomicU64::new(0);
fn no_data_race() {
let t1 = thread::spawn(|| {
for _ in 0..1000 {
SAFE_COUNTER.fetch_add(1, Ordering::Relaxed);
}
});
let t2 = thread::spawn(|| {
for _ in 0..1000 {
SAFE_COUNTER.fetch_add(1, Ordering::Relaxed);
}
});
t1.join().unwrap();
t2.join().unwrap();
assert_eq!(SAFE_COUNTER.load(Ordering::Relaxed), 2000);
}
12.10.2 悬垂指针(Dangling Pointers)
rust
fn dangling_pointer_categories() {
// 类型 1:Use-After-Free
let ptr = {
let v = vec![1, 2, 3];
v.as_ptr()
// v 在这里被 drop,堆内存被释放
};
// unsafe { *ptr } // UB:指针指向已释放的内存
// 类型 2:Use-After-Scope(栈变量)
let ptr2 = {
let x = 42;
&x as *const i32
};
// unsafe { *ptr2 } // UB:x 的栈帧已被回收
// 类型 3:Double Free
// let s = String::from("hello");
// let ptr = s.as_ptr();
// let len = s.len();
// std::mem::forget(s); // 阻止第一次 drop
// unsafe {
// let s2 = String::from_raw_parts(ptr as *mut u8, len, len);
// // s2 被 drop 时释放内存
// let s3 = String::from_raw_parts(ptr as *mut u8, len, len);
// // s3 被 drop 时再次释放------Double Free!
// }
// 类型 4:迭代器失效
// 在 C++ 中常见,Rust 的借用检查器在 safe 代码中防止了这一点
// 但在 unsafe 代码中仍然可能发生
}
12.10.3 无效值(Invalid Values)
Rust 的每个类型都有一组合法的值。创建不在这个集合中的值是 UB:
rust
fn invalid_values() {
// bool 只有两个合法值:0 (false) 和 1 (true)
// let b: bool = unsafe { std::mem::transmute(2u8) }; // UB
// char 必须是合法的 Unicode 标量值(0x0000..=0xD7FF 或 0xE000..=0x10FFFF)
// let c: char = unsafe { std::mem::transmute(0xD800u32) }; // UB:代理对不是合法 char
// 引用必须非空且正确对齐
// let r: &i32 = unsafe { &*std::ptr::null() }; // UB:空引用
// let r: &u64 = unsafe { &*(1usize as *const u64) }; // UB:未对齐(需要 8 字节对齐)
// enum 必须是合法的变体
// enum Fruit { Apple = 0, Banana = 1 }
// let f: Fruit = unsafe { std::mem::transmute(42u8) }; // UB:42 不是任何变体
// str 必须是合法的 UTF-8
// let s: &str = unsafe { std::str::from_utf8_unchecked(&[0xFF, 0xFE]) }; // UB
// NonZeroU32 必须非零
// use std::num::NonZeroU32;
// let nz: NonZeroU32 = unsafe { std::mem::transmute(0u32) }; // UB
}
12.10.4 别名违规(Aliasing Violations)
Rust 的别名规则是最微妙也最容易违反的约束:
rust
fn aliasing_violations() {
// 违规类型 1:同时拥有 &T 和 &mut T
let mut x = 42;
let ptr = &mut x as *mut i32;
let shared = &x;
unsafe {
*ptr = 100; // 通过裸指针修改
// 这里 shared 的假设(值不变)被违反
}
// println!("{}", shared); // 可能打印 42(优化后的缓存值)或 100
// 违规类型 2:从 &T 获取 &mut T(除非通过 UnsafeCell)
let x = 42;
let ptr = &x as *const i32 as *mut i32;
unsafe {
*ptr = 100; // UB:x 不在 UnsafeCell 中,编译器可能把 x 放在只读内存中
}
// 正确做法:使用 UnsafeCell
use std::cell::UnsafeCell;
let x = UnsafeCell::new(42);
unsafe {
*x.get() = 100; // 合法:UnsafeCell 允许通过 &self 修改
}
}
12.10.5 其他 UB 类别
rust
fn other_ub_categories() {
// 1. 栈溢出(在某些平台上是 UB)
// fn infinite_recursion() { infinite_recursion(); }
// 2. 除以零(整数除法)
// let x = 1 / 0; // Rust 会 panic(不是 UB),但在 unsafe 的 LLVM intrinsic 中是 UB
// 3. 整数溢出(在 release 模式下 wrapping,不是 UB)
// Rust 保证整数溢出不是 UB(与 C/C++ 不同)
let x: u8 = 255u8.wrapping_add(1); // 0,完全定义
// 4. 违反函数的安全前置条件
// unsafe { std::slice::from_raw_parts(std::ptr::null::<i32>(), 10) } // UB
// 5. 使用 unreachable_unchecked
// unsafe { std::hint::unreachable_unchecked() } // 如果实际到达此处,UB
// 6. 产生无效的中间值(即使不使用)
// let x: bool = unsafe { std::mem::transmute(2u8) }; // UB,即使后面不用 x
}
12.10.6 UB 的分类总结
12.11 unsafe 最佳实践
12.11.1 最小化 unsafe 表面积
rust
// 不好:整个函数都在 unsafe 中
fn bad_style(data: &[u8]) -> u32 {
unsafe {
let len = data.len();
if len < 4 {
return 0; // 这行不需要 unsafe
}
let ptr = data.as_ptr();
// 大量安全代码和 unsafe 代码混在一起...
let val = std::ptr::read_unaligned(ptr as *const u32);
val.to_le()
}
}
// 好:只在必要的地方使用 unsafe
fn good_style(data: &[u8]) -> u32 {
if data.len() < 4 {
return 0; // 安全代码在 unsafe 之外
}
// SAFETY: 我们已经检查了 data.len() >= 4,
// 所以 data.as_ptr() 指向至少 4 个字节的有效内存
let val = unsafe {
std::ptr::read_unaligned(data.as_ptr() as *const u32)
};
val.to_le() // 安全操作在 unsafe 之外
}
12.11.2 使用 SAFETY 注释文档化不变量
Rust 社区的约定是在每个 unsafe 块前添加 // SAFETY: 注释,解释为什么这个操作是安全的:
rust
impl<T> MyVec<T> {
pub fn swap(&mut self, a: usize, b: usize) {
assert!(a < self.len, "index a out of bounds");
assert!(b < self.len, "index b out of bounds");
if a == b {
return;
}
// SAFETY:
// - a 和 b 都已验证在 [0, self.len) 范围内
// - a != b,所以两个指针不会重叠
// - self.ptr.add(n) 对 n < self.len 总是指向有效的已初始化内存
// (这是 MyVec 的结构不变量)
unsafe {
let pa = self.ptr.add(a);
let pb = self.ptr.add(b);
std::ptr::swap(pa, pb);
}
}
}
12.11.3 为 unsafe fn 编写完整的 Safety 文档
rust
/// 从裸部分构建一个 `MyVec<T>`。
///
/// # Safety
///
/// 调用者必须保证以下所有条件:
///
/// - `ptr` 必须是通过全局分配器分配的(与 `Vec` 使用相同的分配器)
/// - `T` 的对齐要求不超过分配时指定的对齐
/// - `length` 必须小于等于 `capacity`
/// - `ptr` 指向的前 `length` 个元素必须已正确初始化
/// - `capacity` 必须是通过分配器分配的内存能容纳的 `T` 的数量
/// - 分配的内存大小不超过 `isize::MAX` 字节
///
/// 违反上述任何条件都会导致未定义行为。
///
/// # Examples
///
/// ```
/// use std::mem::ManuallyDrop;
///
/// let v = vec![1, 2, 3];
/// let mut v = ManuallyDrop::new(v);
/// let (ptr, len, cap) = (v.as_mut_ptr(), v.len(), v.capacity());
///
/// unsafe {
/// let rebuilt = MyVec::from_raw_parts(ptr, len, cap);
/// assert_eq!(rebuilt.as_slice(), &[1, 2, 3]);
/// }
/// ```
pub unsafe fn from_raw_parts(ptr: *mut T, length: usize, capacity: usize) -> MyVec<T> {
MyVec { ptr, len: length, cap: capacity }
}
12.11.4 使用类型系统减少 unsafe
rust
use std::num::NonZeroUsize;
// 不好:unsafe fn 前置条件需要文档记录
/// # Safety
/// `divisor` must not be zero.
unsafe fn divide_unchecked(a: u64, b: u64) -> u64 {
a / b
}
// 好:用类型系统编码约束,不需要 unsafe
fn divide_safe(a: u64, b: NonZeroUsize) -> u64 {
a / b.get() as u64
}
// 不好:用 bool 表示状态,容易出错
unsafe fn process_if_valid(data: *const u8, len: usize, is_valid: bool) {
if is_valid {
// ...
}
}
// 好:用新类型包装已验证的数据
struct ValidatedData<'a> {
data: &'a [u8],
}
impl<'a> ValidatedData<'a> {
fn new(data: &'a [u8]) -> Result<Self, ValidationError> {
validate(data)?;
Ok(ValidatedData { data })
}
fn process(&self) {
// 不需要 unsafe------验证已在构造时完成
}
}
struct ValidationError;
fn validate(_data: &[u8]) -> Result<(), ValidationError> { Ok(()) }
12.11.5 使用 Miri 测试 unsafe 代码
rust
#[cfg(test)]
mod tests {
use super::*;
// 所有涉及 unsafe 的测试都应该能在 Miri 下运行
#[test]
fn test_vec_push() {
let mut v = MyVec::new();
for i in 0..100 {
v.push(i);
}
assert_eq!(v.len(), 100);
for i in 0..100 {
assert_eq!(v.get(i), Some(&i));
}
}
#[test]
fn test_vec_drop() {
use std::sync::atomic::{AtomicUsize, Ordering};
static DROP_COUNT: AtomicUsize = AtomicUsize::new(0);
struct DropCounter;
impl Drop for DropCounter {
fn drop(&mut self) {
DROP_COUNT.fetch_add(1, Ordering::Relaxed);
}
}
DROP_COUNT.store(0, Ordering::Relaxed);
{
let mut v = MyVec::new();
for _ in 0..10 {
v.push(DropCounter);
}
}
// 验证所有元素都被正确 drop
assert_eq!(DROP_COUNT.load(Ordering::Relaxed), 10);
}
#[test]
fn test_edge_cases() {
// 测试零大小类型(ZST)
let mut v: MyVec<()> = MyVec::new();
for _ in 0..1000 {
v.push(());
}
assert_eq!(v.len(), 1000);
// 测试空 Vec 的 drop
let v: MyVec<String> = MyVec::new();
drop(v); // 不应 panic 或 UB
}
}
bash
# 运行 Miri 测试
cargo +nightly miri test
# 使用严格 provenance 检查
MIRIFLAGS="-Zmiri-strict-provenance" cargo +nightly miri test
# 检查特定测试
cargo +nightly miri test test_vec_push
12.12 高级主题:unsafe 与编译器优化
12.12.1 noalias 优化
Rust 的引用语义允许编译器进行激进的优化。&mut T 保证独占访问,编译器会在 LLVM IR 中生成 noalias 属性:
rust
// 编译器可以优化这段代码
fn add_to_both(a: &mut i32, b: &mut i32) {
*a += 1;
*b += 1;
*a += 1;
// 因为 a 和 b 保证不重叠(&mut 独占性)
// 编译器可以将两次 *a += 1 合并为 *a += 2
}
// 如果用裸指针,编译器不能做这个优化
unsafe fn add_to_both_raw(a: *mut i32, b: *mut i32) {
*a += 1;
*b += 1;
*a += 1;
// a 和 b 可能指向同一个地址
// 编译器必须保守地生成三次独立的加法
}
如果你在 unsafe 代码中违反了别名规则(比如通过裸指针创建了两个 &mut 引用指向同一地址),编译器基于 noalias 的优化会产生错误结果------这就是为什么别名违规是 UB。
12.12.2 niche 优化与 unsafe
Rust 利用类型的"niche"(无效值空间)优化枚举布局:
rust
use std::mem::size_of;
// bool 只用 1 位,剩余 7 位是 niche
assert_eq!(size_of::<bool>(), 1);
assert_eq!(size_of::<Option<bool>>(), 1); // 使用值 2 表示 None
// 引用不能为 null,所以 0 是 niche
assert_eq!(size_of::<&i32>(), 8);
assert_eq!(size_of::<Option<&i32>>(), 8); // 使用 null 表示 None
// NonZeroU64 的 0 是 niche
use std::num::NonZeroU64;
assert_eq!(size_of::<NonZeroU64>(), 8);
assert_eq!(size_of::<Option<NonZeroU64>>(), 8); // 使用 0 表示 None
如果你通过 unsafe 代码创建了一个"无效"值(比如值为 0 的 NonZeroU64),niche 优化会产生灾难性的后果------Option::is_some() 会返回 false,即使你存储了一个 Some 值。
12.12.3 Layout 不变量与 unsafe
编译器在 UnsafeOpKind 中有专门的变体来处理布局约束类型:
rust
// 编译器源码中的相关检查
ExprKind::Adt(box AdtExpr { adt_def, variant_index, .. }) => {
match self.tcx.layout_scalar_valid_range(adt_def.did()) {
(Bound::Unbounded, Bound::Unbounded) => {}
_ => self.requires_unsafe(expr.span, InitializingTypeWith),
}
}
这段编译器源码表明:如果一个 ADT 类型有布局约束(比如 NonZeroU32 的有效范围是 1..=u32::MAX),直接初始化它需要 unsafe。标准库通过 new() 方法添加运行时检查来提供安全的构造方式。
12.13 完整实战:实现一个安全的 FixedBuffer
让我们综合本章所学,实现一个固定大小的环形缓冲区,展示如何在 unsafe 之上构建安全抽象:
rust
use std::alloc::{self, Layout};
use std::marker::PhantomData;
use std::mem::MaybeUninit;
use std::ptr;
/// 固定大小的环形缓冲区
///
/// 不变量(由实现者维护):
/// 1. buf 指向 cap 个 T 大小的已分配内存(cap > 0)
/// 2. head 和 tail 始终在 [0, cap) 范围内
/// 3. buf[head..tail](模 cap)中的元素都已初始化
/// 4. len <= cap
pub struct RingBuffer<T> {
buf: *mut MaybeUninit<T>,
head: usize, // 读取位置
tail: usize, // 写入位置
len: usize,
cap: usize,
_marker: PhantomData<T>, // 告诉编译器我们"拥有" T
}
// SAFETY: RingBuffer 拥有数据的独占所有权,
// 如果 T 可以跨线程发送,RingBuffer 也可以
unsafe impl<T: Send> Send for RingBuffer<T> {}
// SAFETY: &RingBuffer<T> 只允许读取(不可变方法),
// 如果 T 的引用可以跨线程共享,RingBuffer 的引用也可以
unsafe impl<T: Sync> Sync for RingBuffer<T> {}
impl<T> RingBuffer<T> {
/// 创建指定容量的环形缓冲区
pub fn with_capacity(cap: usize) -> Self {
assert!(cap > 0, "capacity must be greater than 0");
let layout = Layout::array::<MaybeUninit<T>>(cap)
.expect("capacity overflow");
// SAFETY: layout 非零大小(cap > 0 且 MaybeUninit<T> 可能是 ZST,
// 但 Layout::array 对 ZST 仍然返回有效的 layout)
let buf = if std::mem::size_of::<T>() == 0 {
// ZST 不需要实际分配
std::ptr::NonNull::dangling().as_ptr()
} else {
let ptr = unsafe { alloc::alloc(layout) };
if ptr.is_null() {
alloc::handle_alloc_error(layout);
}
ptr as *mut MaybeUninit<T>
};
RingBuffer {
buf,
head: 0,
tail: 0,
len: 0,
cap,
_marker: PhantomData,
}
}
/// 推入一个元素。如果缓冲区已满,返回 Err
pub fn push(&mut self, value: T) -> Result<(), T> {
if self.len == self.cap {
return Err(value);
}
// SAFETY:
// - tail 在 [0, cap) 范围内(不变量 2)
// - len < cap,所以 tail 位置没有已初始化的值
// - buf 指向 cap 个元素的有效内存(不变量 1)
unsafe {
self.buf.add(self.tail).write(MaybeUninit::new(value));
}
self.tail = (self.tail + 1) % self.cap;
self.len += 1;
Ok(())
}
/// 弹出最早推入的元素
pub fn pop(&mut self) -> Option<T> {
if self.len == 0 {
return None;
}
// SAFETY:
// - head 在 [0, cap) 范围内(不变量 2)
// - len > 0,所以 head 位置有已初始化的值(不变量 3)
// - 读取后我们会更新 head 和 len,维护不变量
let value = unsafe {
self.buf.add(self.head).read().assume_init()
};
self.head = (self.head + 1) % self.cap;
self.len -= 1;
Some(value)
}
/// 查看最早推入的元素(不移除)
pub fn peek(&self) -> Option<&T> {
if self.len == 0 {
return None;
}
// SAFETY: head 位置有已初始化的值(不变量 3,len > 0)
unsafe {
Some((*self.buf.add(self.head)).assume_init_ref())
}
}
pub fn len(&self) -> usize {
self.len
}
pub fn is_empty(&self) -> bool {
self.len == 0
}
pub fn capacity(&self) -> usize {
self.cap
}
}
impl<T> Drop for RingBuffer<T> {
fn drop(&mut self) {
// Drop 所有已初始化的元素
while self.pop().is_some() {}
// 释放内存
if std::mem::size_of::<T>() != 0 {
let layout = Layout::array::<MaybeUninit<T>>(self.cap).unwrap();
// SAFETY: buf 是通过相同 layout 的 alloc 分配的
unsafe {
alloc::dealloc(self.buf as *mut u8, layout);
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_basic_operations() {
let mut rb = RingBuffer::with_capacity(3);
assert!(rb.push(1).is_ok());
assert!(rb.push(2).is_ok());
assert!(rb.push(3).is_ok());
assert!(rb.push(4).is_err()); // 满了
assert_eq!(rb.pop(), Some(1));
assert_eq!(rb.pop(), Some(2));
assert!(rb.push(4).is_ok()); // 空出位置了
assert!(rb.push(5).is_ok());
assert_eq!(rb.pop(), Some(3));
assert_eq!(rb.pop(), Some(4));
assert_eq!(rb.pop(), Some(5));
assert_eq!(rb.pop(), None);
}
#[test]
fn test_drop_elements() {
use std::sync::atomic::{AtomicUsize, Ordering};
static DROPS: AtomicUsize = AtomicUsize::new(0);
struct DropTracker;
impl Drop for DropTracker {
fn drop(&mut self) {
DROPS.fetch_add(1, Ordering::Relaxed);
}
}
DROPS.store(0, Ordering::Relaxed);
{
let mut rb = RingBuffer::with_capacity(5);
rb.push(DropTracker).unwrap();
rb.push(DropTracker).unwrap();
rb.push(DropTracker).unwrap();
// 弹出一个
rb.pop();
}
// 1 个被 pop(在 pop 时 drop)+ 2 个在 RingBuffer drop 时销毁 = 3
assert_eq!(DROPS.load(Ordering::Relaxed), 3);
}
#[test]
fn test_zst() {
let mut rb = RingBuffer::with_capacity(10);
for _ in 0..10 {
rb.push(()).unwrap();
}
assert_eq!(rb.len(), 10);
for _ in 0..10 {
assert_eq!(rb.pop(), Some(()));
}
}
}
这个实现展示了安全抽象的完整模式:
- 明确的不变量文档:在结构体定义处列出所有不变量
- 安全的公开 API :
push、pop、peek都不需要 unsafe - 最小的 unsafe 表面积:每个 unsafe 块只包含必要的操作
- SAFETY 注释:每个 unsafe 块都解释了为什么操作是安全的
- 正确的 Drop 实现:确保所有元素被析构,内存被释放
- Send/Sync 的正确实现:基于 T 的属性推导线程安全性
- 全面的测试:包括正常路径、Drop 正确性、ZST 边界情况
12.14 本章总结
unsafe 是 Rust 安全模型的完备性补充,而非安全模型的漏洞。它精确地解锁五种编译器无法静态验证的操作,同时保持所有权系统、借用检查器和类型系统的完整运行。
从编译器实现的角度看,unsafe 检查通过 UnsafetyVisitor 在 THIR 层面完成。这个访问者维护一个 SafetyContext 状态机,追踪当前代码是否处于 unsafe 上下文中。当 unsafe 操作(由 UnsafeOpKind 枚举分类)出现在 safe 上下文时,编译器报错。当 unsafe 块不包含任何 unsafe 操作时,编译器发出 unused_unsafe 警告。
标准库是 unsafe 安全抽象的典范:Vec、String、HashMap 内部大量使用裸指针和 unsafe 操作,但通过精心的不变量维护和安全 API 封装,对外提供了完全安全的接口。UnsafeCell 作为唯一允许通过共享引用获取可变指针的类型,是 Cell、RefCell、Mutex 等所有内部可变性类型的基石。
Miri 作为 MIR 解释器,是验证 unsafe 代码正确性的关键工具。它能检测悬垂指针、别名违规、无效值、数据竞争等未定义行为,应该成为每个包含 unsafe 代码的项目的 CI 标配。
编写 unsafe 代码的核心原则:最小化 unsafe 表面积、将安全检查放在 unsafe 之前、使用 // SAFETY: 注释文档化不变量、用类型系统编码约束、用 Miri 测试。遵循这些原则,你可以在享受 unsafe 带来的零成本抽象能力的同时,将风险控制在最小范围内。
理解了 unsafe 的边界和安全抽象模式,我们就有了跨越语言边界的基础。下一章将进入 FFI 的世界------看看 Rust 如何与 C 代码对话,以及编译器如何确保两种语言在 ABI 层面达成共识。