# 2022-41: Rust Drop 踩坑分享

Rust 使用 RAII (Resource Acquisition Is Initialization) 来管理资源：对象初始化会导致资源的初始化，而对象释放时会导致资源的释放。

以 `Mutex` 为例：

```rust
{
    let guard = m.lock();
    // do something
}
// guard freed out of scope.
{
    // we can acquire this lock again.
    let guard = m.lock();
}
```

当 `guard` 离开当前 scope 的时，rust 会保证 `guard` 的 `drop` 被自动调用：

```rust
#[stable(feature = "rust1", since = "1.0.0")]
impl<T: ?Sized> Drop for MutexGuard<'_, T> {
    #[inline]
    fn drop(&mut self) {
        unsafe {
            self.lock.poison.done(&self.poison);
            self.lock.inner.raw_unlock();
        }
    }
}
```

- 如果对应的类型有自己的 `Drop` 实现，rust 会调用 `Drop::drop()`
- 否则则递归对每个字段执行自动生成的 drop 实现

Drop 的 trait 定义如下：

```rust
pub trait Drop {
    fn drop(&mut self);
}
```

非常简单，但是在实际的使用过程中还是很容易踩坑。今天的这期周报就结合一些实际的 BUG 来聊聊我的踩坑经历。

## `_` 与 `_var` 的行为差异

`let _var = abc;` 的语义是十分明确的：创建一个新的绑定，他的生命周期会持续到当前 scope 结束：

```rust
struct Test(&'static str);

impl Drop for Test {
    fn drop(&mut self) {
        println!("Test with {} dropped", self.0)
    }
}

fn main() {
    {
        println!("into scope");
        let _abc = Test("_abc");
        println!("leave scope");
    }
}
```

其执行结果如下：

```txt
into scope
leave scope
Test with _abc dropped
```

但是 `let _ = abc;` 的语义却更晦涩一些：不要将后面的表达式绑定为任何东西。它只是一个 match 表达式，本身并不会导致 drop，之所以我们观察到 drop 是因为它 match 的值本身就是临时的。

很多人将其理解为等价于 `drop(abc)` 或者 `abc;` 是错误的，这里有一个[反例](https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=b1e2686c58996a63fc1b8b120522dbf9)：

```rust
struct Test(&'static str);

impl Drop for Test {
    fn drop(&mut self) {
        println!("Test with {} dropped", self.0)
    }
}

fn main() {
    let x = Test("x");
    {
        println!("into scope");
        let _ = x;
        println!("leave scope");
    }
}
```

其执行结果如下：

```shell
into scope
leave scope
Test with x dropped
```

将其理解为 no-op，也是片面的。我们同样能找出一个[反例](https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=03b4db347ae942a1eaed14ec4e288bee)：

```rust
struct Test(&'static str);

impl Drop for Test {
    fn drop(&mut self) {
        println!("Test with {} dropped", self.0)
    }
}

fn main() {
    println!("into scope");
    let _ = Test("x");
    println!("leave scope");
}
```

其执行结果为：

```rust
into scope
Test with x dropped
leave scope
```

这里还有一些更有趣的[例子](https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=410ca71fe4b067619f544b4bcd22f6cf)：

```rust
struct Test(&'static str);

impl Drop for Test {
    fn drop(&mut self) {
        println!("Test with {} dropped", self.0)
    }
}

fn main() {
    match (Test("a"), Test("b"), Test("c")) {
        (a, _, c) => {
            println!("match arm");
        }
    }
    println!("after match");
}
```

其执行结果为：

```txt
match arm
Test with c dropped
Test with a dropped
Test with b dropped
after match
```

`Test("b")` 的生命周期一直延续到这个 match 语句的最后。

综上所述，`let _ = abc;` 是 match pattern 的延续，其实际的行为是受具体的表达式制约的。我们不应当依赖 `let _ = abc;` 实现任何 drop 的逻辑，其唯一合理用途是用来标记变量不再使用，以免除 `#[must_use]` 警告：

```rust
// remove file, but don't care about its result.
let _ = fs::remove_file("a.txt");
```

在实际的业务逻辑中，我们时常会忽略这一点，以 Databend 最近修复的一个 BUG 为例：[Bug: runtime spawn_batch does not release permit correctly](https://github.com/datafuselabs/databend/issues/8183)。Databend 为了控制 IO 的并发数量，使用 semaphore 来控制任务的并行度。本来期望的时候在任务执行完毕后再释放，但是代码中使用了 `let _ = permit`，导致 permit 释放时机不符合预期，进而导致任务的并发控制不符合预期：

```diff
 let handler = self.handle.spawn(async move {
     // take the ownership of the permit, (implicitly) drop it when task is done
-    let _ = permit;
+    let _pin = permit;
     fut.await
 });
```

## 如何手动调用 drop

处于显而易见的原因，`Drop::drop()` 不允许被手动调用，否则非常容易出现 double free 的问题，Rust 在编译器就会对这样的调用报错。如果想要控制变量的 drop，可以使用 [std::mem::drop](https://doc.rust-lang.org/std/mem/fn.drop.html) 函数，它的原理非常简单：Move 这个变量，然后不返回任何东西。

```rust
#[inline]
#[stable(feature = "rust1", since = "1.0.0")]
#[cfg_attr(not(test), rustc_diagnostic_item = "mem_drop")]
pub fn drop<T>(_x: T) {}
```

本质上相当于：

```rust
let x = Test {};

{
    x;
}
```

但是需要注意，对实现了 `Copy` 的类型来说，调用 `drop` 是没有意义的：

- 编译器会自行维护 Copy 类型在栈上的数据，不能为 Copy 类型实现 `Drop` trait
- 对 Copy 类型调用 drop 总是会复制当前变量然后释放

## 致谢

感谢 [@drmingdrmer](https://github.com/drmingdrmer/) & [@zhang2014](https://github.com/zhang2014) 的讨论，纠正了我的错误观点

## 参考资料

- [透过 Rust 探索系统的本原：RAII](https://mp.weixin.qq.com/s/jaKjzc_1rkDe67rfpnFTgg)
- [Rust Doc: std::ops::Drop](https://doc.rust-lang.org/std/ops/trait.Drop.html)
- [Rust Doc: std::mem::drop](https://doc.rust-lang.org/std/mem/fn.drop.html)
- [Pre-RFC: `#[must_bind]`](https://internals.rust-lang.org/t/pre-rfc-must-bind/12658/23)