Skip to content

Commit 05b67d1

Browse files
authored
RAII chapter for idiomatic rust (#2820)
This PR adds the RAII chapter for the idiomatic Rust deep dive.
1 parent e42c8b3 commit 05b67d1

File tree

11 files changed

+704
-0
lines changed

11 files changed

+704
-0
lines changed

src/SUMMARY.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -441,6 +441,15 @@
441441
- [Semantic Confusion](idiomatic/leveraging-the-type-system/newtype-pattern/semantic-confusion.md)
442442
- [Parse, Don't Validate](idiomatic/leveraging-the-type-system/newtype-pattern/parse-don-t-validate.md)
443443
- [Is It Encapsulated?](idiomatic/leveraging-the-type-system/newtype-pattern/is-it-encapsulated.md)
444+
- [RAII](idiomatic/leveraging-the-type-system/raii.md)
445+
- [Drop Skipped](idiomatic/leveraging-the-type-system/raii/drop_skipped.md)
446+
- [Mutex](idiomatic/leveraging-the-type-system/raii/mutex.md)
447+
- [Drop Guards](idiomatic/leveraging-the-type-system/raii/drop_guards.md)
448+
- [Drop Bomb](idiomatic/leveraging-the-type-system/raii/drop_bomb.md)
449+
- [Drop Bomb Forget](idiomatic/leveraging-the-type-system/raii/drop_bomb_forget.md)
450+
- [forget and drop functions](idiomatic/leveraging-the-type-system/raii/forget_and_drop.md)
451+
- [Scope Guard](idiomatic/leveraging-the-type-system/raii/scope_guard.md)
452+
- [Drop Option](idiomatic/leveraging-the-type-system/raii/drop_option.md)
444453
- [Extension Traits](idiomatic/leveraging-the-type-system/extension-traits.md)
445454
- [Extending Foreign Types](idiomatic/leveraging-the-type-system/extension-traits/extending-foreign-types.md)
446455
- [Method Resolution Conflicts](idiomatic/leveraging-the-type-system/extension-traits/method-resolution-conflicts.md)
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
---
2+
minutes: 15
3+
---
4+
5+
# RAII: `Drop` trait
6+
7+
RAII (**R**esource **A**cquisition **I**s **I**nitialization) ties the lifetime
8+
of a resource to the lifetime of a value.
9+
10+
[Rust uses RAII to manage memory](https://doc.rust-lang.org/rust-by-example/scope/raii.html),
11+
and the `Drop` trait allows you to extend this to other resources, such as file
12+
descriptors or locks.
13+
14+
```rust,editable
15+
pub struct File(std::os::fd::RawFd);
16+
17+
impl File {
18+
pub fn open(path: &str) -> Result<Self, std::io::Error> {
19+
// [...]
20+
Ok(Self(0))
21+
}
22+
23+
pub fn read_to_end(&mut self) -> Result<Vec<u8>, std::io::Error> {
24+
// [...]
25+
Ok(b"example".to_vec())
26+
}
27+
28+
pub fn close(self) -> Result<(), std::io::Error> {
29+
// [...]
30+
Ok(())
31+
}
32+
}
33+
34+
fn main() -> Result<(), std::io::Error> {
35+
let mut file = File::open("example.txt")?;
36+
println!("content: {:?}", file.read_to_end()?);
37+
Ok(())
38+
}
39+
```
40+
41+
<details>
42+
43+
- Easy to miss: `file.close()` is never called. Ask the class if they noticed.
44+
45+
- To release the file descriptor correctly, `file.close()` must be called after
46+
the last use — and also in early-return paths in case of errors.
47+
48+
- Instead of relying on the user to call `close()`, we can implement the `Drop`
49+
trait to release the resource automatically. This ties cleanup to the lifetime
50+
of the `File` value.
51+
52+
```rust,compile_fail
53+
impl Drop for File {
54+
fn drop(&mut self) {
55+
// libc::close(...);
56+
println!("file descriptor was closed");
57+
}
58+
}
59+
```
60+
61+
- Note that `Drop::drop()` cannot return a `Result`. Any failures must be
62+
handled internally or ignored. In the standard library, errors during FD
63+
closure inside `Drop` are silently discarded. See the implementation:
64+
<https://doc.rust-lang.org/src/std/os/fd/owned.rs.html#169-196>
65+
66+
- When is `Drop::drop` called?
67+
68+
Normally, when the `file` variable in `main()` goes out of scope (either on
69+
return or due to a panic), `drop()` is called automatically.
70+
71+
If the file is moved into another function (as is this case with
72+
`File::close()`), the value is dropped when that function returns — not in
73+
`main`.
74+
75+
In contrast, C++ runs destructors in the original scope even for moved-from
76+
values.
77+
78+
- Demo: insert `panic!("oops")` at the start of `read_to_end()` and run it.
79+
`drop()` still runs during unwinding.
80+
81+
### More to Explore
82+
83+
The `Drop` trait has another important limitation: it is not `async`.
84+
85+
This means you cannot `await` inside a destructor, which is often needed when
86+
cleaning up asynchronous resources like sockets, database connections, or tasks
87+
that must signal completion to another system.
88+
89+
- Learn more:
90+
<https://rust-lang.github.io/async-fundamentals-initiative/roadmap/async_drop.html>
91+
- There is an experimental `AsyncDrop` trait available on nightly:
92+
<https://doc.rust-lang.org/nightly/std/future/trait.AsyncDrop.html>
93+
94+
</details>
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
---
2+
minutes: 15
3+
---
4+
5+
# Drop Bombs: Enforcing API Correctness
6+
7+
Use `Drop` to enforce invariants and detect incorrect API usage. A "drop bomb"
8+
panics if a value is dropped without being explicitly finalized.
9+
10+
This pattern is often used when the finalizing operation (like `commit()` or
11+
`rollback()`) needs to return a `Result`, which cannot be done from `Drop`.
12+
13+
```rust,editable
14+
use std::io::{self, Write};
15+
16+
struct Transaction {
17+
active: bool,
18+
}
19+
20+
impl Transaction {
21+
fn start() -> Self {
22+
Self { active: true }
23+
}
24+
25+
fn commit(mut self) -> io::Result<()> {
26+
writeln!(io::stdout(), "COMMIT")?;
27+
self.active = false;
28+
Ok(())
29+
}
30+
}
31+
32+
impl Drop for Transaction {
33+
fn drop(&mut self) {
34+
if self.active {
35+
panic!("Transaction dropped without commit!");
36+
}
37+
}
38+
}
39+
40+
fn main() -> io::Result<()> {
41+
let tx = Transaction::start();
42+
// Use `tx` to build the transaction, then commit it.
43+
// Comment out the call to `commit` to see the panic.
44+
tx.commit()?;
45+
Ok(())
46+
}
47+
```
48+
49+
<details>
50+
51+
- In some systems, a value must be finalized by a specific API before it is
52+
dropped.
53+
54+
For example, a `Transaction` might need to be committed or rolled back.
55+
56+
- A drop bomb ensures that a value like `Transaction` cannot be silently dropped
57+
in an unfinished state. The destructor panics if the transaction has not been
58+
explicitly finalized (for example, with `commit()`).
59+
60+
- The finalizing operation (such as `commit()`) usually takes `self` by value.
61+
This ensures that once the transaction is finalized, the original object can
62+
no longer be used.
63+
64+
- A common reason to use this pattern is when cleanup cannot be done in `Drop`,
65+
either because it is fallible or asynchronous.
66+
67+
- This pattern is appropriate even in public APIs. It can help users catch bugs
68+
early when they forget to explicitly finalize a transactional object.
69+
70+
- If cleanup can safely happen in `Drop`, some APIs choose to panic only in
71+
debug builds. Whether this is appropriate depends on the guarantees your API
72+
must enforce.
73+
74+
- Panicking in release builds is reasonable when silent misuse would cause major
75+
correctness or security problems.
76+
77+
- Question: Why do we need an `active` flag inside `Transaction`? Why can't
78+
`drop()` panic unconditionally?
79+
80+
Expected answer: `commit()` takes `self` by value and runs `drop()`, which
81+
would panic.
82+
83+
## More to explore
84+
85+
Several related patterns help enforce correct teardown or prevent accidental
86+
drops.
87+
88+
- The [`drop_bomb` crate](https://docs.rs/drop_bomb/latest/drop_bomb/): A small
89+
utility that panics if dropped unless explicitly defused with `.defuse()`.
90+
Comes with a `DebugDropBomb` variant that only activates in debug builds.
91+
92+
</details>
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
---
2+
minutes: 10
3+
---
4+
5+
# Drop Bombs: using `std::mem::forget`
6+
7+
```rust,editable
8+
use std::io::{self, Write};
9+
10+
struct Transaction;
11+
12+
impl Transaction {
13+
fn start() -> Self {
14+
Transaction
15+
}
16+
17+
fn commit(self) -> io::Result<()> {
18+
writeln!(io::stdout(), "COMMIT")?;
19+
20+
// Defuse the drop bomb by preventing Drop from ever running.
21+
std::mem::forget(self);
22+
23+
Ok(())
24+
}
25+
}
26+
27+
impl Drop for Transaction {
28+
fn drop(&mut self) {
29+
// This is the "drop bomb"
30+
panic!("Transaction dropped without commit!");
31+
}
32+
}
33+
34+
fn main() -> io::Result<()> {
35+
let tx = Transaction::start();
36+
// Use `tx` to build the transaction, then commit it.
37+
// Comment out the call to `commit` to see the panic.
38+
tx.commit()?;
39+
Ok(())
40+
}
41+
```
42+
43+
<details>
44+
45+
This example removes the flag from the previous slide and makes the drop method
46+
panic unconditionally. To avoid that panic on a successful commit, the commit
47+
method now takes ownership of the transaction and calls
48+
[`std::mem::forget`](https://doc.rust-lang.org/std/mem/fn.forget.html), which
49+
prevents the `Drop::drop()` method from running.
50+
51+
If the forgotten value owned heap allocated memory that would normally be freed
52+
in its `drop()` implementation, one consequence is a memory leak. That is not
53+
the case for the `Transaction` in the example above, since it does not own any
54+
heap memory.
55+
56+
We can avoid needing a runtime flag by using `mem::forget()` in a tactical way.
57+
When the transaction commits successfully, we can defuse the drop bomb by
58+
calling `std::mem::forget` on the value, which prevents its `Drop`
59+
implementation from running.
60+
61+
</details>
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
---
2+
minutes: 10
3+
---
4+
5+
# Drop Guards
6+
7+
A **drop guard** in Rust is a temporary object that performs some kind of
8+
cleanup when it goes out of scope. In the case of `Mutex`, the `lock` method
9+
returns a `MutexGuard` that automatically unlocks the mutex on `drop`:
10+
11+
```rust
12+
struct Mutex {
13+
is_locked: bool,
14+
}
15+
16+
struct MutexGuard<'a> {
17+
mutex: &'a mut Mutex,
18+
}
19+
20+
impl Mutex {
21+
fn new() -> Self {
22+
Self { is_locked: false }
23+
}
24+
25+
fn lock(&mut self) -> MutexGuard<'_> {
26+
self.is_locked = true;
27+
MutexGuard { mutex: self }
28+
}
29+
}
30+
31+
impl Drop for MutexGuard<'_> {
32+
fn drop(&mut self) {
33+
self.mutex.is_locked = false;
34+
}
35+
}
36+
```
37+
38+
<details>
39+
40+
- The example above shows a simplified `Mutex` and its associated guard.
41+
42+
- Even though it is not a production-ready implementation, it illustrates the
43+
core idea:
44+
45+
- the guard represents exclusive access,
46+
- and its `Drop` implementation releases the lock when it goes out of scope.
47+
48+
## More to Explore
49+
50+
This example shows a C++ style mutex that does not contain the data it protects.
51+
While this is non-idiomatic in Rust, the goal here is only to illustrate the
52+
core idea of a drop guard, not to demonstrate a proper Rust mutex design.
53+
54+
For brevity, several features are omitted:
55+
56+
- A real `Mutex<T>` stores the protected value inside the mutex.\
57+
This toy example omits the value entirely to focus only on the drop guard
58+
mechanism.
59+
- Ergonomic access via `Deref` and `DerefMut` on `MutexGuard` (letting the guard
60+
behave like a `&T` or `&mut T`).
61+
- A fully blocking `.lock()` method and a non-blocking `try_lock` variant.
62+
63+
You can explore the
64+
[`Mutex` implementation in Rust’s std library](https://doc.rust-lang.org/std/sync/struct.Mutex.html)
65+
as an example of a production-ready mutex. The
66+
[`Mutex` from the `parking_lot` crate](https://docs.rs/parking_lot/latest/parking_lot/type.Mutex.html)
67+
is another worthwhile reference.
68+
69+
</details>

0 commit comments

Comments
 (0)