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)
|
- [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)
|
- [Is It Encapsulated?](idiomatic/leveraging-the-type-system/newtype-pattern/is-it-encapsulated.md)
|
||||||
- [RAII](idiomatic/leveraging-the-type-system/raii.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
|
that's in the slide? I can slap together a sketch in the playground if
|
||||||
it's not clear what I'm suggesting.
|
it's not clear what I'm suggesting.
|
||||||
```
|
```
|
||||||
|
- `scopeguard` also supports selecting a
|
||||||
- The `ScopeGuard` type in the `scopeguard` crate also includes a `Debug`
|
|
||||||
implementation and a third parameter: a
|
|
||||||
[`Strategy`](https://docs.rs/scopeguard/latest/scopeguard/trait.Strategy.html)
|
[`Strategy`](https://docs.rs/scopeguard/latest/scopeguard/trait.Strategy.html)
|
||||||
that determines when the `drop_fn` should run.
|
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
|
||||||
- By default, the strategy runs the drop function unconditionally. However,
|
strategies.
|
||||||
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:
|
|
||||||
|
|
||||||
```
|
```
|
||||||
- `scopeguard` also supports selecting a
|
- `scopeguard` also supports selecting a
|
||||||
[`Strategy`](https://docs.rs/scopeguard/latest/scopeguard/trait.Strategy.html)
|
[`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