Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions src/SUMMARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -441,6 +441,14 @@
- [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 Guards](idiomatic/leveraging-the-type-system/raii/drop_guards.md)
- [Drop Bomb](idiomatic/leveraging-the-type-system/raii/drop_bomb.md)
- [Drop Skipped](idiomatic/leveraging-the-type-system/raii/drop_skipped.md)
- [Drop Bomb Forget](idiomatic/leveraging-the-type-system/raii/drop_bomb_forget.md)
- [Scope Guard](idiomatic/leveraging-the-type-system/raii/scope_guard.md)
- [Drop Option](idiomatic/leveraging-the-type-system/raii/drop_option.md)
- [Extension Traits](idiomatic/leveraging-the-type-system/extension-traits.md)
- [Extending Foreign Types](idiomatic/leveraging-the-type-system/extension-traits/extending-foreign-types.md)
- [Method Resolution Conflicts](idiomatic/leveraging-the-type-system/extension-traits/method-resolution-conflicts.md)
Expand Down
92 changes: 92 additions & 0 deletions src/idiomatic/leveraging-the-type-system/raii.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
---
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>

- Easy to miss: `file.close()` is never called. Ask the class if they noticed.

- 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 during FD
closure inside `Drop` are silently discarded. See the implementation:
<https://doc.rust-lang.org/src/std/os/fd/owned.rs.html#169-196>

- 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.

- Demo: insert `panic!("oops")` at the start of `read_to_end()` and run it.
`drop()` still runs during unwinding.

### 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>
85 changes: 85 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,85 @@
# 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 {
fn start() -> Self {
Self { active: true }
}

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

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

fn main() -> io::Result<()> {
let tx = Transaction::start();
// Use `tx` to build the transaction, then commit it.
// Comment out the call to `commit` to see the panic.
tx.commit()?;
Ok(())
}
```

<details>

- A drop bomb ensures that a value like `Transaction` cannot be silently dropped
in an unfinished state. The destructor panics if the transaction has not been
explicitly finalized (for example, with `commit()`).

- The finalizing operation (such as `commit()`) usually take `self` by value.
This ensures that once the transaction is finalized, the original object can
no longer be used.

- 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 cleanup can safely happen in `Drop`, some APIs choose to panic only in
debug builds. Whether this is appropriate depends on the guarantees your API
must enforce.

- Panicking in Release builds is reasonable when silent misuse would cause major
correctness or security problems.

## More to explore

Several related patterns help enforce correct teardown or prevent accidental
drops.

- The [`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.

</details>
53 changes: 53 additions & 0 deletions src/idiomatic/leveraging-the-type-system/raii/drop_bomb_forget.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# Drop Bombs: using `std::mem::forget`

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

struct Transaction;

impl Transaction {
fn start() -> Self {
Transaction
}

fn commit(self) -> io::Result<()> {
writeln!(io::stdout(), "COMMIT")?;

// Defuse the drop bomb by preventing Drop from ever running.
std::mem::forget(self);

Ok(())
}
}

impl Drop for Transaction {
fn drop(&mut self) {
// This is the "drop bomb"
panic!("Transaction dropped without commit!");
}
}

fn main() -> io::Result<()> {
let tx = Transaction::start();
// Use `tx` to build the transaction, then commit it.
// Comment out the call to `commit` to see the panic.
tx.commit()?;
Ok(())
}
```

<details>

In the previous slide we saw that calling
[`std::mem::forget`](https://doc.rust-lang.org/std/mem/fn.forget.html) prevents
`Drop::drop` from ever running.

Remember that `mem::forget` leaks the value. This is safe in Rust, but the
memory will not be reclaimed.

However, this avoids needing a runtime flag: when the transaction is
successfully committed, we can _defuse_ the drop bomb — meaning we prevent
`Drop` from running — by calling `std::mem::forget` on the value instead of
letting its destructor run.

</details>
65 changes: 65 additions & 0 deletions src/idiomatic/leveraging-the-type-system/raii/drop_guards.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
# Drop Guards

A **drop guard** in Rust is a temporary object that performs some kind of
cleanup when it goes out of scope. In the case of `Mutex`, the `lock` method
returns a `MutexGuard` that automatically unlocks the mutex on `drop`:

```rust
struct Mutex {
is_locked: bool,
}

