1
0
mirror of https://github.com/google/comprehensive-rust.git synced 2025-08-08 08:22:52 +02:00

apply feedback RAII and rewrite; draft 1

This commit is contained in:
Glen De Cauwsemaecker
2025-08-02 10:34:58 +02:00
parent 9fa819a86c
commit 4ebb43f663
5 changed files with 321 additions and 14 deletions

View File

@ -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)
---

View File

@ -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)

View File

@ -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
}
```
<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. 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<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>

View File

@ -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<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
}
}
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:?}");
}
```
<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>
### Mutex Lock Lifecycle
```bob
+---------------+ +----------------------+
| Mutex<T> | lock | MutexGuard<T> |
| ( Unlocked ) +-------->| ( Exclusive Access ) |
+---------------+ +-------+--------------+
|
| drop
v
+---------------+ yes +-------------------+
| Mutex<T> |<------+ Thread panicking? |
| ( Poisoned ) | +-------+-----------+
+------+--------+ | no
| v
| +---------------+
| lock | Mutex<T> |
| | ( Unlocked ) |
| +---------------+
v
+------------------+
| Err ( Poisoned ) |
+------------------+
```
</details>

View File

@ -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
}
```
<details>
- 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: <TODO>
## 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.
</details>