Unsafe Rust
到目前为止,我们讨论的所有代码都在编译时受到 Rust 内存安全保证的保护。然而,Rust 内部还隐藏着另一种语言,它不强制执行这些内存安全保证:这就是 Unsafe Rust。
为什么需要 Unsafe Rust
Unsafe Rust 存在的原因有两点:
第一,静态分析本质上是保守的。 当编译器试图判断代码是否遵守安全保证时,与其接受一些无效程序,不如拒绝一些有效程序。虽然代码可能没问题,但如果 Rust 编译器没有足够的信息来确定,它就会拒绝这些代码。在这种情况下,你可以使用 unsafe 代码告诉编译器:"相信我,我知道我在做什么。"
第二,底层计算机硬件本质上是不安全的。 如果 Rust 不允许你进行不安全的操作,你就无法完成某些任务。Rust 需要允许你进行底层系统编程,比如直接与操作系统交互,甚至编写自己的操作系统。
使用 unsafe Rust 需要自担风险:如果你不正确地使用 unsafe 代码,可能会因内存不安全而导致问题,例如空指针解引用。
Unsafe 超能力
要切换到 unsafe Rust,使用 unsafe 关键字,然后开始一个包含 unsafe 代码的新块。在 unsafe Rust 中,你可以执行五项在安全 Rust 中无法执行的操作,我们称之为 unsafe 超能力:
- 解引用原始指针
- 调用 unsafe 函数或方法
- 访问或修改可变静态变量
- 实现 unsafe trait
- 访问 union 的字段
重要理解:
unsafe并不会关闭借用检查器或禁用任何其他 Rust 安全检查- 如果在 unsafe 代码中使用引用,它仍然会被检查
unsafe关键字只是让你访问这五个不被编译器检查内存安全的特性unsafe并不意味着代码一定是危险的或肯定会有内存安全问题
解引用原始指针
Unsafe Rust 有两种类似于引用的新类型,称为原始指针(raw pointers)。与引用一样,原始指针可以是不可变的或可变的,分别写作 *const T 和 *mut T。星号不是解引用运算符,它是类型名称的一部分。
原始指针与引用的区别
与引用和智能指针不同,原始指针:
- 允许忽略借用规则,可以同时拥有不可变和可变指针,或多个指向同一位置的可变指针
- 不保证指向有效的内存
- 允许为空
- 不实现任何自动清理
通过选择不使用 Rust 强制执行的这些保证,你可以放弃保证的安全性,以换取更好的性能,或者与 Rust 保证不适用的另一种语言或硬件交互的能力。
创建原始指针
fn main() {
let mut num = 5;
// 从引用创建原始指针(不需要 unsafe)
let r1 = &num as *const i32; // 不可变原始指针
let r2 = &mut num as *mut i32; // 可变原始指针
// Rust 2021 新语法:原始借用运算符
let r3 = &raw const num; // 等同于 &num as *const i32
let r4 = &raw mut num; // 等同于 &mut num as *mut i32
}
注意:我们可以在安全代码中创建原始指针,只是不能在 unsafe 块之外解引用原始指针。
创建指向任意内存地址的原始指针
fn main() {
let address = 0x012345usize;
let r = address as *const i32; // 指向任意内存地址
}
这段代码创建了一个指向任意内存地址的原始指针。尝试使用任意内存是未定义行为:该地址可能有数据,也可能没有,编译器可能会优化代码导致没有内存访问,或者程序可能因段错误而终止。通常没有好的理由写这样的代码。
解引用原始指针
fn main() {
let mut num = 5;
let r1 = &num as *const i32;
let r2 = &mut num as *mut i32;
unsafe {
println!("r1 is: {}", *r1); // 解引用需要 unsafe
println!("r2 is: {}", *r2);
}
}
为什么原始指针有用?
- 与 C 代码交互(FFI)
- 构建借用检查器不理解的安全抽象
- 性能关键的场景
原始指针示例:同时拥有可变和不可变指针
fn main() {
let mut num = 5;
// 如果使用引用,这段代码无法编译
// 因为 Rust 不允许同时存在可变和不可变引用
// 但使用原始指针,这是允许的
let r1 = &num as *const i32; // 不可变指针
let r2 = &mut num as *mut i32; // 可变指针
unsafe {
println!("r1: {}", *r1);
println!("r2: {}", *r2);
}
}
警告:这可能导致数据竞争,请小心使用!
调用 Unsafe 函数
第二种 unsafe 操作是调用 unsafe 函数。Unsafe 函数看起来与常规函数完全相同,但在定义前有一个额外的 unsafe 关键字。
基本 unsafe 函数
unsafe fn dangerous() {
// unsafe 函数体
}
fn main() {
// 必须在 unsafe 块中调用
unsafe {
dangerous();
}
}
如果在 unsafe 块之外调用 unsafe 函数,会得到编译错误。
在 unsafe 函数中使用 unsafe 操作
从 Rust 2024 Edition 开始,unsafe_op_in_unsafe_fn lint 默认警告。这意味着在 unsafe fn 内部调用其他 unsafe 操作时,仍然需要显式的 unsafe {} 块。
为什么需要这个变更?
虽然 unsafe fn 本身表明函数有安全隐患,但函数内部的 unsafe 操作应该被明确标记,这样:
- 更容易审查:可以清楚地看到哪些操作是 unsafe 的
- 更安全:避免意外遗漏 unsafe 操作
- 更好的文档性:代码意图更清晰
// Rust 2024+ 推荐写法
unsafe fn dangerous_work(ptr: *const i32) -> i32 {
// 显式标记 unsafe 操作
unsafe {
*ptr // 解引用原始指针
}
}
// Rust 2024 之前的写法(现在会产生警告)
// unsafe fn old_style(ptr: *const i32) -> i32 {
// *ptr // 警告:需要在 unsafe 块中
// }
unsafe fn dangerous_work(ptr: *const i32) -> i32 {
// unsafe 函数内部可以直接进行 unsafe 操作
// 但 Rust 2024+ 建议添加显式的 unsafe 块
unsafe { *ptr }
}
fn main() {
let x = 42;
let ptr = &x as *const i32;
unsafe {
let result = dangerous_work(ptr);
println!("Result: {}", result);
}
}
创建安全抽象
仅仅因为函数包含 unsafe 代码并不意味着我们需要将整个函数标记为 unsafe。实际上,将 unsafe 代码包装在安全函数中是一种常见的抽象模式。
示例:实现 split_at_mut
use std::slice;
// 这是一个安全函数,内部使用 unsafe 代码
fn split_at_mut(values: &mut [i32], mid: usize) -> (&mut [i32], &mut [i32]) {
let len = values.len();
let ptr = values.as_mut_ptr();
assert!(mid <= len);
unsafe {
(
slice::from_raw_parts_mut(ptr, mid),
slice::from_raw_parts_mut(ptr.add(mid), len - mid),
)
}
}
fn main() {
let mut vector = vec![1, 2, 3, 4, 5, 6];
let (left, right) = split_at_mut(&mut vector, 3);
println!("Left: {:?}", left); // [1, 2, 3]
println!("Right: {:?}", right); // [4, 5, 6]
}
解释:
as_mut_ptr()获取切片的原始指针slice::from_raw_parts_mut是 unsafe 函数,从原始指针创建切片ptr.add(mid)是 unsafe 方法,进行指针运算- 整个函数是安全的,因为我们在内部正确使用了 unsafe 代码
FFI:调用外部函数
Rust 支持外部函数接口(Foreign Function Interface,FFI),允许与其他语言交互:
从 Rust 2024 Edition 开始,extern 块必须使用 unsafe 关键字标记:
// Rust 2024+: extern 块需要 unsafe
unsafe extern "C" {
fn abs(input: i32) -> i32;
}
这是因为在 extern 块中声明的外部函数的正确性无法由 Rust 编译器验证,调用这些函数可能导致未定义行为。
迁移提示:运行 cargo fix --edition 可以自动添加 unsafe 关键字。
// 声明外部 C 函数
unsafe extern "C" {
fn abs(input: i32) -> i32;
}
fn main() {
unsafe {
println!("C 的 abs(-3) 结果: {}", abs(-3));
}
}
"C" 定义了外部函数使用的应用程序二进制接口(ABI)。C ABI 是最常见的,遵循 C 编程语言的 ABI。
从其他语言调用 Rust 函数
// 允许其他语言调用此 Rust 函数
#[no_mangle]
pub extern "C" fn call_from_c() {
println!("从 C 调用了 Rust 函数!");
}
从 Rust 2024 Edition 开始,以下属性必须使用 unsafe(...) 包装:
#[export_name = "..."]#[link_section = "..."]#[no_mangle]
这些属性可能影响链接器行为或与其他语言的交互,不当使用可能导致未定义行为。
// Rust 2024+: 这些属性需要 unsafe
#[unsafe(export_name = "my_function")]
#[unsafe(no_mangle)]
pub extern "C" fn my_function() {}
#[no_mangle]告诉 Rust 编译器不要修改函数名extern "C"使函数使用 C ABI
访问可变静态变量
在 Rust 中,全局变量称为静态变量。
不可变静态变量
static HELLO_WORLD: &str = "Hello, world!";
fn main() {
println!("Message: {}", HELLO_WORLD);
}
访问不可变静态变量是安全的。
静态变量与常量的区别
| 特性 | 常量 (const) | 静态变量 (static) |
|---|---|---|
| 内存位置 | 可以被复制 | 固定地址 |
| 可变性 | 永远不可变 | 可以是可变的 |
| 大小 | 编译时内联 | 有固定内存位置 |
可变静态变量
访问或修改可变静态变量是 unsafe 的:
static mut COUNTER: u32 = 0;
fn add_to_count(inc: u32) {
unsafe {
COUNTER += inc;
}
}
fn main() {
add_to_count(3);
unsafe {
println!("COUNTER: {}", COUNTER);
}
}
为什么这是 unsafe 的?
如果多个线程访问 COUNTER,很可能会导致数据竞争。拥有全局可访问的可变数据很难确保没有数据竞争,这就是 Rust 将可变静态变量视为 unsafe 的原因。
从 Rust 2024 Edition 开始,对 static mut 的引用会产生编译错误。之前这只是警告,现在是硬错误。
static mut DATA: i32 = 0;
// Rust 2024: 这是错误的!
// let ref_to_static = unsafe { &DATA }; // 编译错误
// 正确做法:使用原始指针
let ptr_to_static = unsafe { &raw const DATA }; // Rust 2024+
// 或者使用 std::ptr::addr_of!
let ptr = unsafe { std::ptr::addr_of!(DATA) };
替代方案:推荐使用 std::sync::atomic 类型或 std::sync::Mutex 来实现线程安全的全局状态:
use std::sync::atomic::{AtomicU32, Ordering};
// 使用原子类型替代 static mut
static COUNTER: AtomicU32 = AtomicU32::new(0);
fn increment() {
COUNTER.fetch_add(1, Ordering::SeqCst);
}
fn main() {
increment();
println!("Counter: {}", COUNTER.load(Ordering::SeqCst));
}
更好的替代方案:使用第 16 章讨论的并发技术和线程安全智能指针,让编译器检查跨线程的数据访问是否安全。
实现 Unsafe Trait
当 trait 的至少一个方法有编译器无法验证的不变量时,该 trait 就是不安全的。
// 声明 unsafe trait
unsafe trait Foo {
fn method(&self);
}
// 实现 unsafe trait 也需要 unsafe
unsafe impl Foo for i32 {
fn method(&self) {
println!("Foo method called on {}", self);
}
}
fn main() {
let x = 42;
// 使用 trait 的代码是安全的
unsafe { x.method(); }
}
Send 和 Sync Trait
Send 和 Sync 是 Rust 中重要的 marker trait:
- Send:类型可以安全地在线程间转移所有权
- Sync:类型可以安全地从多个线程访问(
&T可以发送到其他线程)
如果一个类型完全由 Send 和 Sync 类型组成,编译器会自动为其实现这些 trait。如果类型包含非 Send 或非 Sync 的类型(如原始指针),并且你想将其标记为 Send 或 Sync,必须使用 unsafe:
struct MyBox<T>(T);
// 原始指针不是 Send/Sync,但如果我们确定它是安全的
unsafe impl<T: Send> Send for MyBox<T> {}
unsafe impl<T: Sync> Sync for MyBox<T> {}
通过使用 unsafe impl,我们承诺会维护编译器无法验证的不变量。
访问 Union 字段
Union 类似于结构体,但在特定实例中一次只使用一个声明的字段。Union 主要用于与 C 代码中的 union 交互。
union IntOrFloat {
i: i32,
f: f32,
}
fn main() {
let mut u = IntOrFloat { i: 42 };
unsafe {
println!("Integer: {}", u.i);
// 写入不同的字段
u.f = 3.14;
println!("Float: {}", u.f);
// 注意:现在读取 u.i 是未定义行为!
}
}
访问 union 字段是 unsafe 的,因为 Rust 无法保证当前存储在 union 实例中的数据类型。
何时使用 Unsafe
使用 unsafe 来执行上述五种操作(超能力)并没有错,也不被反对。但要正确编写 unsafe 代码更加棘手,因为编译器无法帮助维护内存安全。
何时需要使用 unsafe:
- 与 C 代码交互:调用 C 库函数
- 性能优化:在关键路径上避免边界检查
- 实现底层抽象:如智能指针、容器
- 嵌入式开发:直接访问硬件
- 实现并发原语:如互斥锁、通道
最佳实践
1. 保持 unsafe 块最小化
// 不推荐:整个函数都是 unsafe
unsafe fn bad_example() {
// 很多安全代码...
let x = 5;
let y = 10;
// 只有一行需要 unsafe
*std::ptr::null::<i32>(); // 这会崩溃
}
// 推荐:只包围必要的代码
fn good_example() {
// 安全代码
let x = 5;
let y = 10;
// 只在需要时使用 unsafe
unsafe {
// unsafe 操作
}
}
2. 提供安全抽象
// 内部使用 unsafe,但提供安全 API
pub struct SafeWrapper {
ptr: *mut i32,
}
impl SafeWrapper {
pub fn new(value: i32) -> Self {
let boxed = Box::new(value);
Self {
ptr: Box::into_raw(boxed),
}
}
pub fn get(&self) -> i32 {
unsafe { *self.ptr }
}
pub fn set(&mut self, value: i32) {
unsafe {
*self.ptr = value;
}
}
}
impl Drop for SafeWrapper {
fn drop(&mut self) {
unsafe {
drop(Box::from_raw(self.ptr));
}
}
}
3. 文档化安全不变量
/// # Safety
///
/// 调用者必须确保:
/// - `ptr` 指向有效内存
/// - 内存已正确初始化
/// - 在此函数返回前,内存不会被释放
pub unsafe fn read_value(ptr: *const i32) -> i32 {
*ptr
}
4. 使用工具检查 unsafe 代码
- Miri:Rust 的未定义行为检测工具
- Clippy:linter,可以检测某些 unsafe 问题
- loom:并发代码测试工具
# 使用 Miri 检测未定义行为
cargo +nightly miri test
小结
本章我们学习了:
- Unsafe Rust 存在的原因:静态分析的局限性和底层系统编程需求
- 五种 unsafe 超能力:
- 解引用原始指针
- 调用 unsafe 函数
- 访问可变静态变量
- 实现 unsafe trait
- 访问 union 字段
- 原始指针:
*const T和*mut T - FFI:与其他语言交互
- 最佳实践:保持 unsafe 块最小化、提供安全抽象
记住:使用 unsafe 不是坏事,但需要格外小心。unsafe 关键字的存在是为了帮助你追踪潜在问题的来源。
练习
- 编写一个函数,使用原始指针交换两个变量的值
- 实现一个简单的
MyBox<T>智能指针类型 - 使用 FFI 调用一个 C 标准库函数
- 创建一个使用可变静态变量的计数器,并考虑如何在多线程环境中安全使用
参考资料
- Rust 官方文档 - Unsafe Rust
- Rustonomicon - Unsafe Rust 深入指南
- Rust Reference - Unsafe