From 39ae8f65cec6ff7050153c4e23ee7bf33fb80d3c Mon Sep 17 00:00:00 2001 From: Glen De Cauwsemaecker Date: Tue, 15 Jul 2025 20:39:51 +0200 Subject: [PATCH 1/7] 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 1950476a423a..021c75f380d2 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 000000000000..f67ce441893d --- /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 000000000000..7be2d154fdcd --- /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 000000000000..4b6d09c56cf4 --- /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 000000000000..19f72ad65c1e --- /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. + +
From f4269b7f758623c0fe74904fe1c76fbde3169234 Mon Sep 17 00:00:00 2001 From: Glen De Cauwsemaecker Date: Tue, 15 Jul 2025 20:54:50 +0200 Subject: [PATCH 2/7] apply dprint fmt --- src/SUMMARY.md | 1 + .../leveraging-the-type-system/raii.md | 92 +++++++++---------- .../raii/drop_bomb.md | 52 ++++++----- .../raii/drop_limitations.md | 77 ++++++++-------- .../raii/scope_guards.md | 70 +++++++------- 5 files changed, 149 insertions(+), 143 deletions(-) diff --git a/src/SUMMARY.md b/src/SUMMARY.md index 021c75f380d2..75e521af0fa7 100644 --- a/src/SUMMARY.md +++ b/src/SUMMARY.md @@ -441,6 +441,7 @@ - [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 index f67ce441893d..61e7c36fed05 100644 --- a/src/idiomatic/leveraging-the-type-system/raii.md +++ b/src/idiomatic/leveraging-the-type-system/raii.md @@ -4,11 +4,11 @@ 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. +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 applies RAII automatically for memory management. The `Drop` trait lets you +extend this pattern to anything else. ```rust use std::sync::Mutex; @@ -26,28 +26,31 @@ fn main() {
- 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. + [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). + 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. +- 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 (`{}`). + - 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: + - 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"); @@ -55,62 +58,59 @@ fn main() { // 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. + - 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) +- 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. + - 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. + - 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`. + - 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. + 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) + 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 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. +- 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`. +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 index 7be2d154fdcd..b6ee94cb5439 100644 --- a/src/idiomatic/leveraging-the-type-system/raii/drop_bomb.md +++ b/src/idiomatic/leveraging-the-type-system/raii/drop_bomb.md @@ -1,7 +1,7 @@ # Drop Bombs: Enforcing API Correctness -Use `Drop` to enforce invariants and detect incorrect API usage. -A "drop bomb" panics if not defused. +Use `Drop` to enforce invariants and detect incorrect API usage. A "drop bomb" +panics if not defused. ```rust struct Transaction { @@ -35,28 +35,28 @@ impl Drop for Transaction {
-- 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. +- 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. +- 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. +- 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. +- 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: @@ -68,20 +68,22 @@ impl Drop for Transaction { 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. +- 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. +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. +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 index 4b6d09c56cf4..ef0213567df8 100644 --- a/src/idiomatic/leveraging-the-type-system/raii/drop_limitations.md +++ b/src/idiomatic/leveraging-the-type-system/raii/drop_limitations.md @@ -1,8 +1,7 @@ # 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. +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; @@ -19,17 +18,17 @@ fn write_log() -> io::Result<()> {
-- In the earlier example, our `File` resource owns a file handle - provided by the operating system. +- 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. + > 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 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): @@ -41,45 +40,45 @@ fn write_log() -> io::Result<()> { ``` 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. + 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. +- 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. + [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. +- 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. + 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. + 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 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. +- 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. + 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. + 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 index 19f72ad65c1e..823df51ebac7 100644 --- a/src/idiomatic/leveraging-the-type-system/raii/scope_guards.md +++ b/src/idiomatic/leveraging-the-type-system/raii/scope_guards.md @@ -1,13 +1,18 @@ # Scope Guards -A scope guard makes use of the `Drop` trait -to run a given closure when it goes out of scope. +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 } +use scopeguard::{ScopeGuard, guard}; +use std::{ + fs::{self, File}, + io::Write, +}; + +fn conditional_success() -> bool { + true +} fn main() { let path = "temp.txt"; @@ -35,12 +40,12 @@ fn main() {
- 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. + [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 + - 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 @@ -51,8 +56,8 @@ fn main() { } ``` -- If desired, the "scope guard" pattern can be implemented manually, - starting as follows: +- If desired, the "scope guard" pattern can be implemented manually, starting as + follows: ```rust struct ScopeGuard { @@ -98,33 +103,32 @@ fn main() { } ``` - - 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) + - 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. + - 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. + 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. + 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. + [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.
From bb88a790274428336f6146aecf6126e638be5d8e Mon Sep 17 00:00:00 2001 From: Glen De Cauwsemaecker Date: Tue, 15 Jul 2025 20:57:53 +0200 Subject: [PATCH 3/7] fix build (en) step: scopeguard crate import --- src/idiomatic/leveraging-the-type-system/raii/scope_guards.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/idiomatic/leveraging-the-type-system/raii/scope_guards.md b/src/idiomatic/leveraging-the-type-system/raii/scope_guards.md index 823df51ebac7..93e2c2c47489 100644 --- a/src/idiomatic/leveraging-the-type-system/raii/scope_guards.md +++ b/src/idiomatic/leveraging-the-type-system/raii/scope_guards.md @@ -3,7 +3,7 @@ A scope guard makes use of the `Drop` trait to run a given closure when it goes out of scope. -```rust +```rust,editable,compile_fail use scopeguard::{ScopeGuard, guard}; use std::{ fs::{self, File}, @@ -48,7 +48,7 @@ fn main() { be simplified using [scopeguard's `defer!` macro](https://docs.rs/scopeguard/latest/scopeguard/#defer): - ```rust + ```rust,editable,compile_fail let path = "temp.txt"; scopeguard::defer! { From 5a4838ed646a8bcf2ae4f9f5b9ebf55582a06642 Mon Sep 17 00:00:00 2001 From: Glen De Cauwsemaecker Date: Sat, 26 Jul 2025 15:11:06 +0200 Subject: [PATCH 4/7] integrate feedback of first reviews (RAII) - directly where possible - otherwise as inline feedback as notes to take into account for next draft --- .../leveraging-the-type-system/raii.md | 74 ++++++++++-- .../raii/drop_bomb.md | 113 ++++++++++++++---- .../raii/drop_limitations.md | 43 ++++++- .../raii/scope_guards.md | 66 +++++++++- src/idiomatic/welcome.md | 8 ++ 5 files changed, 266 insertions(+), 38 deletions(-) diff --git a/src/idiomatic/leveraging-the-type-system/raii.md b/src/idiomatic/leveraging-the-type-system/raii.md index 61e7c36fed05..f8a20293e8e0 100644 --- a/src/idiomatic/leveraging-the-type-system/raii.md +++ b/src/idiomatic/leveraging-the-type-system/raii.md @@ -7,17 +7,17 @@ minutes: 30 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 +Rust uses RAII for managing heap memory. 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 mutex = Mutex::new(vec![1, 2, 3]); { - let mut data = mux.lock().unwrap(); + let mut data = mutex.lock().unwrap(); data.push(4); // lock held here } // lock automatically released here } @@ -33,10 +33,13 @@ fn main() { [`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). + [`Drop`](https://doc.rust-lang.org/std/ops/trait.Drop.html) + + TODO: consider devoting 1-2 slides to demonstrate the relevant snippets of + Mutex and MutexGuard API - You may recall from - [the Memory Management chapter](../../memory-management/drop.md) that the + [the Memory Management segment](../../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. @@ -77,11 +80,15 @@ fn main() { - It also holds for unexpected scenarios where a `panic` is triggered, if: - - The stack unwinds on panic (which is the default), allowing for graceful + - The stack is unwound on panic (which is the default), allowing for graceful cleanup of resources. + (TODO: we might want to refactor this to make clear + this also happens in normal function returns) + 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). + [abort on panic](https://github.com/rust-lang/rust/blob/master/library/panic_abort/src/lib.rs), + in which case no destructors will run. - No panic occurs within any of the `drop` methods invoked before reaching the `drop` call of the object `a`. @@ -92,6 +99,16 @@ fn main() { words, the stack is not unwound in this case, and the `drop` method will not be called. + TODO: apply feedback: + + ``` + I think this whole point can be pulled out into its own slide. + Talking about when Drop runs and when it doesn't is worth covering + directly. I think you'd also want to talk about forget on that slide, + and maybe briefly note that leaking destructors is not unsafe + (unless you plan to cover them elsewhere). + ``` + - `Drop` is a great fit for use cases like `Mutex`. When the guard goes out of scope, @@ -102,15 +119,48 @@ fn main() { `lock/unlock` pattern, Rust ensures the lock _cannot_ be forgotten, thanks to the compiler. + TODO: revisit references to C++ and Java, be careful in wording. + E.g. C++ and Java their mutexes are also RAII based + ([std::lock_guard](https://en.cppreference.com/w/cpp/thread/lock_guard.html), [absl::MutexLock](https://github.com/abseil/abseil-cpp/blob/master/absl/synchronization/mutex.h#L583), `synchronized(obj) {}` in Java). + + TODO: incorporate @gribozavr's feedback here: + + ``` + It can't be forgotten, but the MutexGuard can be forgot()'en intentionally, or leaked - like any other value. + + It is a good tie-in to discuss use cases for drop: it is good for cleaning up things within the scope of a process, but not the right tool for guaranteeing that something happens outside of the process (e.g., on local disk, or in another service in a distributed system). + + For example, it is a bad idea to rely exclusively on drop to clean up temporary files: if the program terminates in a way that skips running drop, temporary files will persist, and eventually the computer will run out of space. This can happen if the program crashes or leaks the value whose drop is responsible for deleting the file. In addition to a drop implementation within the program, one also needs a classic unix-style temp file reaper that runs as a separate process. + ``` + - 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 +TODO: apply feedback from @gribozavr when refactoring the RAII content: -To learn more about building synchronization primitives, consider reading -[_Rust Atomics and Locks_ by Mara Bos](https://marabos.nl/atomics/). +``` +First, a custom File type that wraps a file descriptor. A file descriptor is a classic OS-level resource. We could show how to implement a simple read-only file type a with a minimal API: open() and read() to read a single byte. Then show how to implement Drop. Discuss when the drop() function runs, and how it isn't run when values are moved (contrast with C++ where the destructor always runs at the end of the scope, even for moved-from values). Show the forget() function, discuss its signature and what it means. + +In other words, use this simple File type as an opportunity to do a 5-minute refresher on drop and move semantics. I see you're already doing it with instructor notes like "for a composite type such as a struct, all its fields will be dropped" and by mentioning the std::mem::drop() function. Let's lean more into it and make sure that during this discussion we have an example of a drop implementation on the screen. + +Then we move on to Mutex. There we would focus on explaining the idea that for a mutex the "resource" is more abstract. In case of a mutex, the resource is exclusive access to the wrapped value. Thus, we need a second type - a MutexGuard - to represent that. + +The mutex example is perfect to facilitate the drop x panic discussion. Maybe draft an extra slide that shows what happens by default with a naive drop implementation (the drop simply runs, no special code is needed for that), and then discuss why panics poison the mutex in Rust (there is a good chance that the code was mutating the shared data, so its invariants might be broken). +``` + +Also apply feedback from @djimitche: -The book demonstrates, among other topics, how `Drop` and RAII work together in -constructs like `Mutex`. +``` + + +It's probably only necessary to include one "callback" to Fundamentals -- +the important point is to that this slide is a quick review of previous +content, and if students need a deeper refresher they can find that +content in the Fundamentals course. + +That said, these speaker notes are pretty long! Is it possible to trim +this down to just call out the bits necessary for the RAII patterns +introduced here, leaving the rest to the students' memory of Fundamentals? +```
diff --git a/src/idiomatic/leveraging-the-type-system/raii/drop_bomb.md b/src/idiomatic/leveraging-the-type-system/raii/drop_bomb.md index b6ee94cb5439..8c6d17a69b3d 100644 --- a/src/idiomatic/leveraging-the-type-system/raii/drop_bomb.md +++ b/src/idiomatic/leveraging-the-type-system/raii/drop_bomb.md @@ -3,6 +3,19 @@ Use `Drop` to enforce invariants and detect incorrect API usage. A "drop bomb" panics if not defused. +--- +TODO: apply feedback + +I think this slide should also mention that drop bombs are useful in cases +where the finalizing operation (e.g. commit and rollback in the slide's +example) needs to return some kind of output, which means it can't be +handled normally in Drop (realistically, commit and rollback would want to +return Results to indicate if they succeeded or not). This is one of the +limitations of Drop mentioned on the previous slide, so it'd be worth +noting that this pattern is a common way to work around that limitation. + +--- + ```rust struct Transaction { active: bool, @@ -27,7 +40,7 @@ impl Transaction { impl Drop for Transaction { fn drop(&mut self) { if self.active { - panic!("Transaction dropped without commit or roll back!"); + panic!("Transaction dropped without commit or rollback!"); } } } @@ -43,15 +56,69 @@ impl Drop for Transaction { 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 + often necessary to ensure that manually allocated memory from the foreign 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. + TODO: this is a bit far-fetched, better examples suggested by reviewers: + + ``` + Let's imagine what an implementation of an SSH server could look like. An SshServer object + that manages all connections, and an SshConnection object that represents a single connected user. + The connection needs to be unregistered from the server object before it can be dropped - + or else we would be leaking resources like socket descriptors. + + Say, for efficiency the connection does not have a pointer back to the server - + so the connection can't deregister itself within its drop. + Therefore, SshConnection::drop should panic to catch coding bugs. + Instead, we would provide an API like SshServer::deregister(&mut self, conn: SshConnection) + that consumes the connection, deregisters and correctly destroys it. + + -- @randomPoison agrees, this can be dropped: + + I don't think we need to mention FFI at all here, I think the decision + of whether or not to use a drop bomb has more to do with whether or not + there's a reasonable way to cleanup in Drop or if the finalizing + operation needs to return anything (see also my comment on the file). + That situation may come up in FFI, but it's not the FFI specifically + that makes a drop bomb necessary. + ``` + + more feedback: + + ``` + It's worth noting that a very common reason you can't drop the thing is that + the deallocation method is async and there's no async drop. + Database transactions are an excellent example of this, in fact -- + it's typically not possible to just rollback a transaction in drop(). + ``` + +- APIs that are expected to panic like this should document + the cases when a panic will occur under a `Panics` section. + + (^ TODO: this was reworded to be more minimal. Shorter the speaker + notes the better, to make it easier to skim through as instructor) + Original: + > 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. + + TODO: apply feedback: + + ``` + Either edit the example directly (for it to appear with the comment from the beginning), + or add a suggested comment here for the instructor to type or paste. + ``` + + TODO: Also more feedback: + + ``` + Why should this only be used in internal APIs? For example, a library + providing a transactional API (like in the example on this slide) would + want to use this pattern in its public API to help users catch cases + where they forget to finish the transaction. + ``` - 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 @@ -64,9 +131,19 @@ impl Drop for Transaction { This allows you to move out the resource in a controlled way, preventing accidental double cleanup or use-after-drop errors. + TODO: apply feedback + + ``` + I think we should provide an example of this. + I find that it's a common pattern when doing complex logic in Drop. + This might even be worth pulling out into its own slide. + ``` + - [`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. + This requires unsafe code to use, + though, so it's recommended to only use this if strictly necessary. - 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 @@ -74,18 +151,14 @@ impl Drop for Transaction { 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. + TODO: apply feedback: -There is an open RFC issue and discussion about linear types in Rust: -. + ``` + I don't love the wording "it is advisable to restrict the use of drop + bombs to Debug mode". I think the decision of whether to panic in + release builds is heavily dependent on the specifics of the API in + question, and panicking in release builds is an entirely valid decision + imo. + ``` diff --git a/src/idiomatic/leveraging-the-type-system/raii/drop_limitations.md b/src/idiomatic/leveraging-the-type-system/raii/drop_limitations.md index ef0213567df8..8747ff2557ea 100644 --- a/src/idiomatic/leveraging-the-type-system/raii/drop_limitations.md +++ b/src/idiomatic/leveraging-the-type-system/raii/drop_limitations.md @@ -19,7 +19,7 @@ fn write_log() -> io::Result<()> {
- In the earlier example, our `File` resource owns a file handle provided by the - operating system. + operating system. (TODO: be careful in wording: earlier is ambiguous here. Better use "above".) [As stated in the documentation](https://doc.rust-lang.org/std/fs/struct.File.html): @@ -44,9 +44,25 @@ fn write_log() -> io::Result<()> { automatically when a value is popped off the stack during unwinding, leaving no opportunity for error handling. + TODO: apply feedback: + + ``` + This last sentence suggests that there was no other design choice because of unwinding. + That's not true: in C++, for example, one can throw an exception from a destructor while uwinding + because of another exception. Throwing from a destructor is messy and error-prone + (and pretty much every style guide tells you not to do it), + however that is an existence proof that Rust's design choice here was not entirely forced. + It is a good pragmatic choice for sure, but not the only one possible. + + I'd suggest to rewrite this sentence in a way that talks about infallibility + of drop as a pragmatic design choice to keep the complexity of error handling under control + (not as the only possible choice). + ``` + - 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. + (TODO: be careful in wording and context. E.g. here it is about external resources) While panicking in `drop` can serve certain purposes (see [the next chapter on "drop bombs"](./drop_bomb.md)), it should be used @@ -57,6 +73,10 @@ fn write_log() -> io::Result<()> { dropped. And in fact as discussed in previous slide it might never even run at all, leaving the external resource in an undefined state. + (TODO: non-deterministic is incorrect here, fix wording and description) + + (TODO: be careful with wording 'you cannot control'. As you can control, by impl drop) + 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. @@ -66,11 +86,32 @@ fn write_log() -> io::Result<()> { indefinitely. Since the call to `drop` happens implicitly and outside your control, there's no way to apply a timeout or fallback mechanism. + TODO: apply feedback + + ``` + I see what you mean. I'd suggest to first say that drop is special because it terminates + the object lifetime, so it is inherently a "one-shot" API. + That has consequences: things like caller-driven timeouts or retries + simply don't make sense - there's no object anymore after the first + call. (Emphasizing caller-driven is important) + + This equally applies to all APIs that consume the object by value. + + The fact that drop is usually called implicitly though is not important + here. For one, we can call it explicitly (std::mem::drop()); but if that + wasn't available, we could have wrapped the object with drop in an + Option, and then trigger drop by assigning None. + ``` + - 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. + (TODO: apply feedback: + Note that the chapter does not discuss poisoned mutexes + at the moment (I'm requesting that to be added in my comments above) + - 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. diff --git a/src/idiomatic/leveraging-the-type-system/raii/scope_guards.md b/src/idiomatic/leveraging-the-type-system/raii/scope_guards.md index 93e2c2c47489..273b8ac9d638 100644 --- a/src/idiomatic/leveraging-the-type-system/raii/scope_guards.md +++ b/src/idiomatic/leveraging-the-type-system/raii/scope_guards.md @@ -21,12 +21,35 @@ fn main() { // Write something to the file writeln!(file, "temporary data").unwrap(); + /* + * TODO: Apply Feedbak: + * + * Consider making the example a bit more realistic: set it up + * as if we are downloading a file through HTTP, and + * if the connection gets interrupted, we don't want to + * leave behind an incomplete file. I'm not asking to + * add/change code, only to update comments, + * and names of functions and variables. + */ + // Create a scope guard to clean up the file unless we defuse it let cleanup = guard(path, |path| { + println!("Operation failed, deleting file: {:?}", path); // Errors must be handled inside the guard, // but cannot be propagated. let _ = fs::remove_file(path); }); + /* + * TODO: apply feedback + * + * I think this should be put right after let mut file, in order to: + * + * + emphasize that file and cleanup go together, + * + * + to ensure that file gets deleted even if we + * have a problem in writeln!().unwrap(). + * + */ if conditional_success() { // Success path: we want to keep the file @@ -103,6 +126,27 @@ fn main() { } ``` + TODO apply feedback: + + ``` + For large chunks of code in the speaker notes like this, it'd be nicer + to put this in the playground and include the playground link in the + speaker notes instead. That makes it easier to pull up the example on + screen, instead of having to copy-paste it into a playground window + manually. + + That said, I'm not sure this is the example we want to use. This + re-implements the scopeguard crate's functionality, i.e. it's a general + purpose scope guard that calls a closure. If you're implementing a scope + guard manually, you're more likely to be implementing a guard that does + something specific since scopeguard already exists for doing general + purpose scope guards. + + Maybe rework this example to use the "delete a file on failure" example + that's in the slide? I can slap together a sketch in the playground if + it's not clear what I'm suggesting. + ``` + - 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) @@ -115,11 +159,15 @@ fn main() { 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. + TODO: again... more concise, e.g. reduce the above to: + + ``` + - `scopeguard` also supports selecting a + [`Strategy`](https://docs.rs/scopeguard/latest/scopeguard/trait.Strategy.html) + to determine when the cleanup logic should run, i.e. always, + only on successful exit, or only on unwind. The crate + also supports defining custom strategies. + ``` - Recalling the transaction example from [the drop bombs chapter](./drop_bomb.md), we can now combine both concepts: @@ -131,4 +179,12 @@ fn main() { drop logic, this pattern at least allows us to orchestrate fallbacks explicitly and with whatever guarantees or limits we require. + TODO: apply feedback for the above paragraph: + + ``` + Maybe move this point to the top, since it most directly explains what + this example is doing and why. You may want to reword/reorganize this to + merge it with the current first bullet point. + ``` +
diff --git a/src/idiomatic/welcome.md b/src/idiomatic/welcome.md index 889ba721ab2d..de4184a8bba7 100644 --- a/src/idiomatic/welcome.md +++ b/src/idiomatic/welcome.md @@ -32,6 +32,14 @@ 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 From d804144484782b8732b8268e6d5b72ca53d68887 Mon Sep 17 00:00:00 2001 From: Glen De Cauwsemaecker Date: Sat, 26 Jul 2025 21:18:24 +0200 Subject: [PATCH 5/7] add new `RAII` intro segment refresher on `RAII` and use the OS File Descriptor example to start the discussions around RAII all previous content is for now moved to `_old` for my own reference, will add the new content based on the agreed upon new structure next. --- src/SUMMARY.md | 3 - .../leveraging-the-type-system/_old/raii.md | 166 +++++++++++++ .../{ => _old}/raii/drop_bomb.md | 23 +- .../{ => _old}/raii/drop_limitations.md | 15 +- .../{ => _old}/raii/scope_guards.md | 0 .../leveraging-the-type-system/raii.md | 224 +++++++----------- src/idiomatic/welcome.md | 9 +- 7 files changed, 272 insertions(+), 168 deletions(-) create mode 100644 src/idiomatic/leveraging-the-type-system/_old/raii.md rename src/idiomatic/leveraging-the-type-system/{ => _old}/raii/drop_bomb.md (90%) rename src/idiomatic/leveraging-the-type-system/{ => _old}/raii/drop_limitations.md (94%) rename src/idiomatic/leveraging-the-type-system/{ => _old}/raii/scope_guards.md (100%) diff --git a/src/SUMMARY.md b/src/SUMMARY.md index 75e521af0fa7..5450b7e687e4 100644 --- a/src/SUMMARY.md +++ b/src/SUMMARY.md @@ -438,9 +438,6 @@ - [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) --- diff --git a/src/idiomatic/leveraging-the-type-system/_old/raii.md b/src/idiomatic/leveraging-the-type-system/_old/raii.md new file mode 100644 index 000000000000..bb48ef5dff39 --- /dev/null +++ b/src/idiomatic/leveraging-the-type-system/_old/raii.md @@ -0,0 +1,166 @@ +--- +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 uses RAII for managing heap memory. The `Drop` trait lets you extend this +pattern to anything else. + +```rust +use std::sync::Mutex; + +fn main() { + let mutex = Mutex::new(vec![1, 2, 3]); + + { + let mut data = mutex.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) + + TODO: consider devoting 1-2 slides to demonstrate the relevant snippets of + Mutex and MutexGuard API + +- You may recall from + [the Memory Management segment](../../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 is unwound on panic (which is the default), allowing for + graceful cleanup of resources. + + (TODO: we might want to refactor this to make clear this also happens in + normal function returns) + + 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), + in which case no destructors will run. + + - 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. + + TODO: apply feedback: + + ``` + I think this whole point can be pulled out into its own slide. + Talking about when Drop runs and when it doesn't is worth covering + directly. I think you'd also want to talk about forget on that slide, + and maybe briefly note that leaking destructors is not unsafe + (unless you plan to cover them elsewhere). + ``` + +- `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. + + TODO: revisit references to C++ and Java, be careful in wording. E.g. C++ and + Java their mutexes are also RAII based + ([std::lock_guard](https://en.cppreference.com/w/cpp/thread/lock_guard.html), + [absl::MutexLock](https://github.com/abseil/abseil-cpp/blob/master/absl/synchronization/mutex.h#L583), + `synchronized(obj) {}` in Java). + + TODO: incorporate @gribozavr's feedback here: + + ``` + It can't be forgotten, but the MutexGuard can be forgot()'en intentionally, or leaked - like any other value. + + It is a good tie-in to discuss use cases for drop: it is good for cleaning up things within the scope of a process, but not the right tool for guaranteeing that something happens outside of the process (e.g., on local disk, or in another service in a distributed system). + + For example, it is a bad idea to rely exclusively on drop to clean up temporary files: if the program terminates in a way that skips running drop, temporary files will persist, and eventually the computer will run out of space. This can happen if the program crashes or leaks the value whose drop is responsible for deleting the file. In addition to a drop implementation within the program, one also needs a classic unix-style temp file reaper that runs as a separate process. + ``` + +- In other scenarios, the `Drop` trait shows its limitations. Next, we'll look + at what those are and how we can address them. + +TODO: apply feedback from @gribozavr when refactoring the RAII content: + +``` +First, a custom File type that wraps a file descriptor. A file descriptor is a classic OS-level resource. We could show how to implement a simple read-only file type a with a minimal API: open() and read() to read a single byte. Then show how to implement Drop. Discuss when the drop() function runs, and how it isn't run when values are moved (contrast with C++ where the destructor always runs at the end of the scope, even for moved-from values). Show the forget() function, discuss its signature and what it means. + +In other words, use this simple File type as an opportunity to do a 5-minute refresher on drop and move semantics. I see you're already doing it with instructor notes like "for a composite type such as a struct, all its fields will be dropped" and by mentioning the std::mem::drop() function. Let's lean more into it and make sure that during this discussion we have an example of a drop implementation on the screen. + +Then we move on to Mutex. There we would focus on explaining the idea that for a mutex the "resource" is more abstract. In case of a mutex, the resource is exclusive access to the wrapped value. Thus, we need a second type - a MutexGuard - to represent that. + +The mutex example is perfect to facilitate the drop x panic discussion. Maybe draft an extra slide that shows what happens by default with a naive drop implementation (the drop simply runs, no special code is needed for that), and then discuss why panics poison the mutex in Rust (there is a good chance that the code was mutating the shared data, so its invariants might be broken). +``` + +Also apply feedback from @djimitche: + +``` +It's probably only necessary to include one "callback" to Fundamentals -- +the important point is to that this slide is a quick review of previous +content, and if students need a deeper refresher they can find that +content in the Fundamentals course. + +That said, these speaker notes are pretty long! Is it possible to trim +this down to just call out the bits necessary for the RAII patterns +introduced here, leaving the rest to the students' memory of Fundamentals? +``` + +
diff --git a/src/idiomatic/leveraging-the-type-system/raii/drop_bomb.md b/src/idiomatic/leveraging-the-type-system/_old/raii/drop_bomb.md similarity index 90% rename from src/idiomatic/leveraging-the-type-system/raii/drop_bomb.md rename to src/idiomatic/leveraging-the-type-system/_old/raii/drop_bomb.md index 8c6d17a69b3d..bed30a6eb5bf 100644 --- a/src/idiomatic/leveraging-the-type-system/raii/drop_bomb.md +++ b/src/idiomatic/leveraging-the-type-system/_old/raii/drop_bomb.md @@ -13,7 +13,6 @@ handled normally in Drop (realistically, commit and rollback would want to return Results to indicate if they succeeded or not). This is one of the limitations of Drop mentioned on the previous slide, so it'd be worth noting that this pattern is a common way to work around that limitation. - --- ```rust @@ -92,17 +91,14 @@ impl Drop for Transaction { it's typically not possible to just rollback a transaction in drop(). ``` -- APIs that are expected to panic like this should document - the cases when a panic will occur under a `Panics` section. +- APIs that are expected to panic like this should document the cases when a + panic will occur under a `Panics` section. - (^ TODO: this was reworded to be more minimal. Shorter the speaker - notes the better, to make it easier to skim through as instructor) - Original: - > 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. + (^ TODO: this was reworded to be more minimal. Shorter the speaker notes the + better, to make it easier to skim through as instructor) Original: + > 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. TODO: apply feedback: @@ -141,9 +137,8 @@ impl Drop for Transaction { - [`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. - This requires unsafe code to use, - though, so it's recommended to only use this if strictly necessary. + making manual cleanup required and explicit. This requires unsafe code to + use, though, so it's recommended to only use this if strictly necessary. - 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 diff --git a/src/idiomatic/leveraging-the-type-system/raii/drop_limitations.md b/src/idiomatic/leveraging-the-type-system/_old/raii/drop_limitations.md similarity index 94% rename from src/idiomatic/leveraging-the-type-system/raii/drop_limitations.md rename to src/idiomatic/leveraging-the-type-system/_old/raii/drop_limitations.md index 8747ff2557ea..2a359805aa45 100644 --- a/src/idiomatic/leveraging-the-type-system/raii/drop_limitations.md +++ b/src/idiomatic/leveraging-the-type-system/_old/raii/drop_limitations.md @@ -19,7 +19,8 @@ fn write_log() -> io::Result<()> {
- In the earlier example, our `File` resource owns a file handle provided by the - operating system. (TODO: be careful in wording: earlier is ambiguous here. Better use "above".) + operating system. (TODO: be careful in wording: earlier is ambiguous here. + Better use "above".) [As stated in the documentation](https://doc.rust-lang.org/std/fs/struct.File.html): @@ -61,8 +62,8 @@ fn write_log() -> io::Result<()> { - 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. - (TODO: be careful in wording and context. E.g. here it is about external resources) + will abort immediately, and remaining resources will not be cleaned up. (TODO: + be careful in wording and context. E.g. here it is about external resources) While panicking in `drop` can serve certain purposes (see [the next chapter on "drop bombs"](./drop_bomb.md)), it should be used @@ -75,7 +76,8 @@ fn write_log() -> io::Result<()> { (TODO: non-deterministic is incorrect here, fix wording and description) - (TODO: be careful with wording 'you cannot control'. As you can control, by impl drop) + (TODO: be careful with wording 'you cannot control'. As you can control, by + impl drop) 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 @@ -108,9 +110,8 @@ fn write_log() -> io::Result<()> { cause undefined behavior. The poisoned state disappears along with the termination of the program. - (TODO: apply feedback: - Note that the chapter does not discuss poisoned mutexes - at the moment (I'm requesting that to be added in my comments above) + (TODO: apply feedback: Note that the chapter does not discuss poisoned mutexes + at the moment (I'm requesting that to be added in my comments above) - 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. diff --git a/src/idiomatic/leveraging-the-type-system/raii/scope_guards.md b/src/idiomatic/leveraging-the-type-system/_old/raii/scope_guards.md similarity index 100% rename from src/idiomatic/leveraging-the-type-system/raii/scope_guards.md rename to src/idiomatic/leveraging-the-type-system/_old/raii/scope_guards.md diff --git a/src/idiomatic/leveraging-the-type-system/raii.md b/src/idiomatic/leveraging-the-type-system/raii.md index f8a20293e8e0..fec0461d2e8d 100644 --- a/src/idiomatic/leveraging-the-type-system/raii.md +++ b/src/idiomatic/leveraging-the-type-system/raii.md @@ -2,165 +2,111 @@ minutes: 30 --- -# RAII and `Drop` in Practice +# RAII: `Drop` trait -RAII (_Resource Acquisition Is Initialization_) means tying the lifetime of a +RAII (Resource Acquisition Is Initialization) means tying the lifetime of a resource to the lifetime of a value. -Rust uses RAII for managing heap memory. The `Drop` trait lets you -extend this pattern to anything else. +[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 -use std::sync::Mutex; +```rust,editable +struct FileLock; +struct File { + stub: Option, + lock: FileLock, +} +#[derive(Debug)] +struct Error; + +impl File { + fn open(path: &str) -> Result { + println!("acquire file descriptor: {path}"); + Ok(Self { stub: Some(1), lock: FileLock }) + } + + fn read(&mut self) -> Result { + self.stub.take().ok_or(Error) + } + + fn close(self) -> Result<(), Error> { + self.lock.release() + } +} + +impl FileLock { + fn release(self) -> Result<(), Error> { + println!("release file descriptor"); + Ok(()) + } +} fn main() { - let mutex = Mutex::new(vec![1, 2, 3]); + let mut file = File::open("example.txt").unwrap(); + + let mut content = Vec::new(); + while let Ok(byte) = file.read() { + content.push(byte); + } - { - let mut data = mutex.lock().unwrap(); - data.push(4); // lock held here - } // lock automatically released here + println!("content: {content:?}"); } ```
-- 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) - - TODO: consider devoting 1-2 slides to demonstrate the relevant snippets of - Mutex and MutexGuard API - -- You may recall from - [the Memory Management segment](../../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 is unwound on panic (which is the default), allowing for graceful - cleanup of resources. - - (TODO: we might want to refactor this to make clear - this also happens in normal function returns) - - 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), - in which case no destructors will run. - - - 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. - - TODO: apply feedback: - - ``` - I think this whole point can be pulled out into its own slide. - Talking about when Drop runs and when it doesn't is worth covering - directly. I think you'd also want to talk about forget on that slide, - and maybe briefly note that leaking destructors is not unsafe - (unless you plan to cover them elsewhere). - ``` - -- `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. - - TODO: revisit references to C++ and Java, be careful in wording. - E.g. C++ and Java their mutexes are also RAII based - ([std::lock_guard](https://en.cppreference.com/w/cpp/thread/lock_guard.html), [absl::MutexLock](https://github.com/abseil/abseil-cpp/blob/master/absl/synchronization/mutex.h#L583), `synchronized(obj) {}` in Java). - - TODO: incorporate @gribozavr's feedback here: - +- The example shows how easy it is to forget releasing a file descriptor when + managing it manually. In fact, the current code does not release it at all. + Did anyone notice that `file.close()` is missing? + +- Try inserting `file.close().unwrap();` at the end of `main`. Then try moving + it before the loop — Rust will prevent that. Once `file` is moved, it can no + longer be used. The borrow checker ensures we cannot access a `File` after it + has been closed. + +- Instead of relying on the programmer to remember to call `close()`, we can + implement the `Drop` trait to handle cleanup automatically. This ties the + resource to the lifetime of the `File` value. But note: `Drop` cannot return + errors. Anything fallible must be handled inside the `drop()` method or + avoided altogether. + + ```rust,compile_fail + impl Drop for FileLock { + fn drop(&mut self) { + println!("release file descriptor automatically"); + } + } ``` - It can't be forgotten, but the MutexGuard can be forgot()'en intentionally, or leaked - like any other value. - It is a good tie-in to discuss use cases for drop: it is good for cleaning up things within the scope of a process, but not the right tool for guaranteeing that something happens outside of the process (e.g., on local disk, or in another service in a distributed system). +- If we keep both `drop()` and `close()`, the file descriptor is released twice. + To avoid this, remove `close()` and rely on `Drop` alone. - For example, it is a bad idea to rely exclusively on drop to clean up temporary files: if the program terminates in a way that skips running drop, temporary files will persist, and eventually the computer will run out of space. This can happen if the program crashes or leaks the value whose drop is responsible for deleting the file. In addition to a drop implementation within the program, one also needs a classic unix-style temp file reaper that runs as a separate process. - ``` - -- In other scenarios, the `Drop` trait shows its limitations. Next, we'll look - at what those are and how we can address them. - -TODO: apply feedback from @gribozavr when refactoring the RAII content: - -``` -First, a custom File type that wraps a file descriptor. A file descriptor is a classic OS-level resource. We could show how to implement a simple read-only file type a with a minimal API: open() and read() to read a single byte. Then show how to implement Drop. Discuss when the drop() function runs, and how it isn't run when values are moved (contrast with C++ where the destructor always runs at the end of the scope, even for moved-from values). Show the forget() function, discuss its signature and what it means. +- Demonstrate ownership transfer by moving the file into a separate `read_all()` + function. The file is dropped when that local variable goes out of scope — not + in `main`. This contrasts with C++, where the original scope always runs the + destructor, even after moves. -In other words, use this simple File type as an opportunity to do a 5-minute refresher on drop and move semantics. I see you're already doing it with instructor notes like "for a composite type such as a struct, all its fields will be dropped" and by mentioning the std::mem::drop() function. Let's lean more into it and make sure that during this discussion we have an example of a drop implementation on the screen. +- Add `panic!("oops")` at the start of `read_all()` to illustrate that `drop()` + still runs during unwinding. Rust guarantees that destructors run during a + panic unless the panic strategy is set to abort. -Then we move on to Mutex. There we would focus on explaining the idea that for a mutex the "resource" is more abstract. In case of a mutex, the resource is exclusive access to the wrapped value. Thus, we need a second type - a MutexGuard - to represent that. +- There are exceptions where destructors will not run: + - If a destructor panics during unwinding, the program aborts immediately. + - The program also aborts immediately when using `std::process::exit()` or + when the panic strategy is set to `abort`. -The mutex example is perfect to facilitate the drop x panic discussion. Maybe draft an extra slide that shows what happens by default with a naive drop implementation (the drop simply runs, no special code is needed for that), and then discuss why panics poison the mutex in Rust (there is a good chance that the code was mutating the shared data, so its invariants might be broken). -``` - -Also apply feedback from @djimitche: +### 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 notify another system before shutdown. -It's probably only necessary to include one "callback" to Fundamentals -- -the important point is to that this slide is a quick review of previous -content, and if students need a deeper refresher they can find that -content in the Fundamentals course. - -That said, these speaker notes are pretty long! Is it possible to trim -this down to just call out the bits necessary for the RAII patterns -introduced here, leaving the rest to the students' memory of Fundamentals? -``` +- Learn more: + +- Available on nightly: +
diff --git a/src/idiomatic/welcome.md b/src/idiomatic/welcome.md index de4184a8bba7..efe7499269bf 100644 --- a/src/idiomatic/welcome.md +++ b/src/idiomatic/welcome.md @@ -34,11 +34,10 @@ 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. +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 From 5c2ec8c5b1ca9a854fcbb8669f9418264f4a968e Mon Sep 17 00:00:00 2001 From: Glen De Cauwsemaecker Date: Sun, 27 Jul 2025 18:47:38 +0200 Subject: [PATCH 6/7] further improvements to RAII intro chapter --- .../leveraging-the-type-system/raii.md | 67 +++++++++++-------- 1 file changed, 39 insertions(+), 28 deletions(-) diff --git a/src/idiomatic/leveraging-the-type-system/raii.md b/src/idiomatic/leveraging-the-type-system/raii.md index fec0461d2e8d..60a8455d6e0d 100644 --- a/src/idiomatic/leveraging-the-type-system/raii.md +++ b/src/idiomatic/leveraging-the-type-system/raii.md @@ -13,24 +13,24 @@ descriptors or locks. ```rust,editable struct FileLock; -struct File { +pub struct File { stub: Option, lock: FileLock, } #[derive(Debug)] -struct Error; +pub struct Error; impl File { - fn open(path: &str) -> Result { + pub fn open(path: &str) -> Result { println!("acquire file descriptor: {path}"); Ok(Self { stub: Some(1), lock: FileLock }) } - fn read(&mut self) -> Result { + pub fn read(&mut self) -> Result { self.stub.take().ok_or(Error) } - fn close(self) -> Result<(), Error> { + pub fn close(self) -> Result<(), Error> { self.lock.release() } } @@ -56,20 +56,18 @@ fn main() {
-- The example shows how easy it is to forget releasing a file descriptor when +- This example shows how easy it is to forget releasing a file descriptor when managing it manually. In fact, the current code does not release it at all. Did anyone notice that `file.close()` is missing? - Try inserting `file.close().unwrap();` at the end of `main`. Then try moving - it before the loop — Rust will prevent that. Once `file` is moved, it can no - longer be used. The borrow checker ensures we cannot access a `File` after it - has been closed. + it before the loop. Rust will reject this: once `file` is moved, it can no + longer be accessed. The borrow checker enforces this statically. -- Instead of relying on the programmer to remember to call `close()`, we can - implement the `Drop` trait to handle cleanup automatically. This ties the - resource to the lifetime of the `File` value. But note: `Drop` cannot return - errors. Anything fallible must be handled inside the `drop()` method or - avoided altogether. +- Instead of relying on the user to remember to call `close()`, we can implement + the `Drop` trait to release the resource automatically. This ties cleanup to + the lifetime of the `File` value. Note that `Drop` cannot return errors, so + any fallible logic must be handled internally or avoided. ```rust,compile_fail impl Drop for FileLock { @@ -79,30 +77,43 @@ fn main() { } ``` -- If we keep both `drop()` and `close()`, the file descriptor is released twice. - To avoid this, remove `close()` and rely on `Drop` alone. +- If both `drop()` and `close()` are present, the file descriptor is released + twice. To avoid this, remove `close()` and rely solely on `Drop`. -- Demonstrate ownership transfer by moving the file into a separate `read_all()` - function. The file is dropped when that local variable goes out of scope — not - in `main`. This contrasts with C++, where the original scope always runs the - destructor, even after moves. + This also illustrates that when a parent type is dropped, the `drop()` method + of its fields (such as `FileLock`) is automatically called — no extra code is + needed. -- Add `panic!("oops")` at the start of `read_all()` to illustrate that `drop()` - still runs during unwinding. Rust guarantees that destructors run during a - panic unless the panic strategy is set to abort. +- Demonstrate ownership transfer by moving the file into a `read_all()` + function. The file is dropped when the local variable inside that function + goes out of scope, not in `main`. + + This differs from C++, where destructors are tied to the original scope, even + for moved-from values. + + The same mechanism underlies `std::mem::drop`, which lets you drop a value + early: + + ```rust + pub fn drop(_x: T) {} + ``` + +- Insert `panic!("oops")` at the start of `read_all()` to show that `drop()` is + still called during unwinding. Rust ensures this unless the panic strategy is + set to `abort`. - There are exceptions where destructors will not run: - If a destructor panics during unwinding, the program aborts immediately. - - The program also aborts immediately when using `std::process::exit()` or - when the panic strategy is set to `abort`. + - The program also aborts when using `std::process::exit()` or when compiled + with the `abort` panic strategy. ### 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 notify another system before shutdown. +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: From 0baa9903060c8b3c790f8d6345d98a3bf2a10935 Mon Sep 17 00:00:00 2001 From: Glen De Cauwsemaecker Date: Sun, 27 Jul 2025 18:49:33 +0200 Subject: [PATCH 7/7] fix typo --- .../leveraging-the-type-system/_old/raii/drop_limitations.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/idiomatic/leveraging-the-type-system/_old/raii/drop_limitations.md b/src/idiomatic/leveraging-the-type-system/_old/raii/drop_limitations.md index 2a359805aa45..12098a2ba2b0 100644 --- a/src/idiomatic/leveraging-the-type-system/_old/raii/drop_limitations.md +++ b/src/idiomatic/leveraging-the-type-system/_old/raii/drop_limitations.md @@ -49,7 +49,7 @@ fn write_log() -> io::Result<()> { ``` This last sentence suggests that there was no other design choice because of unwinding. - That's not true: in C++, for example, one can throw an exception from a destructor while uwinding + That's not true: in C++, for example, one can throw an exception from a destructor while unwinding because of another exception. Throwing from a destructor is messy and error-prone (and pretty much every style guide tells you not to do it), however that is an existence proof that Rust's design choice here was not entirely forced.