You've already forked comprehensive-rust
mirror of
https://github.com/google/comprehensive-rust.git
synced 2025-08-08 08:22:52 +02:00
initial version of the raii chapters for idiomatic rust
This commit is contained in:
@ -437,7 +437,10 @@
|
||||
- [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)
|
||||
- [Drop Limitations](idiomatic/leveraging-the-type-system/raii/drop_limitations.md)
|
||||
- [Drop Bomb](idiomatic/leveraging-the-type-system/raii/drop_bomb.md)
|
||||
- [Scope Guards](idiomatic/leveraging-the-type-system/raii/scope_guards.md)
|
||||
---
|
||||
|
||||
# Final Words
|
||||
|
116
src/idiomatic/leveraging-the-type-system/raii.md
Normal file
116
src/idiomatic/leveraging-the-type-system/raii.md
Normal file
@ -0,0 +1,116 @@
|
||||
---
|
||||
minutes: 30
|
||||
---
|
||||
|
||||
# RAII and `Drop` in Practice
|
||||
|
||||
RAII (*Resource Acquisition Is Initialization*)
|
||||
means tying the lifetime of a resource to the lifetime of a value.
|
||||
|
||||
Rust applies RAII automatically for memory management.
|
||||
The `Drop` trait lets you extend this pattern to anything else.
|
||||
|
||||
```rust
|
||||
use std::sync::Mutex;
|
||||
|
||||
fn main() {
|
||||
let mux = Mutex::new(vec![1, 2, 3]);
|
||||
|
||||
{
|
||||
let mut data = mux.lock().unwrap();
|
||||
data.push(4); // lock held here
|
||||
} // lock automatically released here
|
||||
}
|
||||
```
|
||||
|
||||
<details>
|
||||
|
||||
- In the above example
|
||||
[the `Mutex`](https://doc.rust-lang.org/std/sync/struct.Mutex.html)
|
||||
owns its data: you can’t access the value inside without first acquiring the lock.
|
||||
|
||||
`mux.lock()` returns a
|
||||
[`MutexGuard`](https://doc.rust-lang.org/std/sync/struct.MutexGuard.html),
|
||||
which [dereferences](https://doc.rust-lang.org/std/ops/trait.DerefMut.html)
|
||||
to the data and implements [`Drop`](https://doc.rust-lang.org/std/ops/trait.Drop.html).
|
||||
|
||||
- You may recall from [the Memory Management chapter](../../memory-management/drop.md)
|
||||
that the [`Drop` trait](https://doc.rust-lang.org/std/ops/trait.Drop.html)
|
||||
lets you define what should happen when a resource is dropped.
|
||||
|
||||
- In [the Blocks and Scopes chapter](../../control-flow-basics/blocks-and-scopes.md),
|
||||
we saw the most common situation where a resource is dropped:
|
||||
when the scope of its _owner_ ends at the boundary of a block (`{}`).
|
||||
|
||||
- The use of
|
||||
[`std::mem::drop(val)`](https://doc.rust-lang.org/std/mem/fn.drop.html)
|
||||
allows you to _move_ a value out of scope before the block ends.
|
||||
|
||||
- There are also other scenarios where this can happen,
|
||||
such as when the value owning the resource is "shadowed" by another value:
|
||||
|
||||
```rust
|
||||
let a = String::from("foo");
|
||||
let a = 3; // ^ The previous string is dropped here
|
||||
// because we shadow its binding with a new value.
|
||||
```
|
||||
|
||||
- Recall also from [the Drop chapter](../../memory-management/drop.md)
|
||||
that for a composite type such as a `struct`, all its fields will be dropped
|
||||
when the struct itself is dropped.
|
||||
If a field implements the `Drop` trait, its `Drop::drop`
|
||||
_trait_ method will also be invoked.
|
||||
|
||||
- In any scenario where the stack unwinds the value, it is guaranteed
|
||||
that the [`Drop::drop`](https://doc.rust-lang.org/std/ops/trait.Drop.html#tymethod.drop)
|
||||
method of a value `a` will be called.
|
||||
|
||||
- This holds true for happy paths such as:
|
||||
|
||||
- Exiting a block or function scope.
|
||||
|
||||
- Returning early with an explicit `return` statement,
|
||||
or implicitly by using
|
||||
[the Try operator (`?`)](../../error-handling/try.md)
|
||||
to early-return `Option` or `Result` values.
|
||||
|
||||
- It also holds for unexpected scenarios where a `panic` is triggered, if:
|
||||
|
||||
- The stack unwinds on panic (which is the default),
|
||||
allowing for graceful cleanup of resources.
|
||||
|
||||
This unwind behavior can be overridden to instead
|
||||
[abort on panic](https://github.com/rust-lang/rust/blob/master/library/panic_abort/src/lib.rs).
|
||||
|
||||
- No panic occurs within any of the `drop` methods
|
||||
invoked before reaching the `drop` call of the object `a`.
|
||||
|
||||
- Note that
|
||||
[an explicit exit of the program](https://doc.rust-lang.org/std/process/fn.exit.html),
|
||||
as sometimes used in CLI tools, terminates the process immediately.
|
||||
In other words, the stack is not unwound in this case,
|
||||
and the `drop` method will not be called.
|
||||
|
||||
- `Drop` is a great fit for use cases like `Mutex`.
|
||||
|
||||
When the guard goes out of scope, [`Drop::drop`](https://doc.rust-lang.org/std/ops/trait.Drop.html#tymethod.drop)
|
||||
is called and unlocks the mutex automatically.
|
||||
|
||||
In contrast to C++ or Java, where you often have to unlock manually
|
||||
or use a `lock/unlock` pattern, Rust ensures the
|
||||
lock *cannot* be forgotten, thanks to the compiler.
|
||||
|
||||
- In other scenarios, the `Drop` trait shows its limitations.
|
||||
Next, we'll look at what those are and how we can
|
||||
address them.
|
||||
|
||||
## More to explore
|
||||
|
||||
To learn more about building synchronization primitives,
|
||||
consider reading [*Rust Atomics and Locks* by Mara Bos](https://marabos.nl/atomics/).
|
||||
|
||||
The book demonstrates, among other topics, how `Drop`
|
||||
and RAII work together in constructs like `Mutex`.
|
||||
|
||||
|
||||
</details>
|
89
src/idiomatic/leveraging-the-type-system/raii/drop_bomb.md
Normal file
89
src/idiomatic/leveraging-the-type-system/raii/drop_bomb.md
Normal file
@ -0,0 +1,89 @@
|
||||
# Drop Bombs: Enforcing API Correctness
|
||||
|
||||
Use `Drop` to enforce invariants and detect incorrect API usage.
|
||||
A "drop bomb" panics if not defused.
|
||||
|
||||
```rust
|
||||
struct Transaction {
|
||||
active: bool,
|
||||
}
|
||||
|
||||
impl Transaction {
|
||||
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 roll back!");
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
<details>
|
||||
|
||||
- The example above uses the drop bomb pattern to enforce at runtime that a transaction
|
||||
is never dropped in an unfinished state. This applies to cases such as a database
|
||||
transaction that remains active in an external system.
|
||||
|
||||
In this example, the programmer must finalize the transaction explicitly,
|
||||
either by committing it or rolling it back to undo any changes.
|
||||
|
||||
- In the context of FFI, where cross-boundary references are involved, it is often necessary
|
||||
to ensure that manually allocated memory from the guest language is cleaned up through
|
||||
an explicit call to a safe API function.
|
||||
|
||||
- Similar to unsafe code, it is recommended that APIs with expectations like these
|
||||
are clearly documented under a Panic section. This helps ensure that users of the API
|
||||
are aware of the consequences of misuse.
|
||||
|
||||
Ideally, drop bombs should be used only in internal APIs to catch bugs early,
|
||||
without placing implicit runtime obligations on library users.
|
||||
|
||||
- If there is a way to restore the system to a valid state using a fallback
|
||||
in the Drop implementation, it is advisable to restrict the use of drop bombs
|
||||
to Debug mode. In Release mode, the Drop implementation could fall back to
|
||||
safe cleanup logic while still logging the incident as an error.
|
||||
|
||||
- Advanced use cases might also rely on the following patterns:
|
||||
|
||||
- [`Option<T>` with `.take()`](https://doc.rust-lang.org/std/option/enum.Option.html#method.take):
|
||||
This allows you to move out the resource in a controlled way, preventing
|
||||
accidental double cleanup or use-after-drop errors.
|
||||
|
||||
- [`ManuallyDrop`](https://doc.rust-lang.org/std/mem/struct.ManuallyDrop.html):
|
||||
A zero-cost wrapper that disables the automatic drop behavior of a value,
|
||||
making manual cleanup required and explicit.
|
||||
|
||||
- The [`drop_bomb` crate](https://docs.rs/drop_bomb/latest/drop_bomb/)
|
||||
provides a way to enforce that certain values are not dropped unless explicitly defused.
|
||||
It can be added to an existing struct and exposes a `.defuse()` method to make dropping safe.
|
||||
The crate also includes a `DebugDropBomb` variant for use in debug-only builds.
|
||||
|
||||
## More to explore
|
||||
|
||||
Rust does not currently support full linear types or typestate programming
|
||||
in the core language. This means the compiler cannot guarantee that a resource
|
||||
was used exactly once or finalized before being dropped.
|
||||
|
||||
Drop bombs serve as a runtime mechanism to enforce such usage invariants manually.
|
||||
This is typically done in a Drop implementation that panics if a required method,
|
||||
such as `.commit()`, was not called before the value went out of scope.
|
||||
|
||||
There is an open RFC issue and discussion about linear types in Rust:
|
||||
<https://github.com/rust-lang/rfcs/issues/814>.
|
||||
|
||||
</details>
|
@ -0,0 +1,85 @@
|
||||
# The limitations of `Drop`
|
||||
|
||||
While `Drop` works well for cases
|
||||
like synchronization primitives, its use becomes more
|
||||
questionable when dealing with I/O or unsafe resources.
|
||||
|
||||
```rust
|
||||
use std::fs::File;
|
||||
use std::io::{self, Write};
|
||||
|
||||
fn write_log() -> io::Result<()> {
|
||||
let mut file = File::create("log.txt")?;
|
||||
// ^ ownership of the (OS) file handle starts here
|
||||
|
||||
writeln!(file, "Logging a message...")?;
|
||||
Ok(())
|
||||
} // file handle goes out of scope here
|
||||
```
|
||||
|
||||
<details>
|
||||
|
||||
- In the earlier example, our `File` resource owns a file handle
|
||||
provided by the operating system.
|
||||
|
||||
[As stated in the documentation](https://doc.rust-lang.org/std/fs/struct.File.html):
|
||||
|
||||
> Files are automatically closed when they go out of scope.
|
||||
> Errors detected on closing are ignored by the implementation of Drop.
|
||||
|
||||
- This highlights a key limitation of the `Drop` trait:
|
||||
it cannot propagate errors to the caller. In other words,
|
||||
fallible cleanup logic cannot be handled by the code using the `File`.
|
||||
|
||||
This becomes clear when looking at the
|
||||
[definition of the `Drop` trait](https://doc.rust-lang.org/std/ops/trait.Drop.html):
|
||||
|
||||
```rust
|
||||
trait Drop {
|
||||
fn drop(&mut self);
|
||||
}
|
||||
```
|
||||
|
||||
Since `drop` does not return a `Result`, any error that occurs during cleanup
|
||||
cannot be surfaced or recovered from. This is by design:
|
||||
`drop` is invoked automatically when a value is popped off the stack during
|
||||
unwinding, leaving no opportunity for error handling.
|
||||
|
||||
- One workaround is to panic inside `drop` when a failure occurs.
|
||||
However, this is risky—if a panic happens while the stack is already unwinding,
|
||||
the program will abort immediately, and remaining resources will not be cleaned up.
|
||||
|
||||
While panicking in `drop` can serve certain purposes (see
|
||||
[the next chapter on "drop bombs"](./drop_bomb.md)), it should be used sparingly
|
||||
and with full awareness of the consequences.
|
||||
|
||||
- Another drawback of `drop` is that its execution is implicit and non-deterministic
|
||||
in terms of timing. You cannot control *when* a value is dropped. And in fact as
|
||||
discussed in previous slide it might never even run at all, leaving the external
|
||||
resource in an undefined state.
|
||||
|
||||
This matters particularly for I/O: normally you might set a timeout on blocking
|
||||
operations, but when I/O occurs in a `drop` implementation, you have no way to
|
||||
enforce such constraints.
|
||||
|
||||
Returning to the `File` example: if the file handle hangs during close (e.g.,
|
||||
due to OS-level buffering or locking), the drop operation could block indefinitely.
|
||||
Since the call to `drop` happens implicitly and outside your control,
|
||||
there's no way to apply a timeout or fallback mechanism.
|
||||
|
||||
- For smart pointers and synchronization primitives, none of these drawbacks matter,
|
||||
since the operations are nearly instant and a program panic does not cause undefined behavior.
|
||||
The poisoned state disappears along with the termination of the program.
|
||||
|
||||
- For use cases such as I/O or FFI, it may be preferable to let the user
|
||||
clean up resources explicitly using a close function.
|
||||
|
||||
However, this approach cannot be enforced at the type level.
|
||||
If explicit cleanup is part of your API contract, you might choose to
|
||||
panic in drop when the resource has not been properly closed.
|
||||
This can help catch contract violations at runtime.
|
||||
|
||||
This is one situation where drop bombs are useful,
|
||||
which we will discuss next.
|
||||
|
||||
</details>
|
130
src/idiomatic/leveraging-the-type-system/raii/scope_guards.md
Normal file
130
src/idiomatic/leveraging-the-type-system/raii/scope_guards.md
Normal file
@ -0,0 +1,130 @@
|
||||
# Scope Guards
|
||||
|
||||
A scope guard makes use of the `Drop` trait
|
||||
to run a given closure when it goes out of scope.
|
||||
|
||||
```rust
|
||||
use std::{io::Write, fs::{self, File}};
|
||||
use scopeguard::{guard, ScopeGuard};
|
||||
|
||||
fn conditional_success() -> bool { true }
|
||||
|
||||
fn main() {
|
||||
let path = "temp.txt";
|
||||
let mut file = File::create(path).expect("cannot create file");
|
||||
|
||||
// Write something to the file
|
||||
writeln!(file, "temporary data").unwrap();
|
||||
|
||||
// Create a scope guard to clean up the file unless we defuse it
|
||||
let cleanup = guard(path, |path| {
|
||||
// Errors must be handled inside the guard,
|
||||
// but cannot be propagated.
|
||||
let _ = fs::remove_file(path);
|
||||
});
|
||||
|
||||
if conditional_success() {
|
||||
// Success path: we want to keep the file
|
||||
let path = ScopeGuard::into_inner(cleanup);
|
||||
} else {
|
||||
// Otherwise, the guard remains active and deletes the file on scope exit
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
<details>
|
||||
|
||||
- This example demonstrates the use of
|
||||
[the `scopeguard` crate](https://docs.rs/scopeguard/latest/scopeguard/),
|
||||
which is commonly used in internal APIs to ensure that a closure runs
|
||||
when a scope exits.
|
||||
|
||||
- If the cleanup logic in the example above were unconditional,
|
||||
the code could be simplified using
|
||||
[scopeguard's `defer!` macro](https://docs.rs/scopeguard/latest/scopeguard/#defer):
|
||||
|
||||
```rust
|
||||
let path = "temp.txt";
|
||||
|
||||
scopeguard::defer! {
|
||||
let _ = std::fs::remove_file(path);
|
||||
}
|
||||
```
|
||||
|
||||
- If desired, the "scope guard" pattern can be implemented manually,
|
||||
starting as follows:
|
||||
|
||||
```rust
|
||||
struct ScopeGuard<T, F: FnOnce()> {
|
||||
value: Option<T>,
|
||||
drop_fn: Option<F>,
|
||||
}
|
||||
|
||||
impl<T, F: FnOnce()> ScopeGuard<T, F> {
|
||||
fn guard(value: T, drop_fn: F) -> Self {
|
||||
Self { value: Some(value), drop_fn: Some(drop_fn) }
|
||||
}
|
||||
|
||||
fn into_inner(mut self) -> T {
|
||||
// The drop function is discarded and will not run
|
||||
self.value.take().unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, F: FnOnce()> Drop for ScopeGuard<T, F> {
|
||||
fn drop(&mut self) {
|
||||
// Run the drop function when the guard goes out of scope.
|
||||
// Note: if `into_inner` was called earlier, the drop function won't run.
|
||||
if let Some(f) = self.drop_fn.take() {
|
||||
f();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, F: FnOnce()> std::ops::Deref for ScopeGuard<T, F> {
|
||||
type Target = T;
|
||||
|
||||
fn deref(&self) -> &T {
|
||||
// Provide shared access to the underlying value
|
||||
self.value.as_ref().unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, F: FnOnce()> std::ops::DerefMut for ScopeGuard<T, F> {
|
||||
fn deref_mut(&mut self) -> &mut T {
|
||||
// Provide exclusive access to the underlying value
|
||||
self.value.as_mut().unwrap()
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- 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)
|
||||
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.
|
||||
|
||||
- Remark also that the crates' `ScopeGuard` makes use of
|
||||
[`ManuallyDrop`](https://doc.rust-lang.org/std/mem/struct.ManuallyDrop.html)
|
||||
instead of `Option` to avoid automatic or premature dropping
|
||||
of values, giving precise manual control and preventing
|
||||
double-drops. This avoids the runtime overhead and semantic ambiguity that comes with using Option.
|
||||
|
||||
- Recalling the transaction example from
|
||||
[the drop bombs chapter](./drop_bomb.md),
|
||||
we can now combine both concepts:
|
||||
define a fallback that runs unless we explicitly abort early.
|
||||
In the success path, we call `ScopeGuard::into_inner`
|
||||
to prevent the rollback, as the transaction has already been committed.
|
||||
|
||||
While we still cannot propagate errors from fallible operations inside the drop logic,
|
||||
this pattern at least allows us to orchestrate fallbacks explicitly
|
||||
and with whatever guarantees or limits we require.
|
||||
|
||||
</details>
|
Reference in New Issue
Block a user