Skip to content
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
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
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)
- [Drop Limitations](idiomatic/leveraging-the-type-system/raii/drop_limitations.md)
- [Drop Bomb](idiomatic/leveraging-the-type-system/raii/drop_bomb.md)
- [Scope Guards](idiomatic/leveraging-the-type-system/raii/scope_guards.md)

---

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

# RAII and `Drop` in Practice

RAII (_Resource Acquisition Is Initialization_) means tying the lifetime of a
resource to the lifetime of a value.

Rust applies RAII automatically for memory management. The `Drop` trait lets you
extend this pattern to anything else.

```rust
use std::sync::Mutex;

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

{
let mut data = mux.lock().unwrap();
data.push(4); // lock held here
} // lock automatically released here
}
```

<details>

- In the above example
[the `Mutex`](https://doc.rust-lang.org/std/sync/struct.Mutex.html) owns its
data: you can’t access the value inside without first acquiring the lock.

`mux.lock()` returns a
[`MutexGuard`](https://doc.rust-lang.org/std/sync/struct.MutexGuard.html),
which [dereferences](https://doc.rust-lang.org/std/ops/trait.DerefMut.html) to
the data and implements
[`Drop`](https://doc.rust-lang.org/std/ops/trait.Drop.html).

- You may recall from
[the Memory Management chapter](../../memory-management/drop.md) that the
[`Drop` trait](https://doc.rust-lang.org/std/ops/trait.Drop.html) lets you
define what should happen when a resource is dropped.

- In
[the Blocks and Scopes chapter](../../control-flow-basics/blocks-and-scopes.md),
we saw the most common situation where a resource is dropped: when the scope
of its _owner_ ends at the boundary of a block (`{}`).

- The use of
[`std::mem::drop(val)`](https://doc.rust-lang.org/std/mem/fn.drop.html)
allows you to _move_ a value out of scope before the block ends.

- There are also other scenarios where this can happen, such as when the value
owning the resource is "shadowed" by another value:

```rust
let a = String::from("foo");
let a = 3; // ^ The previous string is dropped here
// because we shadow its binding with a new value.
```

- Recall also from [the Drop chapter](../../memory-management/drop.md) that
for a composite type such as a `struct`, all its fields will be dropped when
the struct itself is dropped. If a field implements the `Drop` trait, its
`Drop::drop` _trait_ method will also be invoked.

- In any scenario where the stack unwinds the value, it is guaranteed that the
[`Drop::drop`](https://doc.rust-lang.org/std/ops/trait.Drop.html#tymethod.drop)
method of a value `a` will be called.

- This holds true for happy paths such as:

- Exiting a block or function scope.

- Returning early with an explicit `return` statement, or implicitly by
using [the Try operator (`?`)](../../error-handling/try.md) to
early-return `Option` or `Result` values.

- It also holds for unexpected scenarios where a `panic` is triggered, if:

- The stack unwinds on panic (which is the default), allowing for graceful
cleanup of resources.

This unwind behavior can be overridden to instead
[abort on panic](https://github.com/rust-lang/rust/blob/master/library/panic_abort/src/lib.rs).

- No panic occurs within any of the `drop` methods invoked before reaching
the `drop` call of the object `a`.

- Note that
[an explicit exit of the program](https://doc.rust-lang.org/std/process/fn.exit.html),
as sometimes used in CLI tools, terminates the process immediately. In other
words, the stack is not unwound in this case, and the `drop` method will not
be called.

- `Drop` is a great fit for use cases like `Mutex`.

When the guard goes out of scope,
[`Drop::drop`](https://doc.rust-lang.org/std/ops/trait.Drop.html#tymethod.drop)
is called and unlocks the mutex automatically.

In contrast to C++ or Java, where you often have to unlock manually or use a
`lock/unlock` pattern, Rust ensures the lock _cannot_ be forgotten, thanks to
the compiler.

- In other scenarios, the `Drop` trait shows its limitations. Next, we'll look
at what those are and how we can address them.

## More to explore

To learn more about building synchronization primitives, consider reading
[_Rust Atomics and Locks_ by Mara Bos](https://marabos.nl/atomics/).

The book demonstrates, among other topics, how `Drop` and RAII work together in
constructs like `Mutex`.

</details>
91 changes: 91 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,91 @@
# Drop Bombs: Enforcing API Correctness

Use `Drop` to enforce invariants and detect incorrect API usage. A "drop bomb"
panics if not defused.

```rust
struct Transaction {
active: bool,
}

impl Transaction {
fn start() -> Self {
Self { active: true }
}

fn commit(mut self) {
self.active = false;
// Dropped after this point, no panic
}

fn rollback(mut self) {
self.active = false;
// Dropped after this point, no panic
}
}

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

<details>

- The example above uses the drop bomb pattern to enforce at runtime that a
transaction is never dropped in an unfinished state. This applies to cases
such as a database transaction that remains active in an external system.

In this example, the programmer must finalize the transaction explicitly,
either by committing it or rolling it back to undo any changes.

- In the context of FFI, where cross-boundary references are involved, it is
often necessary to ensure that manually allocated memory from the guest
language is cleaned up through an explicit call to a safe API function.

- Similar to unsafe code, it is recommended that APIs with expectations like
these are clearly documented under a Panic section. This helps ensure that
users of the API are aware of the consequences of misuse.

Ideally, drop bombs should be used only in internal APIs to catch bugs early,
without placing implicit runtime obligations on library users.

- If there is a way to restore the system to a valid state using a fallback in
the Drop implementation, it is advisable to restrict the use of drop bombs to
Debug mode. In Release mode, the Drop implementation could fall back to safe
cleanup logic while still logging the incident as an error.

- Advanced use cases might also rely on the following patterns:

- [`Option<T>` with `.take()`](https://doc.rust-lang.org/std/option/enum.Option.html#method.take):
This allows you to move out the resource in a controlled way, preventing
accidental double cleanup or use-after-drop errors.

- [`ManuallyDrop`](https://doc.rust-lang.org/std/mem/struct.ManuallyDrop.html):
A zero-cost wrapper that disables the automatic drop behavior of a value,
making manual cleanup required and explicit.

- The [`drop_bomb` crate](https://docs.rs/drop_bomb/latest/drop_bomb/) provides
a way to enforce that certain values are not dropped unless explicitly
defused. It can be added to an existing struct and exposes a `.defuse()`
method to make dropping safe. The crate also includes a `DebugDropBomb`
variant for use in debug-only builds.

## More to explore

Rust does not currently support full linear types or typestate programming in
the core language. This means the compiler cannot guarantee that a resource was
used exactly once or finalized before being dropped.

Drop bombs serve as a runtime mechanism to enforce such usage invariants
manually. This is typically done in a Drop implementation that panics if a
required method, such as `.commit()`, was not called before the value went out
of scope.

There is an open RFC issue and discussion about linear types in Rust:
<https://github.com/rust-lang/rfcs/issues/814>.

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

While `Drop` works well for cases like synchronization primitives, its use
becomes more questionable when dealing with I/O or unsafe resources.

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

fn write_log() -> io::Result<()> {
let mut file = File::create("log.txt")?;
// ^ ownership of the (OS) file handle starts here

writeln!(file, "Logging a message...")?;
Ok(())
} // file handle goes out of scope here
```

<details>

- In the earlier example, our `File` resource owns a file handle provided by the
operating system.

[As stated in the documentation](https://doc.rust-lang.org/std/fs/struct.File.html):

> Files are automatically closed when they go out of scope. Errors detected on
> closing are ignored by the implementation of Drop.

- This highlights a key limitation of the `Drop` trait: it cannot propagate
errors to the caller. In other words, fallible cleanup logic cannot be handled
by the code using the `File`.

This becomes clear when looking at the
[definition of the `Drop` trait](https://doc.rust-lang.org/std/ops/trait.Drop.html):

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

Since `drop` does not return a `Result`, any error that occurs during cleanup
cannot be surfaced or recovered from. This is by design: `drop` is invoked
automatically when a value is popped off the stack during unwinding, leaving
no opportunity for error handling.

- One workaround is to panic inside `drop` when a failure occurs. However, this
is risky—if a panic happens while the stack is already unwinding, the program
will abort immediately, and remaining resources will not be cleaned up.

While panicking in `drop` can serve certain purposes (see
[the next chapter on "drop bombs"](./drop_bomb.md)), it should be used
sparingly and with full awareness of the consequences.

- Another drawback of `drop` is that its execution is implicit and
non-deterministic in terms of timing. You cannot control _when_ a value is
dropped. And in fact as discussed in previous slide it might never even run at
all, leaving the external resource in an undefined state.

This matters particularly for I/O: normally you might set a timeout on
blocking operations, but when I/O occurs in a `drop` implementation, you have
no way to enforce such constraints.

Returning to the `File` example: if the file handle hangs during close (e.g.,
due to OS-level buffering or locking), the drop operation could block
indefinitely. Since the call to `drop` happens implicitly and outside your
control, there's no way to apply a timeout or fallback mechanism.

- For smart pointers and synchronization primitives, none of these drawbacks
matter, since the operations are nearly instant and a program panic does not
cause undefined behavior. The poisoned state disappears along with the
termination of the program.

- For use cases such as I/O or FFI, it may be preferable to let the user clean
up resources explicitly using a close function.

However, this approach cannot be enforced at the type level. If explicit
cleanup is part of your API contract, you might choose to panic in drop when
the resource has not been properly closed. This can help catch contract
violations at runtime.

This is one situation where drop bombs are useful, which we will discuss next.

</details>
Loading
Loading