struct MutexGuard<'a> {
mutex: &'a mut Mutex,
}

impl Mutex {
fn new() -> Self {
Self { is_locked: false }
}

fn lock(&mut self) -> MutexGuard<'_> {
self.is_locked = true;
MutexGuard { mutex: self }
}
}

impl Drop for MutexGuard<'_> {
fn drop(&mut self) {
self.mutex.is_locked = false;
}
}
```

<details>

- The example above shows a simplified `Mutex` and its associated guard.

- Even though it is not a production-ready implementation, it illustrates the
core idea:

- the guard represents exclusive access,
- and its `Drop` implementation releases the lock when it goes out of scope.

## More to Explore

This example shows a C++ style mutex that does not contain the data it protects.
While this is non idiomatic in Rust, the goal here is only to illustrate the
core idea of a drop guard, not to demonstrate a proper Rust mutex design.

For brevity, several features are omitted:

- A real `Mutex<T>` stores the protected value inside the mutex.\
This toy example omits the value entirely to focus only on the drop guard
mechanism.
- Ergonomic access via `Deref` and `DerefMut` on `MutexGuard` (letting the guard
behave like a `&T` or `&mut T`).
- A fully blocking `.lock()` method and a non blocking `try_lock` variant.

You can explore the
[`Mutex` implementation in Rust’s std library](https://doc.rust-lang.org/std/sync/struct.Mutex.html)
as an example of a production-ready mutex. The
[`Mutex` from the `parking_lot` crate](https://docs.rs/parking_lot/latest/parking_lot/type.Mutex.html)
is another worthwhile reference.

</details>
84 changes: 84 additions & 0 deletions src/idiomatic/leveraging-the-type-system/raii/drop_option.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
# Drop: Option

```rust,editable
struct File(Option<Handle>);

impl File {
fn open(path: &'static str) -> std::io::Result<Self> {
Ok(Self(Some(Handle { path })))
}

fn write(&mut self, data: &str) -> std::io::Result<()> {
match &mut self.0 {
Some(handle) => println!("write '{data}' to file '{}'", handle.path),
None => unreachable!(),
}
Ok(())
}

fn close(mut self) -> std::io::Result<&'static str> {
Ok(self.0.take().unwrap().path)
}
}

impl Drop for File {
fn drop(&mut self) {
if let Some(handle) = self.0.take() {
println!("automatically close handle for file: {}", handle.path);
}
}
}

struct Handle {
path: &'static str,
}
impl Drop for Handle {
fn drop(&mut self) {
println!("close handle for file: {}", self.path)
}
}

fn main() -> std::io::Result<()> {
let mut file = File::open("foo.txt")?;
file.write("hello")?;
println!("manually closed file: {}", file.close()?);
Ok(())
}
```

<details>

- In this example we want to let the user call `close()` manually so that errors
from closing the file can be reported explicitly.

- At the same time we still want RAII semantics: if the user forgets to call
`close()`, the handle must be cleaned up automatically in `Drop`.

- Wrapping the handle in an `Option` gives us both behaviors. `close()` extracts
the handle with `take()`, and `Drop` only runs cleanup if a handle is still
present.

Demo: remove the `.close()` call and run the code — `Drop` now prints the
automatic cleanup.

- The main downside is ergonomics. `Option` forces us to handle both the `Some`
and `None` case even in places where, logically, `None` cannot occur. Rust’s
type system cannot express that relationship between `File` and its `Handle`,
so we handle both cases manually.

## More to explore

Instead of `Option` we could use
[`ManuallyDrop`](https://doc.rust-lang.org/std/mem/struct.ManuallyDrop.html),
which suppresses automatic destruction by preventing Rust from calling `Drop`
for the value; you must handle teardown yourself.

The [_scopeguard_ example](./scope_guard.md) on the previous slide shows how
`ManuallyDrop` can replace `Option` to avoid handling `None` in places where the
value should always exist.

In such designs we typically track the drop state with a separate flag next to
the `ManuallyDrop<Handle>`, which lets us track whether the handle has already
been manually consumed.

</details>
Loading
Loading