第十章 Unsafe Rust (Unsafe Rust)
一、导读
1.1 本章学习目标
理解 Unsafe Rust 的设计哲学——它是安全抽象的底层基础,而非鼓励不安全的编程
掌握 Unsafe Rust 的五种"超级能力"及其使用场景
理解裸指针(Raw Pointers)的类型、创建和解引用规则
掌握 FFI(Foreign Function Interface)的调用约定,能够与 C 语言库互操作
理解 unsafe 块的作用域规则和安全边界的封装方法
掌握 UnsafeCell<T> 的工作原理及其作为内部可变性基础的作用
了解编写安全的 Unsafe 抽象的最佳实践和验证方法
能够阅读和理解标准库中的 unsafe 代码
1.2 为什么本章如此重要
Unsafe Rust 是 Rust 语言中最具争议也最被误解的特性。许多开发者将 unsafe 等同于"不安全"或"危险",从而完全回避它。然而事实恰恰相反——Rust 标准库中大量的核心抽象(Vec<T>、HashMap、Box<T>、Rc<T>、Cell<T> 等)都是在 unsafe 块中实现的。没有 unsafe,Rust 就无法与操作系统交互、无法实现高性能的数据结构、无法与其他语言的库互操作。
理解 Unsafe Rust 的关键在于区分两个层次:安全抽象(Safe Abstraction)和不安全实现(Unsafe Implementation)。Vec<T> 对外是安全的——你可以随意使用它而不会导致内存错误。但它的内部实现使用了裸指针和手动内存管理,这些操作在 unsafe 块中完成。Vec 的作者承担了验证安全性的责任,使得使用者不需要理解底层的 unsafe 细节。
本章将系统地讲解 Unsafe Rust 的五种超级能力:解引用裸指针、调用 unsafe 函数或方法、访问或修改可变静态变量、实现 unsafe trait、访问 union 的字段。我们还将深入探讨 FFI 的使用、UnsafeCell 的工作原理,以及编写安全 unsafe 抽象的最佳实践。
学习本章内容的目的不是鼓励你在日常编程中使用 unsafe,而是让你理解 Rust 安全抽象的底层机制,能够阅读和理解标准库和第三方库中的 unsafe 代码,并在确实需要时能够正确地编写 unsafe 代码。
1.3 核心问题引导
Unsafe Rust 的五种超级能力分别是什么?为什么需要它们?
裸指针(*const T 和 *mut T)与引用(&T 和 &mut T)有什么本质区别?
FFI 中如何保证 Rust 和 C 之间的数据类型兼容?repr(C) 的作用是什么?
UnsafeCell<T> 为什么是 Rust 内部可变性的基础?它与 Cell<T> 和 RefCell<T> 是什么关系?
如何编写一个"安全的 unsafe 抽象"?需要满足哪些条件?
Miri 工具如何帮助验证 unsafe 代码的正确性?
二、核心概念详解
10.1 Unsafe 的五种超级能力
在 unsafe 块中,Rust 允许以下五种在安全 Rust 中被禁止的操作:
解引用裸指针(Dereference a raw pointer)
调用 unsafe 函数或方法(Call an unsafe function or method)
访问或修改可变静态变量(Access or modify a mutable static variable)
实现 unsafe trait(Implement an unsafe trait)
访问 union 的字段(Access fields of a union)
unsafe {
// 1. 解引用裸指针
let raw_ptr: *const i32 = &42;
let value = *raw_ptr;
// 2. 调用 unsafe 函数
// dangerous();
// 3. 修改可变静态变量
// static mut COUNTER: u32 = 0;
// COUNTER += 1;
}unsafe 不意味着不安全:unsafe 块只是解除了编译器的某些检查。编写 unsafe 代码的开发者有责任确保代码实际上不会导致未定义行为(Undefined Behavior, UB)。如果 unsafe 代码违反了其文档中规定的安全条件(Safety Contract),则整个程序的行为都是未定义的——即使安全代码部分也可能出错。
10.2 裸指针
裸指针分为 *const T(不可变裸指针)和 *mut T(可变裸指针)。与引用不同,裸指针:
- 可以指向空地址(null)
- 不保证指向有效的内存
- 不参与借用检查,编译器不追踪其生命周期
- 可以任意读写(
*mut T)
let mut num = 5;
// 从引用创建裸指针(安全操作)
let r1 = &num as *const i32;
let r2 = &mut num as *mut i32;
// 直接创建裸指针(可能指向无效内存)
let address = 0x7fff5d2a2c3c as *const i32;
unsafe {
println!("r1: {}", *r1);
println!("r2: {}", *r2);
// println!("address: {}", *address); // 可能段错误
}裸指针的使用场景:裸指针主要用于实现底层数据结构(如链表、B+树)、FFI 交互和与 C 库的互操作。在安全抽象的内部,裸指针是管理内存和实现高性能数据结构的基本工具。
10.3 FFI:与 C 语言互操作
FFI(Foreign Function Interface)允许 Rust 调用 C 函数,也允许 C 调用 Rust 函数。
调用 C 函数:
// 声明外部 C 函数
extern "C" {
fn abs(input: i32) -> i32;
fn printf(format: *const i8, ...) -> i32;
}
fn main() {
unsafe {
println!("abs(-3) = {}", abs(-3));
}
}导出 Rust 函数供 C 调用:
#[no_mangle]
pub extern "C" fn add(a: i32, b: i32) -> i32 {
a + b
}`repr(C)` 属性:确保 Rust 结构体的内存布局与 C 兼容:
#[repr(C)]
struct Point {
x: f64,
y: f64,
}
extern "C" {
fn process_point(p: *const Point) -> f64;
}bindgen 工具:bindgen 可以自动从 C 头文件生成 Rust FFI 绑定,大大减少了手动编写 FFI 声明的工作量。
10.4 可变静态变量
Rust 允许定义全局可变变量(static mut),但访问它们是 unsafe 的:
static mut GLOBAL_COUNTER: u32 = 0;
fn increment() {
unsafe {
GLOBAL_COUNTER += 1;
}
}
fn get_count() -> u32 {
unsafe { GLOBAL_COUNTER }
}线程安全问题:static mut 不是线程安全的。在多线程环境中访问可变静态变量需要额外的同步机制。通常推荐使用 AtomicU32 或 Mutex 替代 static mut。
use std::sync::atomic::{AtomicU32, Ordering};
static COUNTER: AtomicU32 = AtomicU32::new(0);
fn increment() {
COUNTER.fetch_add(1, Ordering::SeqCst);
}10.5 UnsafeCell 与内部可变性
UnsafeCell<T> 是 Rust 中所有内部可变性(Interior Mutability)的基础。它是唯一一个被编译器特殊对待的类型——&UnsafeCell<T> 允许在共享引用的同时修改内部数据。
use std::cell::UnsafeCell;
struct SharedCounter {
value: UnsafeCell<i32>,
}
// 安全地封装 UnsafeCell
impl SharedCounter {
fn new(value: i32) -> Self {
SharedCounter {
value: UnsafeCell::new(value),
}
}
fn increment(&self) {
unsafe {
*self.value.get() += 1;
}
}
fn get(&self) -> i32 {
unsafe { *self.value.get() }
}
}
// 注意:上面的实现不是线程安全的!
// 需要额外的同步机制Cell<T>、RefCell<T> 和 OnceCell<T> 都是基于 UnsafeCell<T> 构建的安全抽象。它们在运行时检查借用规则,将编译期检查转为运行期检查。
10.6 编写安全的 Unsafe 抽象
编写 unsafe 代码的核心原则是:在安全边界内封装所有不安全操作。
use std::alloc::{alloc, dealloc, Layout};
use std::ptr;
struct SimpleVec<T> {
ptr: *mut T,
len: usize,
capacity: usize,
}
impl<T> SimpleVec<T> {
fn new() -> Self {
SimpleVec {
ptr: ptr::null_mut(),
len: 0,
capacity: 0,
}
}
fn push(&mut self, value: T) {
if self.len == self.capacity {
self.grow();
}
unsafe {
ptr::write(self.ptr.add(self.len), value);
}
self.len += 1;
}
fn grow(&mut self) {
let new_capacity = if self.capacity == 0 { 4 } else { self.capacity * 2 };
let new_layout = Layout::array::<T>(new_capacity).unwrap();
let new_ptr = unsafe {
if self.capacity == 0 {
alloc(new_layout) as *mut T
} else {
let old_layout = Layout::array::<T>(self.capacity).unwrap();
std::alloc::realloc(self.ptr as *mut u8, old_layout, new_layout.size()) as *mut T
}
};
if new_ptr.is_null() {
panic!("内存分配失败");
}
self.ptr = new_ptr;
self.capacity = new_capacity;
}
fn get(&self, index: usize) -> Option<&T> {
if index < self.len {
Some(unsafe { &*self.ptr.add(index) })
} else {
None
}
}
}
impl<T> Drop for SimpleVec<T> {
fn drop(&mut self) {
if self.capacity > 0 {
unsafe {
for i in 0..self.len {
ptr::drop_in_place(self.ptr.add(i));
}
let layout = Layout::array::<T>(self.capacity).unwrap();
dealloc(self.ptr as *mut u8, layout);
}
}
}
}三、深入分析与讨论
3.1 Unsafe 的正确性验证
验证 unsafe 代码的正确性是 Rust 开发中最具挑战性的任务之一。Rust 定义了严格的"未定义行为"(UB)规则,unsafe 代码必须避免所有 UB:
- 不能解引用空指针或悬垂指针
- 不能创建无效的引用(如引用已释放的内存)
- 不能修改不可变数据
- 不能产生数据竞争
- 不能使用无效的枚举值
Miri 是 Rust 的中间表示(MIR)解释器,可以在测试中检测许多 unsafe 代码的错误。通过 cargo +nightly miri test 运行测试,Miri 可以发现内存泄漏、悬垂指针、数据竞争等问题。
3.2 Unsafe 的替代方案
在许多场景下,可以使用安全代码替代 unsafe:
- 使用
AtomicPtr替代裸指针操作 - 使用
Box和Vec替代手动内存管理 - 使用
RefCell替代UnsafeCell的直接使用 - 使用
bindgen或cxx替代手写 FFI
只有在这些安全抽象无法满足需求时(如实现新的数据结构、与 C 库交互),才应使用 unsafe。
3.3 Unsafe 代码的文档规范
每个 unsafe 函数和 unsafe 块都应该有清晰的安全条件文档。安全条件描述了调用者必须保证的前提条件,以及函数保证的行为。
四、实践案例
4.1 实现简单的链表
使用裸指针实现一个单向链表。
use std::ptr;
struct Node<T> {
value: T,
next: *mut Node<T>,
}
struct LinkedList<T> {
head: *mut Node<T>,
len: usize,
}
impl<T> LinkedList<T> {
fn new() -> Self {
LinkedList {
head: ptr::null_mut(),
len: 0,
}
}
fn push_front(&mut self, value: T) {
let new_node = Box::into_raw(Box::new(Node {
value,
next: self.head,
}));
self.head = new_node;
self.len += 1;
}
fn pop_front(&mut self) -> Option<T> {
if self.head.is_null() {
return None;
}
unsafe {
let old_head = Box::from_raw(self.head);
self.head = old_head.next;
self.len -= 1;
Some(old_head.value)
}
}
fn len(&self) -> usize {
self.len
}
fn is_empty(&self) -> bool {
self.len == 0
}
}
impl<T> Drop for LinkedList<T> {
fn drop(&mut self) {
while self.pop_front().is_some() {}
}
}
fn main() {
let mut list = LinkedList::new();
list.push_front(3);
list.push_front(2);
list.push_front(1);
println!("长度: {}", list.len());
println!("弹出: {:?}", list.pop_front());
println!("弹出: {:?}", list.pop_front());
println!("长度: {}", list.len());
}4.2 调用 C 标准库
通过 FFI 调用 C 标准库的字符串处理函数。
use std::ffi::{CStr, CString};
use std::os::raw::c_char;
extern "C" {
fn strlen(s: *const c_char) -> usize;
fn strcat(dest: *mut c_char, src: *const c_char) -> *mut c_char;
}
fn rust_strlen(s: &str) -> usize {
let c_string = CString::new(s).unwrap();
unsafe { strlen(c_string.as_ptr()) }
}
fn main() {
let s = "Hello, Unsafe Rust!";
println!("Rust len: {}", s.len());
println!("C strlen: {}", rust_strlen(s));
let c_str = unsafe {
let ptr = b"Hello from C\0" as *const u8 as *const c_char;
CStr::from_ptr(ptr)
};
println!("C string: {:?}", c_str);
}五、常见误区与注意事项
认为 unsafe 就是"不检查":unsafe 不是关闭所有检查,只是解除了编译器的部分安全检查。开发者仍然需要确保代码的正确性,否则会导致未定义行为。
在 unsafe 块中编写过多代码:unsafe 块应该尽可能小,只包含真正需要 unsafe 的操作。将安全检查放在 unsafe 块之外,确保只有满足安全条件时才会进入 unsafe 块。
忽略对齐(Alignment)要求:裸指针操作必须考虑内存对齐。未对齐的访问在某些架构上会导致硬件异常,在 Rust 中是未定义行为。
忘记实现 Drop:手动管理的内存必须在 Drop 实现中正确释放,否则会导致内存泄漏。同时要注意"双重释放"问题。
混淆 `*const T` 与 `&T`:裸指针不保证指向有效内存,也不保证生命周期。从裸指针创建引用时必须确保这些条件满足。
六、本章小结
本章深入讲解了 Unsafe Rust 的设计理念和实践方法。Unsafe Rust 提供了五种超级能力:解引用裸指针、调用 unsafe 函数、访问可变静态变量、实现 unsafe trait 和访问 union 字段。这些能力是实现底层数据结构、FFI 交互和性能关键代码的基础。UnsafeCell<T> 是所有内部可变性抽象的底层基础。编写 unsafe 代码的核心原则是在安全边界内封装所有不安全操作,确保对外暴露的 API 是安全的。Miri 工具可以帮助验证 unsafe 代码的正确性。
七、思考题与练习
编写一个函数,使用裸指针交换两个变量的值。解释为什么这个操作需要 unsafe。
实现一个 MyBox<T> 智能指针,使用裸指针管理堆内存。实现 Deref、Drop 和 new 方法。
通过 FFI 调用 C 标准库的 qsort 函数对 Rust 数组排序。注意回调函数的定义方式。
解释 UnsafeCell<T> 为什么是 Cell、RefCell 和 Mutex 的基础。画出它们的关系图。
实现一个线程安全的 UnsafeCell 包装器,使用 AtomicBool 实现运行时借用检查。
研究 std::mem::transmute 的作用和危险性。给出一个合理使用 transmute 的场景。
使用 Miri 工具检测一个有 Bug 的 unsafe 程序。描述 Miri 发现了什么问题。
阅读 Vec<T> 的源码(alloc/src/vec/mod.rs),找出其中使用 unsafe 的部分,解释为什么这些操作必须是 unsafe 的。