-
Notifications
You must be signed in to change notification settings - Fork 1.9k
RAII chapter for idiomatic rust #2820
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
GlenDC
wants to merge
14
commits into
main
Choose a base branch
from
raii
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from 3 commits
Commits
Show all changes
14 commits
Select commit
Hold shift + click to select a range
39ae8f6
initial version of the raii chapters for idiomatic rust
GlenDC f4269b7
apply dprint fmt
GlenDC bb88a79
fix build (en) step: scopeguard crate import
GlenDC 5a4838e
integrate feedback of first reviews (RAII)
GlenDC d804144
add new `RAII` intro segment
GlenDC 5c2ec8c
further improvements to RAII intro chapter
GlenDC 0baa990
fix typo
GlenDC 9fa819a
improve RAII intro segment further based on Luca's feedbacl
GlenDC 4ebb43f
apply feedback RAII and rewrite; draft 1
GlenDC 42a4237
improve drop bomb code example
GlenDC 2923cf3
prepare raii chapter for next reviews
GlenDC 48c5baa
fix raii drop bomb example
GlenDC cc5f3b5
address feedback 1/2 of @randomPoison
GlenDC 55e4753
address @randomPoison feedback 2/2
GlenDC File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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]); | ||
GlenDC marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
{ | ||
let mut data = mux.lock().unwrap(); | ||
data.push(4); // lock held here | ||
} // lock automatically released here | ||
} | ||
GlenDC marked this conversation as resolved.
Show resolved
Hide resolved
|
||
``` | ||
|
||
<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). | ||
GlenDC marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
- You may recall from | ||
[the Memory Management chapter](../../memory-management/drop.md) that the | ||
GlenDC marked this conversation as resolved.
Show resolved
Hide resolved
|
||
[`Drop` trait](https://doc.rust-lang.org/std/ops/trait.Drop.html) lets you | ||
define what should happen when a resource is dropped. | ||
|
||
- In | ||
GlenDC marked this conversation as resolved.
Show resolved
Hide resolved
|
||
[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. | ||
GlenDC marked this conversation as resolved.
Show resolved
Hide resolved
|
||
``` | ||
|
||
- 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 | ||
GlenDC marked this conversation as resolved.
Show resolved
Hide resolved
|
||
[`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). | ||
GlenDC marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
- 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. | ||
GlenDC marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
- `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 | ||
GlenDC marked this conversation as resolved.
Show resolved
Hide resolved
|
||
`lock/unlock` pattern, Rust ensures the lock _cannot_ be forgotten, thanks to | ||
the compiler. | ||
GlenDC marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
- 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> | ||
GlenDC marked this conversation as resolved.
Show resolved
Hide resolved
|
91 changes: 91 additions & 0 deletions
91
src/idiomatic/leveraging-the-type-system/raii/drop_bomb.md
GlenDC marked this conversation as resolved.
Show resolved
Hide resolved
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,91 @@ | ||
# Drop Bombs: Enforcing API Correctness | ||
|
||
Use `Drop` to enforce invariants and detect incorrect API usage. A "drop bomb" | ||
panics if not defused. | ||
GlenDC marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
```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!"); | ||
GlenDC marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} | ||
} | ||
} | ||
``` | ||
|
||
<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 | ||
GlenDC marked this conversation as resolved.
Show resolved
Hide resolved
|
||
language is cleaned up through an explicit call to a safe API function. | ||
GlenDC marked this conversation as resolved.
Show resolved
Hide resolved
GlenDC marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
- 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. | ||
GlenDC marked this conversation as resolved.
Show resolved
Hide resolved
GlenDC marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
Ideally, drop bombs should be used only in internal APIs to catch bugs early, | ||
without placing implicit runtime obligations on library users. | ||
GlenDC marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
- 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. | ||
GlenDC marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
- 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. | ||
GlenDC marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
- [`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. | ||
GlenDC marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
- 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. | ||
GlenDC marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
There is an open RFC issue and discussion about linear types in Rust: | ||
<https://github.com/rust-lang/rfcs/issues/814>. | ||
GlenDC marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
</details> |
84 changes: 84 additions & 0 deletions
84
src/idiomatic/leveraging-the-type-system/raii/drop_limitations.md
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,84 @@ | ||
# 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 | ||
GlenDC marked this conversation as resolved.
Show resolved
Hide resolved
|
||
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. | ||
GlenDC marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
- 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. | ||
GlenDC marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
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 | ||
GlenDC marked this conversation as resolved.
Show resolved
Hide resolved
|
||
dropped. And in fact as discussed in previous slide it might never even run at | ||
GlenDC marked this conversation as resolved.
Show resolved
Hide resolved
|
||
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. | ||
GlenDC marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
- 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 | ||
GlenDC marked this conversation as resolved.
Show resolved
Hide resolved
|
||
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> |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.