You've already forked comprehensive-rust
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:
@ -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)
|
||||
|
||||
---
|
||||
|
||||
|
@ -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)
|
||||
|
110
src/idiomatic/leveraging-the-type-system/raii/drop_bomb.md
Normal file
110
src/idiomatic/leveraging-the-type-system/raii/drop_bomb.md
Normal 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>
|
127
src/idiomatic/leveraging-the-type-system/raii/mutex.md
Normal file
127
src/idiomatic/leveraging-the-type-system/raii/mutex.md
Normal 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>
|
77
src/idiomatic/leveraging-the-type-system/raii/scope_guard.md
Normal file
77
src/idiomatic/leveraging-the-type-system/raii/scope_guard.md
Normal 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>
|
Reference in New Issue
Block a user