Appearance
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
的生命周期为'a
,b
的生命周期为'b
,那么'a: 'b
成立,而'b: 'a
不成立。因此,在a
里面引用b
一定是行不通的。
但是,假如我们把它们在一条语句中一起声明:
rust
let (a, b) = (default(), default());
它们的生命周期是相等的。如果我们记录a
的生命周期为'a
,b
的生命周期为'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 的部分,后面还会有改进。