Rust中Drop检查 与 PhantomData

Drop checker check 了什么

​ 到目前为止我们提到生命周期的长短时,指的都是非严格的关系。也就是说,当我们写'a: 'b的时候,'a其实也可以和'b一样长。乍一看,这一点没什么意义。本来也不会有两个东西被同时销毁的,不是吗?我们去掉下面的let表达式的语法糖看看

let x;
let y;
{
    let x;
    {
        let y;
    }
}

​ 每一个都创建了自己的作用域,可以很清楚地看出来一个在另一个之前被销毁。但是,如果是下面这样的呢

let (x, y) = (vec![], vec![]);

​ 有哪一个比另一个存活更长吗?答案是,没有,没有哪个严格地比另一个长。当然,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

struct S<'a, T> {
    // ...
}

​ S 是 使用了泛型的类型, 若 S 实现了 Drop trait, 那么 rust drop checker 要求 泛型参数(即 'a, T) 的 lifetime 必须要超过它, 即 超过 S的实例

let s: S<'a, T> = // ...

​ 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> 的编译.

Last modification:April 15th, 2021 at 08:41 pm
安安,亲觉得有用, 请随意打赏嗷