Skip to content

20.3 内存释放

20.3.1 Vec 的析构函数

在 Rust 中,RAII 手法是非常常用的资源管理方式。Vec 就是利用 RAII 来进行资源管理的。因此,接下来我们需要分析 Vec 的析构函数:


rust
unsafe impl<#[may_dangle] T> Drop for Vec<T> {
    fn drop(&mut self) {
        unsafe {
            // use drop for [T]
            ptr::drop_in_place(&mut self[..]);
        }
        // RawVec handles deallocation
    }
}

目前版本的 Vec,在 impl Drop trait 的时候使用了unsafe关键字,而且使用了#[may_dangle] 这个 attribute。它是跟 drop check 相关的,下一节会继续分析。现在继续看这个析构函数的逻辑,它调用了 libcore 里面的ptr::drop_in_place函数:


rust
#[lang = "drop_in_place"]
#[allow(unconditional_recursion)]
pub unsafe fn drop_in_place<T: ?Sized>(to_drop: *mut T) {
    // Code here does not matter - this is replaced by the
    // real drop glue by the compiler.
    drop_in_place(to_drop);
}

而这个函数有#[lang="drop_in_place"]cattribute,这说明它是编译器提供的特殊实现,所以它的函数体我们就不再继续深究了,这里写的不是它的真实逻辑。总之它就是告诉编译器,调用这个指针指向对象的析构函数。需要注意的是约束T: ?Sized,这意味着这个泛型参数可以是定长类型,也可以是变长类型,即 DST。当 T 是变长类型的时候,这个指针*mut T实际上是一个“胖指针”,这种情况它也是可以处理的。

所以我们看到在 Vec 的析构函数里面,传递进去的实际参数是一个数组切片slice。编译器会逐个调用这个slice里面每个对象的析构函数。

Vec 的析构函数调用完之后,编译器还会自动调用它所有成员的析构函数。我们再看一下 RawVec 类型的析构函数:


rust
unsafe impl<#[may_dangle] T, A: Alloc> Drop for RawVec<T, A> {
    /// Frees the memory owned by the RawVec *without* trying to Drop its contents.
    fn drop(&mut self) {
        unsafe { self.dealloc_buffer(); }
    }
}

它做的事情就是回收内存,无须调用析构函数。其中dealloc_buffer函数的实现为:


rust
impl<T, A: Alloc> RawVec<T, A> {
    /// Frees the memory owned by the RawVec *without* trying to Drop its contents.
    pub unsafe fn dealloc_buffer(&mut self) {
        let elem_size = mem::size_of::<T>();
        if elem_size != 0 {
            if let Some(layout) = self.current_layout() {
                let ptr = self.ptr() as *mut u8;
                self.a.dealloc(ptr, layout);
            }
        }
    }
}

size_of::<T>()0的时候,根本没有在堆上分配内存,所以无须处理;否则,调用 allocator 的dealloc函数即可。

20.3.2 Drop Check

下面来详细说明一下什么是 drop check。Vec 的析构函数中出现的#[may_dangle]究竟是什么呢?

请大家注意,'a: 'b这个标记代表的含义是'a'b长或者相等。什么情况下,它们可以相等呢?当两个变量声明在同一条语句的时候,它们的生命周期是相等的。

也就是说,假如我们按顺序声明两个变量:


rust
let a = default();
let b = default();

那么a的生命周期一定严格大于b的生命周期。如果我们记录a的生命周期为'ab的生命周期为'b,那么'a: 'b成立,而'b: 'a不成立。因此,在a里面引用b一定是行不通的。

但是,假如我们把它们在一条语句中一起声明:


rust
let (a, b) = (default(), default());

它们的生命周期是相等的。如果我们记录a的生命周期为'ab的生命周期为'b,那么'a: 'b成立,而'b: 'a也成立。我们可以用示例来证明:


rust
fn main() {
    {
        let (a, mut b) : (i32, Option<&i32>) = (1, None);
        b = Some(&a);
    }
    {
        let (mut a, b) : (Option<&i32>, i32) = (None, 1);
        a = Some(&b);
    }
}

上面的代码可以编译通过,正是说明了以上的状况。

Rust 之所以这么规定,是因为在同一条语句中声明出来的不同变量绑定,无法根据先后关系确定出哪个严格包含于哪个。tuple 里面的两个成员的生命周期不存在严格大于和小于关系,struct 里面不同成员的生命周期也不存在严格大于和小于关系,数组里面不同成员的生命周期同样不存在严格大于和小于关系。它们的生命周期都是相等的。

这就引出了一个问题。两个不同的变量在析构的时候,总会出现一个先一个后,不可能同时析构。如果同一条语句中声明的不同变量可以存在引用关系,那么如果我们在析构函数中,试图访问另外一个变量,会出现什么情况呢?我们写一个示例:


rust
struct T { dropped: bool }

impl T {
    fn new() -> Self {
        T { dropped: false }
    }
}

impl Drop for T {
    fn drop(&mut self) {
        self.dropped = true;
    }
}

struct R<'a> {
    inner: Option<&'a T>
}

impl<'a> R<'a> {
    fn new() -> Self {
        R { inner: None }
    }
    fn set_ref<'b :'a>(&mut self, ptr: &'b T) {
        self.inner = Some(ptr);
    }
}

impl<'a> Drop for R<'a> {
    fn drop(&mut self) {
        if let Some(ref inner) = self.inner {
            println!("droppen R when T is {}", inner.dropped);
        }
    }
}

fn main() {
    {
        let (a, mut b) : (T, R) = (T::new(), R::new());
        b.set_ref(&a);
    }
    {
        let (mut a, b) : (R, T) = (R::new(), T::new());
        a.set_ref(&b);
    }
}

这个示例保持了上个示例的代码结构,只是把基本类型替换成了带有析构函数的自定义类型。编译之后出现了生命周期编译错误:


rust
error[E0597]: `a` does not live long enough

这样看来,我们原来想象的,在析构函数中访问相同生命周期的变量,制造内存不安全的想法是行不通的。

为什么前面的代码使用基本i32&i32类型可以编译通过,而我们换成自定义类型就通不过了呢?这就是所谓的 drop checker。Rust 在涉及析构函数的时候有个特殊规定,即如果两个变量具有析构函数,且有互相引用的关系,那么它们的生命周期必须满足“严格大于”的关系。这个关系目前在源码中表达不出来,但是为了防止析构函数中出现内存安全问题,编译器内部对此专门做了检查。

但是这种检查又有点过于严格。因为在很多情况下,虽然它们有引用关系,但是并没有在析构函数中做数据访问。此事取决于析构函数具体做了什么。如果析构函数没有做什么危险的事情,那么它们之间的生命周期满足普通的大于等于关系就够了。所以设计者决定,暂时留一个后门,让用户告诉编译器这个析构函数究竟危险还是不危险,这就是#[may_dangle] attribute 的由来。在上例中,我们如果把 R 类型的析构函数改为:


rust
unsafe impl<#[may_dangle] 'a> Drop for R<'a> {
    fn drop(&mut self) {
    }
}

再打开相应的 feature gate:


rust
#![feature(generic_param_attrs, dropck_eyepatch)]

以上代码就可以编译通过,生命周期冲突问题就消失了。这就是为什么Vec<T>的析构函数用了#[may_dangle]的原因。加了这个 attribute 可以让 Vec 类型容纳生命周期不满足“严格大于”关系的元素。

关于此事的详细解释,请参考 RFC-1327-dropck-param-eyepatch。这个功能也只是临时措施,关于 drop check 的部分,后面还会有改进。

Released under the MIT License