Rust 的安全保证建立在编译器的静态检查上,但有些事情编译器做不了——比如调用 C 库、操作硬件、或者实现某些零开销抽象。这时候就需要 unsafe。用好 unsafe 是 Rust 进阶的必经之路。
unsafe 的五种能力
在 unsafe 块中,你可以做五件平时不允许的事:
1. 解引用裸指针
let x = 42;
let r1 = &x as *const i32; // 创建裸指针是安全的
let r2 = x as *const i32 as *mut i32;
unsafe {
println!("r1 = {}", *r1); // 解引用裸指针需要 unsafe
}
裸指针不受借用规则约束,可以同时存在多个可变裸指针。但也意味着编译器不再保证内存安全,你得自己保证。
2. 调用 unsafe 函数
unsafe fn dangerous() {
// 这里可以做任何 unsafe 操作
}
unsafe {
dangerous();
}
3. 实现 unsafe trait
unsafe trait Foo {
fn method(&self);
}
unsafe impl Foo for i32 {
fn method(&self) {
println!("{}", self);
}
}
标准库里 Send 和 Sync 就是 unsafe trait。自动推导覆盖了大部分类型,但如果你用了裸指针等,可能需要手动实现。
4. 访问或修改可变静态变量
static mut COUNTER: u32 = 0;
fn increment() {
unsafe {
COUNTER += 1; // 访问可变静态变量需要 unsafe
}
}
可变静态变量在多线程下有数据竞争风险,所以需要 unsafe。实际项目中很少直接用,一般会用 AtomicU32 或 Mutex<T> 替代。
5. 访问 extern 函数(FFI)
extern "C" {
fn abs(input: i32) -> i32;
}
fn main() {
unsafe {
println!("abs(-3) = {}", abs(-3));
}
}
何时需要 unsafe
大部分 Rust 代码不需要 unsafe。但这些场景绕不开:
- 调用 C/C++ 库(FFI):最常见的理由
- 实现底层数据结构:比如链表、跳表等需要裸指针
- 性能关键路径:跳过边界检查(
get_unchecked) - 和操作系统交互:系统调用、内存映射
FFI 调用 C 库示例
假设要调用 C 的 strlen 函数:
use std::ffi::CString;
use std::os::raw::c_char;
extern "C" {
fn strlen(s: *const c_char) -> usize;
}
fn safe_strlen(s: &str) -> usize {
let c_string = CString::new(s).expect("CString::new failed");
unsafe {
strlen(c_string.as_ptr())
}
}
fn main() {
let len = safe_strlen("hello");
println!("length: {}", len); // 5
}
注意 safe_strlen 是安全函数,它内部使用了 unsafe,但对外暴露的接口是安全的。这就是"安全抽象"的思路。
安全抽象 unsafe 代码
unsafe 代码的核心原则:缩小 unsafe 的范围,对外提供安全接口。
标准库里到处都是这个模式。比如 Vec<T> 内部用了裸指针管理内存,但对外的 push、pop、get 都是安全的。
一个实际例子——实现一个 split_at_mut:
fn split_at_mut(slice: &mut [i32], mid: usize) -> (&mut [i32], &mut [i32]) {
let len = slice.len();
assert!(mid <= len);
let ptr = slice.as_mut_ptr();
unsafe {
(
std::slice::from_raw_parts_mut(ptr, mid),
std::slice::from_raw_parts_mut(ptr.add(mid), len - mid),
)
}
}
编译器不允许同时有两个 &mut 引用指向同一个 slice,但我们知道这两个切片是不重叠的,所以用 unsafe 绕过检查是合理的。关键是外层函数的签名是安全的,且 assert! 保证了 mid 不会越界。
常见陷阱
1. 悬垂指针
let r;
{
let x = 42;
r = &x as *const i32;
}
// x 已经被释放,r 是悬垂指针
unsafe {
println!("{}", *r); // 未定义行为!
}
裸指针没有生命周期检查,用完一定要确认指向的数据还活着。
2. 类型双关(Type Punning)
// 危险!不同类型的内存布局可能不同
let x: u32 = 42;
let y: f32 = unsafe { std::mem::transmute(x) };
transmute 是最危险的 unsafe 操作之一,它会把一种类型的字节直接重新解释为另一种类型。除非你完全理解两种类型的内存布局,否则不要用。
3. 违反不变量
let mut v = vec![1, 2, 3];
unsafe {
v.set_len(100); // 声称有100个元素,但实际只有3个
}
// 后续访问 v[50] 就是未定义行为
unsafe 代码必须维护类型的不变量。Vec 的不变量是 len <= capacity 且 len 范围内的元素都已初始化。
实践建议
- 能不用 unsafe 就不用。先想想有没有安全的替代方案。
- unsafe 块尽量小。只把确实需要 unsafe 的那一行包起来,不要整个函数都标 unsafe。
- 写好注释。每个 unsafe 块旁边都应该写清楚为什么这是安全的(Safety invariant)。
- 用
#[deny(unsafe_op_in_unsafe_fn)]。Rust 2024 edition 默认开启,在 unsafe fn 内部也要求显式 unsafe 块。 - 用 Miri 测试。
cargo +nightly miri test能检测出很多未定义行为。
unsafe 不是 Rust 的缺陷,恰恰是它的优势——在需要的时候能突破安全边界,又能通过安全抽象把风险控制在最小范围。