1
0
mirror of https://github.com/google/comprehensive-rust.git synced 2025-12-17 05:19:22 +02:00

RAII chapter for idiomatic rust (#2820)

This PR adds the RAII chapter for the idiomatic Rust deep dive.
This commit is contained in:
Glen De Cauwsemaecker
2025-12-05 15:12:25 +01:00
committed by GitHub
parent e42c8b36d4
commit 05b67d12a5
11 changed files with 704 additions and 0 deletions

View File

@@ -441,6 +441,15 @@
- [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 Skipped](idiomatic/leveraging-the-type-system/raii/drop_skipped.md)
- [Mutex](idiomatic/leveraging-the-type-system/raii/mutex.md)
- [Drop Guards](idiomatic/leveraging-the-type-system/raii/drop_guards.md)
- [Drop Bomb](idiomatic/leveraging-the-type-system/raii/drop_bomb.md)
- [Drop Bomb Forget](idiomatic/leveraging-the-type-system/raii/drop_bomb_forget.md)
- [forget and drop functions](idiomatic/leveraging-the-type-system/raii/forget_and_drop.md)
- [Scope Guard](idiomatic/leveraging-the-type-system/raii/scope_guard.md)
- [Drop Option](idiomatic/leveraging-the-type-system/raii/drop_option.md)
- [Extension Traits](idiomatic/leveraging-the-type-system/extension-traits.md)
- [Extending Foreign Types](idiomatic/leveraging-the-type-system/extension-traits/extending-foreign-types.md)
- [Method Resolution Conflicts](idiomatic/leveraging-the-type-system/extension-traits/method-resolution-conflicts.md)

View File

@@ -0,0 +1,94 @@
---
minutes: 15
---
# 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) {
// libc::close(...);
println!("file descriptor was closed");
}
}
```
- Note that `Drop::drop()` cannot return a `Result`. Any failures 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 (as is this case with
`File::close()`), the 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>

View File

@@ -0,0 +1,92 @@
---
minutes: 15
---
# 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
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>
- In some systems, a value must be finalized by a specific API before it is
dropped.
For example, a `Transaction` might need to be committed or rolled back.
- 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 takes `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.
- Question: Why do we need an `active` flag inside `Transaction`? Why can't
`drop()` panic unconditionally?
Expected answer: `commit()` takes `self` by value and runs `drop()`, which
would panic.
## 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.
</details>

View File

