-
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
23
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 all commits
Commits
Show all changes
23 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 1d67c9b
Merge branch 'main' into raii
GlenDC 4c408b7
address more feedback
GlenDC 8f017de
Merge branch 'main' into raii
GlenDC 26c92f4
address more feedback
GlenDC 1b1a3ab
Merge branch 'main' into raii
GlenDC 67cefcc
run dprint fmt
GlenDC 3789e70
apply rem. feedback to raii slides + introduce drop_skipped slide
GlenDC 4a79ba4
add 2 extra slides
GlenDC cacc981
improve the raii chapter slides based on the new STYLE guidelines
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,92 @@ | ||
| --- | ||
| minutes: 30 | ||
| --- | ||
|
|
||
| # RAII: `Drop` trait | ||
|
|
||
| RAII (**R**esource **A**cquisition **I**s **I**nitialization) ties the lifetime | ||
| of a resource to the lifetime of a value. | ||
|
|
||
| [Rust uses RAII to manage memory](https://doc.rust-lang.org/rust-by-example/scope/raii.html), | ||
| and the `Drop` trait allows you to extend this to other resources, such as file | ||
| descriptors or locks. | ||
|
|
||
| ```rust,editable | ||
| pub struct File(std::os::fd::RawFd); | ||
|
|
||
| impl File { | ||
| pub fn open(path: &str) -> Result<Self, std::io::Error> { | ||
| // [...] | ||
| Ok(Self(0)) | ||
| } | ||
|
|
||
| pub fn read_to_end(&mut self) -> Result<Vec<u8>, std::io::Error> { | ||
| // [...] | ||
| Ok(b"example".to_vec()) | ||
| } | ||
|
|
||
| pub fn close(self) -> Result<(), std::io::Error> { | ||
| // [...] | ||
| Ok(()) | ||
| } | ||
| } | ||
|
|
||
| fn main() -> Result<(), std::io::Error> { | ||
| let mut file = File::open("example.txt")?; | ||
| println!("content: {:?}", file.read_to_end()?); | ||
| Ok(()) | ||
| } | ||
| ``` | ||
|
|
||
| <details> | ||
|
|
||
| - Easy to miss: `file.close()` is never called. Ask the class if they noticed. | ||
|
|
||
| - To release the file descriptor correctly, `file.close()` must be called after | ||
| the last use — and also in early-return paths in case of errors. | ||
|
|
||
| - Instead of relying on the user to call `close()`, we can implement the `Drop` | ||
| trait to release the resource automatically. This ties cleanup to the lifetime | ||
| of the `File` value. | ||
|
|
||
| ```rust,compile_fail | ||
| impl Drop for File { | ||
| fn drop(&mut self) { | ||
| println!("release file descriptor automatically"); | ||
| } | ||
| } | ||
| ``` | ||
|
|
||
| - Note that `Drop::drop` cannot return errors. Any fallible logic must be | ||
| handled internally or ignored. In the standard library, errors during FD | ||
| closure inside `Drop` are silently discarded. See the implementation: | ||
| <https://doc.rust-lang.org/src/std/os/fd/owned.rs.html#169-196> | ||
|
|
||
| - When is `Drop::drop` called? | ||
|
|
||
| Normally, when the `file` variable in `main` goes out of scope (either on | ||
| return or due to a panic), `drop()` is called automatically. | ||
|
|
||
| If the file is moved into another function, for example `read_all()`, the | ||
GlenDC marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| value is dropped when that function returns — not in `main`. | ||
|
|
||
| In contrast, C++ runs destructors in the original scope even for moved-from | ||
| values. | ||
|
|
||
| - Demo: insert `panic!("oops")` at the start of `read_to_end()` and run it. | ||
| `drop()` still runs during unwinding. | ||
|
|
||
| ### More to Explore | ||
|
|
||
| The `Drop` trait has another important limitation: it is not `async`. | ||
|
|
||
| This means you cannot `await` inside a destructor, which is often needed when | ||
| cleaning up asynchronous resources like sockets, database connections, or tasks | ||
| that must signal completion to another system. | ||
|
|
||
| - Learn more: | ||
| <https://rust-lang.github.io/async-fundamentals-initiative/roadmap/async_drop.html> | ||
| - There is an experimental `AsyncDrop` trait available on nightly: | ||
| <https://doc.rust-lang.org/nightly/std/future/trait.AsyncDrop.html> | ||
|
|
||
| </details> | ||
GlenDC marked this conversation as resolved.
Show resolved
Hide resolved
|
||
85 changes: 85 additions & 0 deletions
85
src/idiomatic/leveraging-the-type-system/raii/drop_bomb.md
GlenDC marked this conversation as resolved.
Show resolved
Hide resolved
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,85 @@ | ||
| # 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`. | ||
GlenDC marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| ```rust,editable | ||
| use std::io::{self, Write}; | ||
|
|
||
| struct Transaction { | ||
| active: bool, | ||
| } | ||
|
|
||
| impl Transaction { | ||
| fn start() -> Self { | ||
| Self { active: true } | ||
| } | ||
|
|
||
| fn commit(mut self) -> io::Result<()> { | ||
| writeln!(io::stdout(), "COMMIT")?; | ||
| self.active = false; | ||
| Ok(()) | ||
| } | ||
| } | ||
|
|
||
| impl Drop for Transaction { | ||
| fn drop(&mut self) { | ||
| if self.active { | ||
| panic!("Transaction dropped without commit!"); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| fn main() -> io::Result<()> { | ||
| let tx = Transaction::start(); | ||
| // Use `tx` to build the transaction, then commit it. | ||
| // Comment out the call to `commit` to see the panic. | ||
| tx.commit()?; | ||
| Ok(()) | ||
| } | ||
| ``` | ||
|
|
||
| <details> | ||
|
|
||
| - A drop bomb ensures that a value like `Transaction` cannot be silently dropped | ||
| in an unfinished state. The destructor panics if the transaction has not been | ||
| explicitly finalized (for example, with `commit()`). | ||
|
|
||
| - The finalizing operation (such as `commit()`) usually take `self` by value. | ||
| This ensures that once the transaction is finalized, the original object can | ||
| no longer be used. | ||
|
|
||
| - A common reason to use this pattern is when cleanup cannot be done in `Drop`, | ||
| either because it is fallible or asynchronous. | ||
|
|
||
| - 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 cleanup can safely happen in `Drop`, some APIs choose to panic only in | ||
| debug builds. Whether this is appropriate depends on the guarantees your API | ||
| must enforce. | ||
|
|
||
| - Panicking in Release builds is reasonable when silent misuse would cause major | ||
| correctness or security problems. | ||
|
|
||
| ## More to explore | ||
|
|
||
| Several related patterns help enforce correct teardown or prevent accidental | ||
| drops. | ||
|
|
||
| - The [`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. | ||
|
|
||
| </details> | ||
53 changes: 53 additions & 0 deletions
53
src/idiomatic/leveraging-the-type-system/raii/drop_bomb_forget.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,53 @@ | ||
| # Drop Bombs: using `std::mem::forget` | ||
|
|
||
| ```rust,editable | ||
| use std::io::{self, Write}; | ||
|
|
||
| struct Transaction; | ||
|
|
||
| impl Transaction { | ||
| fn start() -> Self { | ||
| Transaction | ||
| } | ||
|
|
||
| fn commit(self) -> io::Result<()> { | ||
| writeln!(io::stdout(), "COMMIT")?; | ||
|
|
||
| // Defuse the drop bomb by preventing Drop from ever running. | ||
| std::mem::forget(self); | ||
|
|
||
| Ok(()) | ||
| } | ||
| } | ||
|
|
||
| impl Drop for Transaction { | ||
| fn drop(&mut self) { | ||
| // This is the "drop bomb" | ||
| panic!("Transaction dropped without commit!"); | ||
| } | ||
| } | ||
|
|
||
| fn main() -> io::Result<()> { | ||
| let tx = Transaction::start(); | ||
| // Use `tx` to build the transaction, then commit it. | ||
| // Comment out the call to `commit` to see the panic. | ||
| tx.commit()?; | ||
| Ok(()) | ||
| } | ||
| ``` | ||
|
|
||
| <details> | ||
|
|
||
| In the previous slide we saw that calling | ||
| [`std::mem::forget`](https://doc.rust-lang.org/std/mem/fn.forget.html) prevents | ||
| `Drop::drop` from ever running. | ||
|
|
||
| Remember that `mem::forget` leaks the value. This is safe in Rust, but the | ||
| memory will not be reclaimed. | ||
|
|
||
| However, this avoids needing a runtime flag: when the transaction is | ||
| successfully committed, we can _defuse_ the drop bomb — meaning we prevent | ||
| `Drop` from running — by calling `std::mem::forget` on the value instead of | ||
| letting its destructor run. | ||
|
|
||
| </details> |
65 changes: 65 additions & 0 deletions
65
src/idiomatic/leveraging-the-type-system/raii/drop_guards.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,65 @@ | ||
| # Drop Guards | ||
|
|
||
| A **drop guard** in Rust is a temporary object that performs some kind of | ||
| cleanup when it goes out of scope. In the case of `Mutex`, the `lock` method | ||
| returns a `MutexGuard` that automatically unlocks the mutex on `drop`: | ||
|
|
||
| ```rust | ||
| struct Mutex { | ||
| is_locked: bool, | ||
| } | ||
|
|
||
| struct MutexGuard<'a> { | ||
| mutex: &'a mut Mutex, | ||
| } | ||
|
|
||
| impl Mutex { | ||
| fn new() -> Self { | ||
| Self { is_locked: false } | ||
| } | ||
|
|
||
| fn lock(&mut self) -> MutexGuard<'_> { | ||
| self.is_locked = true; | ||
| MutexGuard { mutex: self } | ||
| } | ||
| } | ||
|
|
||
| impl Drop for MutexGuard<'_> { | ||
| fn drop(&mut self) { | ||
| self.mutex.is_locked = false; | ||
| } | ||
| } | ||
| ``` | ||
|
|
||
| <details> | ||
|
|
||
| - The example above shows a simplified `Mutex` and its associated guard. | ||
|
|
||
| - Even though it is not a production-ready implementation, it illustrates the | ||
| core idea: | ||
|
|
||
| - the guard represents exclusive access, | ||
| - and its `Drop` implementation releases the lock when it goes out of scope. | ||
|
|
||
| ## More to Explore | ||
|
|
||
| This example shows a C++ style mutex that does not contain the data it protects. | ||
| While this is non idiomatic in Rust, the goal here is only to illustrate the | ||
| core idea of a drop guard, not to demonstrate a proper Rust mutex design. | ||
|
|
||
| For brevity, several features are omitted: | ||
|
|
||
| - A real `Mutex<T>` stores the protected value inside the mutex.\ | ||
| This toy example omits the value entirely to focus only on the drop guard | ||
| mechanism. | ||
| - Ergonomic access via `Deref` and `DerefMut` on `MutexGuard` (letting the guard | ||
| behave like a `&T` or `&mut T`). | ||
| - A fully blocking `.lock()` method and a non blocking `try_lock` variant. | ||
|
|
||
| You can explore the | ||
| [`Mutex` implementation in Rust’s std library](https://doc.rust-lang.org/std/sync/struct.Mutex.html) | ||
| as an example of a production-ready mutex. The | ||
| [`Mutex` from the `parking_lot` crate](https://docs.rs/parking_lot/latest/parking_lot/type.Mutex.html) | ||
| is another worthwhile reference. | ||
|
|
||
| </details> |
84 changes: 84 additions & 0 deletions
84
src/idiomatic/leveraging-the-type-system/raii/drop_option.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 @@ | ||
| # Drop: Option | ||
|
|
||
| ```rust,editable | ||
| struct File(Option<Handle>); | ||
|
|
||
| impl File { | ||
| fn open(path: &'static str) -> std::io::Result<Self> { | ||
| Ok(Self(Some(Handle { path }))) | ||
| } | ||
|
|
||
| fn write(&mut self, data: &str) -> std::io::Result<()> { | ||
| match &mut self.0 { | ||
| Some(handle) => println!("write '{data}' to file '{}'", handle.path), | ||
| None => unreachable!(), | ||
| } | ||
| Ok(()) | ||
| } | ||
|
|
||
| fn close(mut self) -> std::io::Result<&'static str> { | ||
| Ok(self.0.take().unwrap().path) | ||
| } | ||
| } | ||
|
|
||
| impl Drop for File { | ||
| fn drop(&mut self) { | ||
| if let Some(handle) = self.0.take() { | ||
| println!("automatically close handle for file: {}", handle.path); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| struct Handle { | ||
| path: &'static str, | ||
| } | ||
| impl Drop for Handle { | ||
| fn drop(&mut self) { | ||
| println!("close handle for file: {}", self.path) | ||
| } | ||
| } | ||
|
|
||
| fn main() -> std::io::Result<()> { | ||
| let mut file = File::open("foo.txt")?; | ||
| file.write("hello")?; | ||
| println!("manually closed file: {}", file.close()?); | ||
| Ok(()) | ||
| } | ||
| ``` | ||
|
|
||
| <details> | ||
|
|
||
| - In this example we want to let the user call `close()` manually so that errors | ||
| from closing the file can be reported explicitly. | ||
|
|
||
| - At the same time we still want RAII semantics: if the user forgets to call | ||
| `close()`, the handle must be cleaned up automatically in `Drop`. | ||
|
|
||
| - Wrapping the handle in an `Option` gives us both behaviors. `close()` extracts | ||
| the handle with `take()`, and `Drop` only runs cleanup if a handle is still | ||
| present. | ||
|
|
||
| Demo: remove the `.close()` call and run the code — `Drop` now prints the | ||
| automatic cleanup. | ||
|
|
||
| - The main downside is ergonomics. `Option` forces us to handle both the `Some` | ||
| and `None` case even in places where, logically, `None` cannot occur. Rust’s | ||
| type system cannot express that relationship between `File` and its `Handle`, | ||
| so we handle both cases manually. | ||
|
|
||
| ## More to explore | ||
|
|
||
| Instead of `Option` we could use | ||
| [`ManuallyDrop`](https://doc.rust-lang.org/std/mem/struct.ManuallyDrop.html), | ||
| which suppresses automatic destruction by preventing Rust from calling `Drop` | ||
| for the value; you must handle teardown yourself. | ||
|
|
||
| The [_scopeguard_ example](./scope_guard.md) on the previous slide shows how | ||
| `ManuallyDrop` can replace `Option` to avoid handling `None` in places where the | ||
| value should always exist. | ||
|
|
||
| In such designs we typically track the drop state with a separate flag next to | ||
| the `ManuallyDrop<Handle>`, which lets us track whether the handle has already | ||
| been manually consumed. | ||
|
|
||
| </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.