Rust: life-time
1 · Rust life-time#
1.1 · Borrow Checker#
本质:XOR 约束——任何实体要么可变(mutable),要么可别名(aliasable),二者不可兼得。这让编译器无需假设内存被别名引用,从而获得更多优化空间。
Borrow Checker 还强制执行对象的 lifetime:如果对象 A 持有对象 B 的非拥有引用(non-owning reference),那么 A 不能比 B 活得更久,否则 A 就持有了一个悬垂引用。
1.2 · 用 unsafe 演示 use-after-free#
用 raw pointer 构建一个简单的 fat pointer 结构体 DataBuffer:
struct DataBuffer {
data: *const f32,
length: usize,
}
impl DataBuffer {
fn new(sl: &[f32]) -> Self {
Self {
data: sl.as_ptr(),
length: sl.len(),
}
}
}
new()接受一个 slice 引用,提取其 raw pointer,但不拥有底层内存- 编译器对 raw pointer 不做 lifetime 跟踪,也不做 bounds checking
当在一个内部 scope 里重新创建 buf 并赋值给外部的 dbuffer,scope 结束后 buf 被 drop,dbuffer 就持有了悬垂指针——经典的 use-after-free:
let mut dbuffer = DataBuffer::new(&buf);
{
let buf = vec![0.0, 1.0, 2.0, 3.0];
dbuffer = DataBuffer::new(&buf);
} // buf dropped, dbuffer 现在是悬垂指针
assert_eq!(3.0, dbuffer[3]); // UB!
1.3 · 用 Lifetime 修复#
1.3.1 · 1. 给 struct 标注 lifetime#
use std::marker::PhantomData;
struct DataBuffer<'a> {
data: *const f32,
length: usize,
_marker: PhantomData<&'a f32>,
}
- 因为 raw pointer 不携带 lifetime 信息,编译器会报
E0392: lifetime parameter 'a is never used - 用
PhantomData<&'a f32>作为零大小标记(zero-sized marker),告诉编译器这个 struct 逻辑上借用了'a生命周期的数据
1.3.2 · 2. 在构造函数中绑定 lifetime#
impl<'a> DataBuffer<'a> {
fn new(sl: &'a [f32]) -> Self {
Self {
data: sl.as_ptr(),
length: sl.len(),
_marker: PhantomData,
}
}
}
sl: &'a [f32]将参数的 lifetime 与 struct 的'a绑定- 编译器现在知道
DataBuffer不能比传入的 slice 引用活得更久
1.3.3 · 3. Index trait 用匿名 lifetime#
impl Index<usize> for DataBuffer<'_> {
type Output = f32;
fn index(&self, i: usize) -> &Self::Output {
assert!(i < self.length);
unsafe { &*self.data.add(i) }
}
}
1.3.4 · 效果
加上 lifetime 后,之前的 use-after-free 代码直接编译失败:
error[E0597]: `buf` does not live long enough
1.4 · 核心要点
- PhantomData pattern 是 Rust 标准库中的常见惯用法,用于让编译器追踪 raw pointer 背后的 lifetime
- Lifetime 标注不改变运行时行为,只是给编译器提供静态分析信息
- C 编译器信任你(会愉快地让你搬起石头砸自己的脚),Rust 编译器则通过 lifetime 系统拒绝编译不安全的代码