Skip to content
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
39ae8f6
initial version of the raii chapters for idiomatic rust
GlenDC Jul 15, 2025
f4269b7
apply dprint fmt
GlenDC Jul 15, 2025
bb88a79
fix build (en) step: scopeguard crate import
GlenDC Jul 15, 2025
5a4838e
integrate feedback of first reviews (RAII)
GlenDC Jul 26, 2025
d804144
add new `RAII` intro segment
GlenDC Jul 26, 2025
5c2ec8c
further improvements to RAII intro chapter
GlenDC Jul 27, 2025
0baa990
fix typo
GlenDC Jul 27, 2025
9fa819a
improve RAII intro segment further based on Luca's feedbacl
GlenDC Aug 1, 2025
4ebb43f
apply feedback RAII and rewrite; draft 1
GlenDC Aug 2, 2025
42a4237
improve drop bomb code example
GlenDC Aug 2, 2025
2923cf3
prepare raii chapter for next reviews
GlenDC Aug 3, 2025
48c5baa
fix raii drop bomb example
GlenDC Aug 3, 2025
cc5f3b5
address feedback 1/2 of @randomPoison
GlenDC Aug 30, 2025
55e4753
address @randomPoison feedback 2/2
GlenDC Aug 30, 2025
1d67c9b
Merge branch 'main' into raii
GlenDC Sep 29, 2025
4c408b7
address more feedback
GlenDC Nov 3, 2025
8f017de
Merge branch 'main' into raii
GlenDC Nov 3, 2025
26c92f4
address more feedback
GlenDC Nov 30, 2025
1b1a3ab
Merge branch 'main' into raii
GlenDC Nov 30, 2025
67cefcc
run dprint fmt
GlenDC Nov 30, 2025
3789e70
apply rem. feedback to raii slides + introduce drop_skipped slide
GlenDC Nov 30, 2025
4a79ba4
add 2 extra slides
GlenDC Nov 30, 2025
cacc981
improve the raii chapter slides based on the new STYLE guidelines
GlenDC Nov 30, 2025
499d0c5
apply feedback from Kevin B.
GlenDC Dec 1, 2025
bc2cf46
apply rem. feedback from previously unresolved thread
GlenDC Dec 1, 2025
169f357
address 1/2 of feedback from yesterday
GlenDC Dec 2, 2025
809fc4e
address more feedback regarding existing slides + 1 new slide
GlenDC Dec 2, 2025
897f7b2
improve drop skipped example
GlenDC Dec 2, 2025
45bdc0a
add timings to RAII chapter
GlenDC Dec 5, 2025
b423c45
address feedback on RAII chapters 12-05
GlenDC Dec 5, 2025
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
5 changes: 5 additions & 0 deletions src/SUMMARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -437,6 +437,11 @@
- [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)
- [Scope Guard](idiomatic/leveraging-the-type-system/raii/scope_guard.md)

---

Expand Down
112 changes: 112 additions & 0 deletions src/idiomatic/leveraging-the-type-system/raii.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
---
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.

- 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>
85 changes: 85 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,85 @@
# Drop Guards

A **drop guard** in Rust is a temporary _RAII_ guard that executes a specific
action when it goes out of scope.

It acts as a wrapper around a value, ensuring that some cleanup or secondary
behavior happens automatically when the guard is dropped.

One of the most common examples is `MutexGuard`, which represents temporary
exclusive access to a shared resource.

```rust
#[derive(Debug)]
struct Mutex<T> {
value: std::cell::UnsafeCell<T>,
is_locked: std::sync::atomic::AtomicBool,
}

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

impl<T> Mutex<T> {
fn new(value: T) -> Self {
Self {
value: std::cell::UnsafeCell::new(value),
is_locked: std::sync::atomic::AtomicBool::new(false),
}
}

fn lock(&self) -> MutexGuard<'_, T> {
// Acquire the lock and create the guard object.
if self.is_locked.swap(true, std::sync::atomic::Ordering::AcqRel) {
todo!("Block until the lock is released");
}
let value = unsafe { &mut *self.value.get() };
MutexGuard { value, mutex: self }
}
}

impl<'a, T> Drop for MutexGuard<'a, T> {
fn drop(&mut self) {
self.mutex.is_locked.store(false, std::sync::atomic::Ordering::Release);
}
}

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

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

<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 enforces exclusive access, and its `Drop` implementation
guarantees that the lock is released when the guard goes out of scope.

- A few things are left out for brevity:

- `Deref` and `DerefMut` implementations for `MutexGuard`, which would allow
you to use the guard as if it were a direct reference to the inner value.
- Making `.lock()` truly blocking, so that it waits until the mutex is free
before returning.
- In addition, a `.try_lock()` method could be added to provide a
non-blocking alternative, returning `Option::None` or `Result::Err(...)`
if the mutex is still locked.

- Panics are not explicitly handled in the `Drop` implementation here. In
practice, one can use `std::thread::panicking()` to check if the guard was
dropped during a panic.

- The standard library’s `std::sync::Mutex` uses this to implement
**poisoning**, where a mutex is marked as poisoned if a panic occurs while
holding the lock, since the protected value may now be in an inconsistent
state.

</details>
54 changes: 54 additions & 0 deletions src/idiomatic/leveraging-the-type-system/raii/mutex.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
# Mutex and MutexGuard

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
use std::sync::Mutex;

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

let mut guard = m.lock().unwrap();
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.

</details>
Loading
Loading