diff --git a/src/SUMMARY.md b/src/SUMMARY.md index 5450b7e6..04267a6c 100644 --- a/src/SUMMARY.md +++ b/src/SUMMARY.md @@ -438,6 +438,9 @@ - [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) --- diff --git a/src/idiomatic/leveraging-the-type-system/_old/raii/scope_guards.md b/src/idiomatic/leveraging-the-type-system/_old/raii/scope_guards.md index 273b8ac9..829c85b0 100644 --- a/src/idiomatic/leveraging-the-type-system/_old/raii/scope_guards.md +++ b/src/idiomatic/leveraging-the-type-system/_old/raii/scope_guards.md @@ -146,21 +146,11 @@ fn main() { that's in the slide? I can slap together a sketch in the playground if it's not clear what I'm suggesting. ``` - - - The `ScopeGuard` type in the `scopeguard` crate also includes a `Debug` - implementation and a third parameter: a + - `scopeguard` also supports selecting a [`Strategy`](https://docs.rs/scopeguard/latest/scopeguard/trait.Strategy.html) - that determines when the `drop_fn` should run. - - - By default, the strategy runs the drop function unconditionally. However, - the crate also provides built-in strategies to run the drop function only - during unwinding (due to a panic), or only on successful scope exit. - - You can also implement your own `Strategy` trait to define custom - conditions for when the cleanup should occur. - - TODO: again... more concise, e.g. reduce the above to: - + to determine when the cleanup logic should run, i.e. always, only on + successful exit, or only on unwind. The crate also supports defining custom + strategies. ``` - `scopeguard` also supports selecting a [`Strategy`](https://docs.rs/scopeguard/latest/scopeguard/trait.Strategy.html) diff --git a/src/idiomatic/leveraging-the-type-system/raii/drop_bomb.md b/src/idiomatic/leveraging-the-type-system/raii/drop_bomb.md new file mode 100644 index 00000000..3cea9057 --- /dev/null +++ b/src/idiomatic/leveraging-the-type-system/raii/drop_bomb.md @@ -0,0 +1,110 @@ +# 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 +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) { + 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 rollback!"); + } + } +} + +fn main() { + // OK: commit defuses the bomb + let tx1 = Transaction::start(); + tx1.commit(); + + // Uncomment to see the panic: + // let tx2 = Transaction::start(); + // dropped without commit or rollback → panic +} +``` + +
+ +- 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. For example, most databases do + not allow rollback to be safely handled inside `drop()` alone. + +- 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` 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: + + +
diff --git a/src/idiomatic/leveraging-the-type-system/raii/mutex.md b/src/idiomatic/leveraging-the-type-system/raii/mutex.md new file mode 100644 index 00000000..f883abdb --- /dev/null +++ b/src/idiomatic/leveraging-the-type-system/raii/mutex.md @@ -0,0 +1,127 @@ +# 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 { + value: std::cell::UnsafeCell, + // [...] +} + +#[derive(Debug)] +struct MutexGuard<'a, T> { + value: &'a mut T, + // [...] +} + +impl Mutex { + fn new(value: T) -> Self { + Self { + value: std::cell::UnsafeCell::new(value), + // [...] + } + } + + fn lock(&self) -> MutexGuard { + // [...] + 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 + } +} + +impl<'a, T> Drop for MutexGuard<'a, T> { + fn drop(&mut self) { + // [...] + println!("drop MutexGuard"); + } +} + +fn main() { + let m = Mutex::new(vec![1, 2, 3]); + + let mut guard = m.lock(); + guard.push(4); + guard.push(5); + println!("{guard:?}"); +} +``` + +
+ +- 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: + + +### Mutex Lock Lifecycle + +```bob ++---------------+ +----------------------+ +| Mutex | lock | MutexGuard | +| ( Unlocked ) +-------->| ( Exclusive Access ) | ++---------------+ +-------+--------------+ + | + | drop + v ++---------------+ yes +-------------------+ +| Mutex |<------+ Thread panicking? | +| ( Poisoned ) | +-------+-----------+ ++------+--------+ | no + | v + | +---------------+ + | lock | Mutex | + | | ( Unlocked ) | + | +---------------+ + v ++------------------+ +| Err ( Poisoned ) | ++------------------+ +``` + +
diff --git a/src/idiomatic/leveraging-the-type-system/raii/scope_guard.md b/src/idiomatic/leveraging-the-type-system/raii/scope_guard.md new file mode 100644 index 00000000..fb45e7a3 --- /dev/null +++ b/src/idiomatic/leveraging-the-type-system/raii/scope_guard.md @@ -0,0 +1,77 @@ +# Scope Guards + +A scope guard uses the `Drop` trait to ensure cleanup code runs automatically +when a scope exits — even if due to an error. + +```rust,editable,compile_fail +use scopeguard::{ScopeGuard, guard}; +use std::{ + fs::{self, File}, + io::Write, +}; + +fn download_successful() -> bool { + true // change to false to simulate failure +} + +fn main() { + let path = "download.tmp"; + let mut file = File::create(path).expect("cannot create temporary file"); + + // Set up cleanup immediately after file creation + let cleanup = guard(path, |path| { + println!("download failed, deleting: {:?}", path); + let _ = fs::remove_file(path); + }); + + writeln!(file, "partial data...").unwrap(); + + if download_successful() { + // Download succeeded, keep the file + let _path = ScopeGuard::into_inner(cleanup); + println!("Download complete!"); + } + // Otherwise, the guard runs and deletes the file +} +``` + +
+ +- This example simulates an HTTP download. We create a temporary file first, + then use a scope guard to ensure that the file is deleted if the download + fails. + +- The guard is placed directly after creating the file, so even if `writeln!()` + fails, the file will still be cleaned up. This ordering is essential for + correctness. + +- The guard's closure runs on scope exit unless defused with + `ScopeGuard::into_inner`. In the success path, we defuse it to preserve the + file. + +- This pattern is useful when you want fallbacks or cleanup code to run + automatically but only if success is not explicitly signaled. + +- The `scopeguard` crate also supports cleanup strategies via the + [`Strategy`](https://docs.rs/scopeguard/latest/scopeguard/trait.Strategy.html) + trait. You can choose to run the guard on unwind only, or on success only, not + just always. + +## Manual Implementation Example + +If you need a custom scope guard for a very specific task, you can implement one +manually. Here's a standalone example that mirrors the file deletion scenario +shown above: + +## Related Patterns + +- Recall from the [Drop Bombs](./drop_bomb.md) chapter: drop bombs enforce that + a resource is finalized. Scope guards take that further — they let you define + automatic fallback behavior for cleanup when a resource is _not_ explicitly + finalized. + +- This pattern works well in combination with `Drop`, especially in fallible or + multi-step operations where cleanup needs to be predictable, regardless of + which step failed. + +