From 39ae8f65cec6ff7050153c4e23ee7bf33fb80d3c Mon Sep 17 00:00:00 2001 From: Glen De Cauwsemaecker Date: Tue, 15 Jul 2025 20:39:51 +0200 Subject: [PATCH] initial version of the raii chapters for idiomatic rust --- src/SUMMARY.md | 5 +- .../leveraging-the-type-system/raii.md | 116 ++++++++++++++++ .../raii/drop_bomb.md | 89 ++++++++++++ .../raii/drop_limitations.md | 85 ++++++++++++ .../raii/scope_guards.md | 130 ++++++++++++++++++ 5 files changed, 424 insertions(+), 1 deletion(-) create mode 100644 src/idiomatic/leveraging-the-type-system/raii.md create mode 100644 src/idiomatic/leveraging-the-type-system/raii/drop_bomb.md create mode 100644 src/idiomatic/leveraging-the-type-system/raii/drop_limitations.md create mode 100644 src/idiomatic/leveraging-the-type-system/raii/scope_guards.md diff --git a/src/SUMMARY.md b/src/SUMMARY.md index 1950476a..021c75f3 100644 --- a/src/SUMMARY.md +++ b/src/SUMMARY.md @@ -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 diff --git a/src/idiomatic/leveraging-the-type-system/raii.md b/src/idiomatic/leveraging-the-type-system/raii.md new file mode 100644 index 00000000..f67ce441 --- /dev/null +++ b/src/idiomatic/leveraging-the-type-system/raii.md @@ -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 +} +``` + +
+ +- 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`. + + +
diff --git a/src/idiomatic/leveraging-the-type-system/raii/drop_bomb.md b/src/idiomatic/leveraging-the-type-system/raii/drop_bomb.md new file mode 100644 index 00000000..7be2d154 --- /dev/null +++ b/src/idiomatic/leveraging-the-type-system/raii/drop_bomb.md @@ -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!"); + } + } +} +``` + +
+ +- 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` 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: +. + +
diff --git a/src/idiomatic/leveraging-the-type-system/raii/drop_limitations.md b/src/idiomatic/leveraging-the-type-system/raii/drop_limitations.md new file mode 100644 index 00000000..4b6d09c5 --- /dev/null +++ b/src/idiomatic/leveraging-the-type-system/raii/drop_limitations.md @@ -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 +``` + +
+ +- 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. + +
diff --git a/src/idiomatic/leveraging-the-type-system/raii/scope_guards.md b/src/idiomatic/leveraging-the-type-system/raii/scope_guards.md new file mode 100644 index 00000000..19f72ad6 --- /dev/null +++ b/src/idiomatic/leveraging-the-type-system/raii/scope_guards.md @@ -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 + } +} +``` + +
+ +- 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 { + value: Option, + drop_fn: Option, + } + + impl ScopeGuard { + 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 Drop for ScopeGuard { + 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 std::ops::Deref for ScopeGuard { + type Target = T; + + fn deref(&self) -> &T { + // Provide shared access to the underlying value + self.value.as_ref().unwrap() + } + } + + impl std::ops::DerefMut for ScopeGuard { + 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. + +