Rust中Drop检查 与 PhantomData
Drop checker check 了什么¶
到目前为止我们提到生命周期的长短时,指的都是非严格的关系。也就是说,当我们写'a: 'b
的时候,'a
其实也可以和'b
一样长。乍一看,这一点没什么意义。本来也不会有两个东西被同时销毁的,不是吗?我们去掉下面的let
表达式的语法糖看看
每一个都创建了自己的作用域,可以很清楚地看出来一个在另一个之前被销毁。但是,如果是下面这样的呢
有哪一个比另一个存活更长吗?答案是,没有,没有哪个严格地比另一个长。当然,x和y中肯定有一个比另一个先销毁,但是销毁的顺序是不确定的。并非只有元组是这样,复合结构体从Rust 1.0开始就不会保证它们的销毁顺序
我们已经清楚了元组和结构体这种内置复合类型的行为了。那么Vec这样的类型又是什么样的呢?Vec必须通过标准库代码手动销毁它的元素。通常来说,所有实现了Drop的类型在临死前都有一次回光返照的机会。所以,对于实现了Drop的类型,编译器没有充分的理由判断它们的内容的实际销毁顺序
可是我们为什么要关心这个?因为如果系统不够小心,就可能搞出来悬垂指针。考虑下面这个简单的程序, 这段程序是正确且可以正常编译的。days
并不严格地比inspector
存活得更长,但这没什么关系。只要inspector
还存活着,days
就一定也活着
struct Inspector<'a>(&'a u8);
fn main() {
let (inspector, days);
days = Box::new(1);
inspector = Inspector(&days);
}
可如果我们添加一个析构函数,程序就不能编译了!
struct Inspector<'a>(&'a u8);
impl<'a> Drop for Inspector<'a> {
fn drop(&mut self) {
println!("再过{}天我就退休了!", self.0);
}
}
fn main() {
let (inspector, days);
days = Box::new(1);
inspector = Inspector(&days);
// 如果days碰巧先被销毁了
// 那么当销毁Inspector的时候,它会读取被释放的内存
}
实现Drop
使得Inspector
可以在销毁前执行任意的代码。一些通常认为和它生命周期一样长的类型可能实际上比它先销毁,而这会有潜在的问题.
有意思的是,只有泛型需要考虑这个问题。如果不是泛型的话,那么唯一可用的生命周期就是 'static
为了使泛型类型能够正确地实现drop, 它的泛型参数必须严格地超过他自己本身
即对于如下形式的类型定义, 即泛型 T 的 lifetime 必须长过 S
S 是 使用了泛型的类型, 若 S 实现了 Drop trait, 那么 rust drop checker 要求 泛型参数(即 'a, T) 的 lifetime 必须要超过它, 即 超过 S的实例
rust drop checker 会检查 'a outlive 's, 这里 's 表示着 s 的 lifetime, 即 'a: 's 要成立. 同样 T: 's 也要成立!
为什么还需要 PhantomData?¶
其实到了这里, 对 PhantomData 与 Drop checker 感觉还有点迷糊, 比如如下 BugBox 实现:
struct BugBox<T> {
d: *const T,
// _marker: std::marker::PhantomData<T>,
}
impl<T> BugBox<T> {
fn new(val: T) -> BugBox<T> {
let p = unsafe {alloc(Layout::new::<T>()) as *mut _};
unsafe {
ptr::write(p, val);
}
BugBox {
d: p,
// _marker: Default::default(),
}
}
}
impl<T> Drop for BugBox<T> {
fn drop(&mut self) {
let d = unsafe {ptr::read(self.d)};
std::mem::drop(d);
unsafe {dealloc(self.d as *mut _, Layout::new::<T>());}
}
}
rust drop checker 会要求 T outlive BugBox, 有没有 PhantomData 无所谓啊! 确实, 上面 BugBox 实现确实目前来说没有问题. 但当与如下类型结合使用时, 就会产生不应该的编译错误.
struct Safe1<'a>(&'a str, &'static str);
unsafe impl<#[may_dangle] 'a> Drop for Safe1<'a> {
fn drop(&mut self) {
println!("Safe1(_, {}) knows when *not* to inspect.", self.1);
}
}
struct SafeS<'a> {
b: Option<BugBox<Safe1<'a>>>,
s: String,
}
pub fn main() {
let mut ss = SafeS {
b: None,
s: "".to_string(),
};
ss.b = Some(BugBox::new(Safe1(&ss.s, "")));
}
如上代码人肉判断是没有问题的, 但由于 BugBox 实现了 Drop, rust drop checker 要求 Safe<'a>
outlive BugBox, 而实际上这一要求又不满足导致了编译报错. 但人肉判断, 这里 ‘Safe<'a>
outlive BugBox’ 并不是必须的, 因为 Safe 在其 drop 中并未访问 'a
. 所以对 BugBox 做了第一版修改, 使用 may_dangle
attribute:
unsafe impl<#[may_dangle] T> Drop for BugBox<T> {
fn drop(&mut self) {
let d = unsafe {ptr::read(self.d)};
std::mem::drop(d);
unsafe {dealloc(self.d as *mut _, Layout::new::<T>());}
}
}
但这样又引入了另外一个问题, 由于 T 标记了 may_dangle, 因此 rust drop checker 不再要求 T outlive BugBox, 所以可以写出如下会导致 use-after-free 的代码:
struct Safe<'a>(&'a str, &'static str);
impl<'a> Drop for Safe<'a> {
fn drop(&mut self) {
println!("Safe({}, {}) knows when *not* to inspect.", self.0, self.1);
}
}
struct Str(String);
impl Drop for Str{
fn drop(&mut self) {
self.0 = "DROPED!!!".to_string();
}
}
struct SafeS<'a> {
s: Str,
b: Option<BugBox<Safe<'a>>>,
}
pub fn main() {
let mut ss = SafeS {
b: None,
s: Str("HelloWorld".to_string()),
};
ss.b = Some(BugBox::new(Safe(&ss.s.0, "")));
}
所以这时候就需要为 BugBox 引入 PhantomData 了. 这样在与 Safe 一起使用时可以得到预期内的编译报错:
Compiling playground v0.0.1 (/playground)
error[E0713]: borrow may still be in use when destructor runs
--> src/main.rs:58:34
|
58 | ss.b = Some(BugBox::new(Safe(&ss.s.0, "")));
| ^^^^^^^
59 | }
| -
| |
| here, drop of `ss` needs exclusive access to `ss.s.0`, because the type `Str` implements the `Drop` trait
| borrow might be used here, when `ss` is dropped and runs the destructor for type `SafeS<'_>`
|
= note: consider using a `let` binding to create a longer lived value
error: aborting due to previous error
而且又不影响 BugBox<Safe1>
的编译.