@@ -0,0 +1,61 @@
---
minutes: 10
---
# 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>
This example removes the flag from the previous slide and makes the drop method
panic unconditionally. To avoid that panic on a successful commit, the commit
method now takes ownership of the transaction and calls
[`std::mem::forget`](https://doc.rust-lang.org/std/mem/fn.forget.html), which
prevents the `Drop::drop()` method from running.
If the forgotten value owned heap allocated memory that would normally be freed
in its `drop()` implementation, one consequence is a memory leak. That is not
the case for the `Transaction` in the example above, since it does not own any
heap memory.
We can avoid needing a runtime flag by using `mem::forget()` in a tactical way.
When the transaction commits successfully, we can defuse the drop bomb by
calling `std::mem::forget` on the value, which prevents its `Drop`
implementation from running.
</details>

View File

@@ -0,0 +1,69 @@
---
minutes: 10
---
# 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>

View File

@@ -0,0 +1,88 @@
---
minutes: 10
---
# 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 closing handle for file: {}", handle.path);
}
}
}
struct Handle {
path: &'static str,
}
impl Drop for Handle {
fn drop(&mut self) {
println!("closed 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>

View File

@@ -0,0 +1,122 @@
---
minutes: 15
---
# Drop can be skipped
There are cases where destructors will not run.
```rust,editable
#[derive(Debug)]
struct OwnedFd(i32);
impl Drop for OwnedFd {
fn drop(&mut self) {
println!("OwnedFd::drop() called with raw fd: {:?}", self.0);
}
}
impl Drop for TmpFile {
fn drop(&mut self) {
println!("TmpFile::drop() called with owned fd: {:?}", self.0);
// libc::unlink("/tmp/file")
// panic!("TmpFile::drop() panics");
}
}
#[derive(Debug)]
struct TmpFile(OwnedFd);
impl TmpFile {
fn open() -> Self {
Self(OwnedFd(2))
}
fn close(&self) {
panic!("TmpFile::close(): not implemented yet");
}
}
fn main() {
let owned_fd = OwnedFd(1);
let file = TmpFile::open();
std::process::exit(0);
// std::mem::forget(file);
// file.close();
let _ = owned_fd;
}
```
<details>
- Drop is not guaranteed to always run. There is a number of cases when drop is
skipped: the program can crash or exit, the value with the drop implementation
can be leaked etc.
- In the version that calls
[`std::process::exit`](https://doc.rust-lang.org/std/process/fn.exit.html),
`TmpFile::drop()` is never run because `exit()` terminates the process
immediately without any opportunity for a `drop()` method to be called.
- You can prevent accidental use of `exit` by denying the
[`clippy::exit`](https://rust-lang.github.io/rust-clippy/stable/index.html#exit)
lint.
- If you remove the `std::process::exit(0)` line, each `drop()` method in this
simple case will run in turn.
- Try uncommenting the
[`std::mem::forget`](https://doc.rust-lang.org/std/mem/fn.forget.html) call.
What do you think will happen?
`mem::forget()` takes ownership and "forgets" about the value `file` without
running its **destructor** `Drop::drop()`. The destructor of `owned_fd` is
still run.
- Remove the `mem::forget()` call, then uncomment the `file.close()` call below
it. What do you expect now?
With the default `panic = "unwind"` setting, the stack still unwinds and
destructors run, even when the panic starts in `main`.
- With
[`panic = "abort"`](https://doc.rust-lang.org/cargo/reference/profiles.html#panic)
no destructors are run.
- As a last step, uncomment the `panic!` inside `TmpFile::drop()` and run it.
Ask the class: which destructors run before the abort?
After a double panic, Rust no longer guarantees that remaining destructors
will run:
- Some cleanup that was already in progress may still complete (for example,
field destructors of the value currently being dropped),
- but anything scheduled later in the unwind path might be skipped entirely.
- This is why we say you cannot rely solely on `drop()` for critical external
cleanup, nor assume that a double panic aborts without running any further
destructors.
- Some languages forbid or restrict exceptions in destructors. Rust allows
panicking in `Drop::drop`, but it is almost never a good idea, since it can
disrupt unwinding and lead to unpredictable cleanup. It is best avoided unless
there is a very specific need, such as in the case of a **drop bomb**.
- Drop is suitable for cleaning up resources within the scope of a process, but
it is not the right tool for providing hard guarantees that something happens
outside of the process (e.g., on local disk, or in another service in a
distributed system).
- For example, deleting a temporary file in `drop()` is fine in a toy example,
but in a real program you would still need an external cleanup mechanism such
as a temp file reaper.
- In contrast, we can rely on `drop()` to unlock a mutex, since it is a
process-local resource. If `drop()` is skipped and the mutex is left locked,
it has no lasting effects outside the process.
</details>

View File

@@ -0,0 +1,41 @@
---
minutes: 10
---
# forget and drop functions
Below are the signatures for the
[`drop()`](https://doc.rust-lang.org/std/mem/fn.drop.html) and
[`forget()`](https://doc.rust-lang.org/std/mem/fn.forget.html) functions:
```rust
// std::mem::forget
fn forget<T>(t: T) {
let _ = std::mem::ManuallyDrop::new(t);
}
// std::mem::drop
fn drop<T>(_x: T) {}
```
<details>
- Both `mem::forget()` and `mem::drop()` take ownership of the value `t`.
- Despite having the same function signature, they have opposite effects:
- `forget()` uses
[`ManuallyDrop`](https://doc.rust-lang.org/std/mem/struct.ManuallyDrop.html)
to prevent the destructor `Drop::drop()` from being invoked.
This is useful for scenarios such as implementing a drop bomb or otherwise
opting out of destructor behavior.
Be careful though, since any resources the value exclusively owns such as
heap allocated memory or file handles will remain in an unreachable state.
- `drop()` is a convenience function for disposing of a value. Because `t` is
moved into the function, it is automatically dropped which triggers its
`Drop::drop()` implementation before the parent function returns.
</details>

View File

@@ -0,0 +1,43 @@
---
minutes: 10
---
# Mutex and MutexGuard
In earlier examples, RAII was used to manage concrete resources like file
descriptors. With a `Mutex`, the "resource" is mutable access to a value. You
access the value by calling `lock`, which then returns a `MutexGuard` which will
unlock the `Mutex` automatically when dropped.
```rust
use std::sync::Mutex;
fn main() {
let m = Mutex::new(vec![1, 2, 3]);
let mut guard = m.lock().unwrap();
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 logical: temporary exclusive access to the data inside.
- This right is represented by a `MutexGuard`. Only one guard for this mutex can
exist at a time. While it lives, it provides `&mut T` access.
- Although `lock()` takes `&self`, it returns a `MutexGuard` with mutable
access. This works through _interior mutability_, where a type manages its own
borrowing rules internally to allow mutation through `&self`.
- `MutexGuard` implements `Deref` and `DerefMut`, making access ergonomic. You
lock the mutex and use the guard like a `&mut T`.
- The mutex is released by `MutexGuard::drop()`. You never call an explicit
unlock function.
</details>

View File

@@ -0,0 +1,78 @@
---
minutes: 15
---
# Scope Guards
A scope guard uses the `Drop` trait to run cleanup code automatically when a
scope exits, even during unwinding.
```rust,editable,compile_fail
use scopeguard::{ScopeGuard, guard};
use std::fs::{self, File};
use std::io::Write;
fn download_successful() -> bool {
// [...]
true
}
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 '{path}' complete!");
}
// Otherwise, the guard runs and deletes the file
}
```
<details>
- This example models a download workflow. We create a temporary file first,
then use a scope guard to ensure that the file is deleted if the download
fails.
- The `scopeguard` crate allows you to conveniently define a single-use
`Drop`-based cleanup without defining a custom type with a custom `Drop`
implementation.
- The guard is created 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()` creates a `ScopeGuard` instance. It a user-defined value (in
this case, `path`) and the cleanup closure that later receives this value.
- The guard's closure runs on scope exit unless it is _defused_ with
`ScopeGuard::into_inner` (removing the value so the guard does nothing on
drop). In the success path, we call `into_inner` so the guard will not delete
the file.
- A scope guard is similar to the `defer` feature in Go.
- This pattern is ideal for "cleanup on failure" scenarios, where a cleanup
should run by default unless a success path is explicitly taken.
- This pattern is also useful when you don't control the cleanup strategy of the
resource object. In this example, `File::drop()` closes the file but does not
delete it, and we can't change the standard library to delete the file instead
(nor should we, it is not a good idea anyway).
- 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.
</details>

View File

@@ -32,6 +32,13 @@ decisions within the context and constraints of your own projects.
The course will cover the topics listed below. Each topic may be covered in one
or more slides, depending on its complexity and relevance.
## Target Audience
Engineers with at least 2-3 years of coding experience in C, C++11 or newer,
Java 7 or newer, Python 2 or 3, Go or any other similar imperative programming
language. We have no expectation of experience with more modern or feature-rich
languages like Swift, Kotlin, C#, or TypeScript.
### Foundations of API design
- Golden rule: prioritize clarity and readability at the callsite. People will