Skip to content

RAII chapter for idiomatic rust #2820

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 12 commits into
base: main
Choose a base branch
from
4 changes: 4 additions & 0 deletions src/SUMMARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -437,6 +437,10 @@
- [Semantic Confusion](idiomatic/leveraging-the-type-system/newtype-pattern/semantic-confusion.md)
- [Parse, Don't Validate](idiomatic/leveraging-the-type-system/newtype-pattern/parse-don-t-validate.md)
- [Is It Encapsulated?](idiomatic/leveraging-the-type-system/newtype-pattern/is-it-encapsulated.md)
- [RAII](idiomatic/leveraging-the-type-system/raii.md)
- [Mutex](idiomatic/leveraging-the-type-system/raii/mutex.md)
- [Drop Bomb](idiomatic/leveraging-the-type-system/raii/drop_bomb.md)
- [Scope Guard](idiomatic/leveraging-the-type-system/raii/scope_guard.md)

---

Expand Down
113 changes: 113 additions & 0 deletions src/idiomatic/leveraging-the-type-system/raii.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
---
minutes: 30
---

# RAII: `Drop` trait

RAII (**R**esource **A**cquisition **I**s **I**nitialization) ties the lifetime
of a resource to the lifetime of a value.

[Rust uses RAII to manage memory](https://doc.rust-lang.org/rust-by-example/scope/raii.html),
and the `Drop` trait allows you to extend this to other resources, such as file
descriptors or locks.

```rust,editable
pub struct File(std::os::fd::RawFd);

impl File {
pub fn open(path: &str) -> Result<Self, std::io::Error> {
// [...]
Ok(Self(0))
}

pub fn read_to_end(&mut self) -> Result<Vec<u8>, std::io::Error> {
// [...]
Ok(b"example".to_vec())
}

pub fn close(self) -> Result<(), std::io::Error> {
// [...]
Ok(())
}
}

fn main() -> Result<(), std::io::Error> {
let mut file = File::open("example.txt")?;
println!("content: {:?}", file.read_to_end()?);
Ok(())
}
```

<details>

- This example shows how easy it is to forget releasing a file descriptor when
managing it manually. The code as written does not call `file.close()`. Did
anyone in the class notice?

- To release the file descriptor correctly, `file.close()` must be called after
the last use — and also in early-return paths in case of errors.

- Instead of relying on the user to call `close()`, we can implement the `Drop`
trait to release the resource automatically. This ties cleanup to the lifetime
of the `File` value.

```rust,compile_fail
impl Drop for File {
fn drop(&mut self) {
println!("release file descriptor automatically");
}
}
```

- Note that `Drop::drop` cannot return errors. Any fallible logic must be
handled internally or ignored. In the standard library, errors returned while
closing an owned file descriptor during `Drop` are silently discarded:
<https://doc.rust-lang.org/src/std/os/fd/owned.rs.html#169-196>

- If both `drop()` and `close()` exist, the file descriptor may be released
twice. To avoid this, remove `close()` and rely solely on `Drop`.

- When is `Drop::drop` called?

Normally, when the `file` variable in `main` goes out of scope (either on
return or due to a panic), `drop()` is called automatically.

If the file is moved into another function, for example `read_all()`, the
value is dropped when that function returns — not in `main`.

In contrast, C++ runs destructors in the original scope even for moved-from
values.

- The same mechanism powers `std::mem::drop`:

```rust
pub fn drop<T>(_x: T) {}
```

You can use it to force early destruction of a value before its natural end of
scope.

- Insert `panic!("oops")` at the start of `read_to_end()` to show that `drop()`
still runs during unwinding. Rust guarantees this unless the panic strategy is
set to `abort`.
Comment on lines +90 to +92
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
- Insert `panic!("oops")` at the start of `read_to_end()` to show that `drop()`
still runs during unwinding. Rust guarantees this unless the panic strategy is
set to `abort`.
- Insert `panic!("oops")` at the start of `read_to_end()` to show that `drop()`
still runs during unwinding.

You cover this in the next bullet point, so I think this one can be simplified.


- There are cases where destructors will not run:
- If a destructor itself panics during unwinding, the program aborts
immediately.
- If the program exits with `std::process::exit()` or is compiled with the
`abort` panic strategy, destructors are skipped.

### More to Explore

The `Drop` trait has another important limitation: it is not `async`.

This means you cannot `await` inside a destructor, which is often needed when
cleaning up asynchronous resources like sockets, database connections, or tasks
that must signal completion to another system.

- Learn more:
<https://rust-lang.github.io/async-fundamentals-initiative/roadmap/async_drop.html>
- There is an experimental `AsyncDrop` trait available on nightly:
<https://doc.rust-lang.org/nightly/std/future/trait.AsyncDrop.html>

</details>
123 changes: 123 additions & 0 deletions src/idiomatic/leveraging-the-type-system/raii/drop_bomb.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
# Drop Bombs: Enforcing API Correctness

Use `Drop` to enforce invariants and detect incorrect API usage. A "drop bomb"
panics if a value is dropped without being explicitly finalized.

This pattern is often used when the finalizing operation (like `commit()` or
`rollback()`) needs to return a `Result`, which cannot be done from `Drop`.

```rust,editable
use std::io::{self, Write};

struct Transaction {
active: bool,
}

impl Transaction {
/// Begin a [`Transaction`].
///
/// ## Panics
///
/// Panics if the transaction is dropped without
/// calling [`Self::commit`] or [`Self::rollback`].
fn start() -> Self {
Self { active: true }
}

fn commit(mut self) -> io::Result<()> {
writeln!(io::stdout(), "COMMIT")?;
self.active = false;
Ok(())
}

fn rollback(mut self) -> io::Result<()> {
writeln!(io::stdout(), "ROLLBACK")?;
self.active = false;
Ok(())
}
}

impl Drop for Transaction {
fn drop(&mut self) {
if self.active {
panic!("Transaction dropped without commit or rollback!");
}
}
}

fn main() -> io::Result<()> {
let tx = Transaction::start();

if some_condition() {
tx.commit()?;
} else {
tx.rollback()?;
}

// Uncomment to see the panic:
// let tx2 = Transaction::start();

Ok(())
}

fn some_condition() -> bool {
// [...]
true
}
```

<details>

- This pattern ensures that a value like `Transaction` cannot be silently
dropped in an unfinished state. The destructor panics if neither `commit()`
nor `rollback()` has been called.

- A common reason to use this pattern is when cleanup cannot be done in `Drop`,
either because it is fallible or asynchronous.

- This pattern is appropriate even in public APIs. It can help users catch bugs
early when they forget to explicitly finalize a transactional object.

- If a value can be safely cleaned up in `Drop`, consider falling back to that
behavior in Release mode and panicking only in Debug. This decision should be
made based on the guarantees your API provides.

- Panicking in Release builds is a valid choice if silent misuse could lead to
serious correctness issues or security concerns.

## Additional Patterns

- [`Option<T>` with `.take()`](https://doc.rust-lang.org/std/option/enum.Option.html#method.take):
A common pattern inside `Drop` to move out internal values and prevent double
drops.

```rust,compile_fail
impl Drop for MyResource {
fn drop(&mut self) {
if let Some(handle) = self.handle.take() {
// do cleanup with handle
}
}
}
```

- [`ManuallyDrop`](https://doc.rust-lang.org/std/mem/struct.ManuallyDrop.html):
Prevents automatic destruction and gives full manual control. Requires
`unsafe`, so only use when strictly necessary.

- [`drop_bomb` crate](https://docs.rs/drop_bomb/latest/drop_bomb/): A small
utility that panics if dropped unless explicitly defused with `.defuse()`.
Comes with a `DebugDropBomb` variant that only activates in debug builds.

- In some systems, a value must be finalized by a specific API before it is
dropped.

For example, an `SshConnection` might need to be deregistered from an
`SshServer` before being dropped, or the program panics. This helps catch
programming mistakes during development and enforces correct teardown at
runtime.

See a working example in
[the Rust playground](https://play.rust-lang.org/?version=stable&mode=debug&edition=2024&gist=3223f5fa5e821cd32461c3af7162cd55).

</details>
131 changes: 131 additions & 0 deletions src/idiomatic/leveraging-the-type-system/raii/mutex.md
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've left a couple of comments in here about improving the example code, but I'd like to also propose a higher level change. I think we could split this slide in two, where the first slide would be "Mutex and MutexGuard" that just demonstrates using a mutex, i.e. what's in the main function on this slide. Then we'd have a second slide "Drop Guards" that shows the dummy implementation of Mutex and MutexGuard in order to show how to implement this pattern.

I think doing it that way would flow a bit better, because we would first show how the pattern is used, then dig into the details of how the pattern is implemented. It'd also allow us to slightly expand the example implementation to be a bit more detailed. Here's a playground link with a rough example of what the second slide would show. That doesn't compile currently because it's modifying a field of Mutex in a &self method. That could be fixed with atomics, which would be more accurate but would also add more clutter that I think would distract from the main point of demonstrating the general pattern of drop guards.

Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
# Mutex

In earlier examples, RAII was used to manage concrete resources like file
descriptors. With a `Mutex`, the resource is more abstract: exclusive access to
a value.

Rust models this using a `MutexGuard`, which ties access to a critical section
to the lifetime of a value on the stack.

```rust
#[derive(Debug)]
struct Mutex<T> {
value: std::cell::UnsafeCell<T>,
// [...]
}

#[derive(Debug)]
struct MutexGuard<'a, T> {
value: &'a mut T,
// [...]
}

impl<T> Mutex<T> {
fn new(value: T) -> Self {
Self {
value: std::cell::UnsafeCell::new(value),
// [...]
}
}

fn lock(&self) -> MutexGuard<T> {
// [...]
let value = unsafe { &mut *self.value.get() };
MutexGuard { value }
}
}

impl<'a, T> std::ops::Deref for MutexGuard<'a, T> {
type Target = T;
fn deref(&self) -> &T {
self.value
}
}

impl<'a, T> std::ops::DerefMut for MutexGuard<'a, T> {
fn deref_mut(&mut self) -> &mut T {
self.value
}
}
Comment on lines +38 to +49
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For brevity I think we can omit the Deref impls from the example and have the code in main directly access guard.value. That's not an accurate depiction of how the actual MutexGuard works, but I think is good enough for us here.


impl<'a, T> Drop for MutexGuard<'a, T> {
fn drop(&mut self) {
// [...]
println!("drop MutexGuard");
}
Comment on lines +52 to +55
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is failing to demonstrate the crucial part of MutexGuard: That it reports back to the Mutex on drop, unlocking the mutex. I think we should have a stubbed-out unlock method on Mutex and call it on drop here.

}

fn main() {
let m = Mutex::new(vec![1, 2, 3]);

let mut guard = m.lock();
guard.push(4);
guard.push(5);
println!("{guard:?}");
}
```

<details>

- A `Mutex` controls exclusive access to a value. Unlike earlier RAII examples,
the resource here is not external but logical: the right to mutate shared
data.

- This right is represented by a `MutexGuard`. Only one can exist at a time.
While it lives, it provides `&mut T` access — enforced using `UnsafeCell`.

- Although `lock()` takes `&self`, it returns a `MutexGuard` with mutable
access. This is possible through interior mutability: a common pattern for
safe shared-state mutation.

- `MutexGuard` implements `Deref` and `DerefMut`, making access ergonomic. You
lock the mutex, use the guard like a `&mut T`, and the lock is released
automatically when the guard goes out of scope.

- The release is handled by `Drop`. There is no need to call a separate unlock
function — this is RAII in action.

## Poisoning

- If a thread panics while holding the lock, the value may be in a corrupt
state.

- To signal this, the standard library uses poisoning. When `Drop` runs during a
panic, the mutex marks itself as poisoned.

- On the next `lock()`, this shows up as an error. The caller must decide
whether to proceed or handle the error differently.

- See this example showing the standard library API with poisoning:
<https://play.rust-lang.org/?version=stable&mode=debug&edition=2024&gist=6fb0c2e9e5cbcbbae1c664f4650b8c92>
Comment on lines +88 to +100
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think it's worth going into poisoning here. The point here is to demonstrate the drop guard pattern, not to talk in detail about how mutexes work.


### Mutex Lock Lifecycle

```bob
+---------------+ +----------------------+
| Mutex<T> | lock | MutexGuard<T> |
| Unlocked | ------> | Exclusive Access |
+---------------+ +----------------------+

^ | drop
| no |
+---------------+ |
| |
| V

+---------------+ yes +-------------------+
| Mutex<T> | <---- | Thread panicking? |
| Poisoned | +-------------------+
+---------------+

|
| lock
|
v

+------------------+
| Err ( Poisoned ) |
+------------------+
```
Comment on lines +102 to +129
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we need this diagram, especially if we drop the discussion of poisoning as I suggested in another comment.


</details>
Loading
Loading