diff --git a/src/SUMMARY.md b/src/SUMMARY.md index a5929bad8455..9b34e23ffb8a 100644 --- a/src/SUMMARY.md +++ b/src/SUMMARY.md @@ -441,6 +441,14 @@ - [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) + - [Mutex](idiomatic/leveraging-the-type-system/raii/mutex.md) + - [Drop Guards](idiomatic/leveraging-the-type-system/raii/drop_guards.md) + - [Drop Bomb](idiomatic/leveraging-the-type-system/raii/drop_bomb.md) + - [Drop Skipped](idiomatic/leveraging-the-type-system/raii/drop_skipped.md) + - [Drop Bomb Forget](idiomatic/leveraging-the-type-system/raii/drop_bomb_forget.md) + - [Scope Guard](idiomatic/leveraging-the-type-system/raii/scope_guard.md) + - [Drop Option](idiomatic/leveraging-the-type-system/raii/drop_option.md) - [Extension Traits](idiomatic/leveraging-the-type-system/extension-traits.md) - [Extending Foreign Types](idiomatic/leveraging-the-type-system/extension-traits/extending-foreign-types.md) - [Method Resolution Conflicts](idiomatic/leveraging-the-type-system/extension-traits/method-resolution-conflicts.md) 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..2f654650cd62 --- /dev/null +++ b/src/idiomatic/leveraging-the-type-system/raii.md @@ -0,0 +1,92 @@ +--- +minutes: 60 +--- + +# RAII: `Drop` trait + +RAII (**R**esource **A**cquisition **I**s **I**nitialization) ties the lifetime +of a resource to the lifetime of a value. + +[Rust uses RAII to manage memory](https://doc.rust-lang.org/rust-by-example/scope/raii.html), +and the `Drop` trait allows you to extend this to other resources, such as file +descriptors or locks. + +```rust,editable +pub struct File(std::os::fd::RawFd); + +impl File { + pub fn open(path: &str) -> Result { + // [...] + Ok(Self(0)) + } + + pub fn read_to_end(&mut self) -> Result, std::io::Error> { + // [...] + Ok(b"example".to_vec()) + } + + pub fn close(self) -> Result<(), std::io::Error> { + // [...] + Ok(()) + } +} + +fn main() -> Result<(), std::io::Error> { + let mut file = File::open("example.txt")?; + println!("content: {:?}", file.read_to_end()?); + Ok(()) +} +``` + +
+ +- Easy to miss: `file.close()` is never called. Ask the class if they noticed. + +- To release the file descriptor correctly, `file.close()` must be called after + the last use — and also in early-return paths in case of errors. + +- Instead of relying on the user to call `close()`, we can implement the `Drop` + trait to release the resource automatically. This ties cleanup to the lifetime + of the `File` value. + + ```rust,compile_fail + impl Drop for File { + fn drop(&mut self) { + println!("release file descriptor automatically"); + } + } + ``` + +- Note that `Drop::drop` cannot return a Result. Any fallible logic must be + handled internally or ignored. In the standard library, errors during FD + closure inside `Drop` are silently discarded. See the implementation: + + +- When is `Drop::drop` called? + + Normally, when the `file` variable in `main` goes out of scope (either on + return or due to a panic), `drop()` is called automatically. + + If the file is moved into another function, the value is dropped when that + function returns — not in `main`. + + In contrast, C++ runs destructors in the original scope even for moved-from + values. + +- Demo: insert `panic!("oops")` at the start of `read_to_end()` and run it. + `drop()` still runs during unwinding. + +### More to Explore + +The `Drop` trait has another important limitation: it is not `async`. + +This means you cannot `await` inside a destructor, which is often needed when +cleaning up asynchronous resources like sockets, database connections, or tasks +that must signal completion to another system. + +- Learn more: + +- There is an experimental `AsyncDrop` trait available on nightly: + + +
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..1025c2a39dc6 --- /dev/null +++ b/src/idiomatic/leveraging-the-type-system/raii/drop_bomb.md @@ -0,0 +1,85 @@ +# Drop Bombs: Enforcing API Correctness + +Use `Drop` to enforce invariants and detect incorrect API usage. A "drop bomb" +panics if a value is dropped without being explicitly finalized. + +This pattern is often used when the finalizing operation (like `commit()` or +`rollback()`) needs to return a `Result`, which cannot be done from `Drop`. + +```rust,editable +use std::io::{self, Write}; + +struct Transaction { + active: bool, +} + +impl Transaction { + fn start() -> Self { + Self { active: true } + } + + fn commit(mut self) -> io::Result<()> { + writeln!(io::stdout(), "COMMIT")?; + self.active = false; + Ok(()) + } +} + +impl Drop for Transaction { + fn drop(&mut self) { + if self.active { + panic!("Transaction dropped without commit!"); + } + } +} + +fn main() -> io::Result<()> { + let tx = Transaction::start(); + // Use `tx` to build the transaction, then commit it. + // Comment out the call to `commit` to see the panic. + tx.commit()?; + Ok(()) +} +``` + +
+ +- A drop bomb ensures that a value like `Transaction` cannot be silently dropped + in an unfinished state. The destructor panics if the transaction has not been + explicitly finalized (for example, with `commit()`). + +- The finalizing operation (such as `commit()`) usually takes `self` by value. + This ensures that once the transaction is finalized, the original object can + no longer be used. + +- A common reason to use this pattern is when cleanup cannot be done in `Drop`, + either because it is fallible or asynchronous. + +- This pattern is appropriate even in public APIs. It can help users catch bugs + early when they forget to explicitly finalize a transactional object. + +- If cleanup can safely happen in `Drop`, some APIs choose to panic only in + debug builds. Whether this is appropriate depends on the guarantees your API + must enforce. + +- Panicking in release builds is reasonable when silent misuse would cause major + correctness or security problems. + +## More to explore + +Several related patterns help enforce correct teardown or prevent accidental +drops. + +- The [`drop_bomb` crate](https://docs.rs/drop_bomb/latest/drop_bomb/): A small + utility that panics if dropped unless explicitly defused with `.defuse()`. + Comes with a `DebugDropBomb` variant that only activates in debug builds. + +- In some systems, a value must be finalized by a specific API before it is + dropped. + + For example, an `SshConnection` might need to be deregistered from an + `SshServer` before being dropped, or the program panics. This helps catch + programming mistakes during development and enforces correct teardown at + runtime. + +
diff --git a/src/idiomatic/leveraging-the-type-system/raii/drop_bomb_forget.md b/src/idiomatic/leveraging-the-type-system/raii/drop_bomb_forget.md new file mode 100644 index 000000000000..043babbb30d7 --- /dev/null +++ b/src/idiomatic/leveraging-the-type-system/raii/drop_bomb_forget.md @@ -0,0 +1,53 @@ +# Drop Bombs: using `std::mem::forget` + +```rust,editable +use std::io::{self, Write}; + +struct Transaction; + +impl Transaction { + fn start() -> Self { + Transaction + } + + fn commit(self) -> io::Result<()> { + writeln!(io::stdout(), "COMMIT")?; + + // Defuse the drop bomb by preventing Drop from ever running. + std::mem::forget(self); + + Ok(()) + } +} + +impl Drop for Transaction { + fn drop(&mut self) { + // This is the "drop bomb" + panic!("Transaction dropped without commit!"); + } +} + +fn main() -> io::Result<()> { + let tx = Transaction::start(); + // Use `tx` to build the transaction, then commit it. + // Comment out the call to `commit` to see the panic. + tx.commit()?; + Ok(()) +} +``` + +
+ +In the previous slide we saw that calling +[`std::mem::forget`](https://doc.rust-lang.org/std/mem/fn.forget.html) prevents +`Drop::drop` from ever running. + +Remember that `mem::forget` intentionally leaks the value. This is memory-safe +in Rust, but the memory will not be reclaimed. + +However, this avoids needing a runtime flag: when the transaction is +successfully committed, we can _defuse_ the drop bomb — meaning we prevent +`Drop` from running — by calling `std::mem::forget` on the value instead of +letting its destructor run. + +
diff --git a/src/idiomatic/leveraging-the-type-system/raii/drop_guards.md b/src/idiomatic/leveraging-the-type-system/raii/drop_guards.md new file mode 100644 index 000000000000..f8c9f7123302 --- /dev/null +++ b/src/idiomatic/leveraging-the-type-system/raii/drop_guards.md @@ -0,0 +1,65 @@ +# Drop Guards + +A **drop guard** in Rust is a temporary object that performs some kind of +cleanup when it goes out of scope. In the case of `Mutex`, the `lock` method +returns a `MutexGuard` that automatically unlocks the mutex on `drop`: + +```rust +struct Mutex { + is_locked: bool, +} + +struct MutexGuard<'a> { + mutex: &'a mut Mutex, +} + +impl Mutex { + fn new() -> Self { + Self { is_locked: false } + } + + fn lock(&mut self) -> MutexGuard<'_> { + self.is_locked = true; + MutexGuard { mutex: self } + } +} + +impl Drop for MutexGuard<'_> { + fn drop(&mut self) { + self.mutex.is_locked = false; + } +} +``` + +
+ +- The example above shows a simplified `Mutex` and its associated guard. + +- Even though it is not a production-ready implementation, it illustrates the + core idea: + + - the guard represents exclusive access, + - and its `Drop` implementation releases the lock when it goes out of scope. + +## More to Explore + +This example shows a C++ style mutex that does not contain the data it protects. +While this is non-idiomatic in Rust, the goal here is only to illustrate the +core idea of a drop guard, not to demonstrate a proper Rust mutex design. + +For brevity, several features are omitted: + +- A real `Mutex` stores the protected value inside the mutex.\ + This toy example omits the value entirely to focus only on the drop guard + mechanism. +- Ergonomic access via `Deref` and `DerefMut` on `MutexGuard` (letting the guard + behave like a `&T` or `&mut T`). +- A fully blocking `.lock()` method and a non-blocking `try_lock` variant. + +You can explore the +[`Mutex` implementation in Rust’s std library](https://doc.rust-lang.org/std/sync/struct.Mutex.html) +as an example of a production-ready mutex. The +[`Mutex` from the `parking_lot` crate](https://docs.rs/parking_lot/latest/parking_lot/type.Mutex.html) +is another worthwhile reference. + +
diff --git a/src/idiomatic/leveraging-the-type-system/raii/drop_option.md b/src/idiomatic/leveraging-the-type-system/raii/drop_option.md new file mode 100644 index 000000000000..6d3ff9e75662 --- /dev/null +++ b/src/idiomatic/leveraging-the-type-system/raii/drop_option.md @@ -0,0 +1,84 @@ +# Drop: Option + +```rust,editable +struct File(Option); + +impl File { + fn open(path: &'static str) -> std::io::Result { + Ok(Self(Some(Handle { path }))) + } + + fn write(&mut self, data: &str) -> std::io::Result<()> { + match &mut self.0 { + Some(handle) => println!("write '{data}' to file '{}'", handle.path), + None => unreachable!(), + } + Ok(()) + } + + fn close(mut self) -> std::io::Result<&'static str> { + Ok(self.0.take().unwrap().path) + } +} + +impl Drop for File { + fn drop(&mut self) { + if let Some(handle) = self.0.take() { + println!("automatically close handle for file: {}", handle.path); + } + } +} + +struct Handle { + path: &'static str, +} +impl Drop for Handle { + fn drop(&mut self) { + println!("close handle for file: {}", self.path) + } +} + +fn main() -> std::io::Result<()> { + let mut file = File::open("foo.txt")?; + file.write("hello")?; + println!("manually closed file: {}", file.close()?); + Ok(()) +} +``` + +
+ +- In this example we want to let the user call `close()` manually so that errors + from closing the file can be reported explicitly. + +- At the same time we still want RAII semantics: if the user forgets to call + `close()`, the handle must be cleaned up automatically in `Drop`. + +- Wrapping the handle in an `Option` gives us both behaviors. `close()` extracts + the handle with `take()`, and `Drop` only runs cleanup if a handle is still + present. + + Demo: remove the `.close()` call and run the code — `Drop` now prints the + automatic cleanup. + +- The main downside is ergonomics. `Option` forces us to handle both the `Some` + and `None` case even in places where, logically, `None` cannot occur. Rust’s + type system cannot express that relationship between `File` and its `Handle`, + so we handle both cases manually. + +## More to explore + +Instead of `Option` we could use +[`ManuallyDrop`](https://doc.rust-lang.org/std/mem/struct.ManuallyDrop.html), +which suppresses automatic destruction by preventing Rust from calling `Drop` +for the value; you must handle teardown yourself. + +The [_scopeguard_ example](./scope_guard.md) on the previous slide shows how +`ManuallyDrop` can replace `Option` to avoid handling `None` in places where the +value should always exist. + +In such designs we typically track the drop state with a separate flag next to +the `ManuallyDrop`, which lets us track whether the handle has already +been manually consumed. + +
diff --git a/src/idiomatic/leveraging-the-type-system/raii/drop_skipped.md b/src/idiomatic/leveraging-the-type-system/raii/drop_skipped.md new file mode 100644 index 000000000000..f662ee5ab781 --- /dev/null +++ b/src/idiomatic/leveraging-the-type-system/raii/drop_skipped.md @@ -0,0 +1,69 @@ +# Drop can be skipped + +There are cases where destructors will not run. + +```rust,editable +#[derive(Debug)] +struct Foo(T); + +impl Drop for Foo { + fn drop(&mut self) { + println!("{self:?}::drop() called"); + + // panic!("{self:?}::drop() panics"); + + println!("{self:?}::drop() finished, with inner: {:?}", self.0); + } +} + +fn main() { + let value = Foo(Foo(Foo(()))); + + std::process::exit(0); + + // std::mem::forget(value); + + // panic!("main() panics with value: {value:?}"); +} +``` + +
+ +- In the version that calls + [`std::process::exit`](https://doc.rust-lang.org/std/process/fn.exit.html), + `Foo::drop` is never run because `exit` terminates the process immediately + without unwinding. + + - You can prevent accidental use of `exit` by denying the + [`clippy::exit`](https://rust-lang.github.io/rust-clippy/stable/index.html#exit) + lint. + +- If you remove the `std::process::exit(0)` line, the stack will unwind normally + and each `drop()` method will run in turn. + +- Try uncommenting the + [`std::mem::forget`](https://doc.rust-lang.org/std/mem/fn.forget.html) call. + What do you think will happen? + + Forgetting a value intentionally _leaks_ it — the memory is never reclaimed, + but this is still memory-safe in Rust. Since the value is never dropped, its + destructor does not run. + + [`Box::leak`](https://doc.rust-lang.org/std/boxed/struct.Box.html#method.leak) + is another example of intentional leaking, often used to create data that + lives for the remainder of the process. + +- Remove the `mem::forget` call, then uncomment the `panic!` below it. What do + you expect now? + + With the default `panic = "unwind"` setting, the stack still unwinds and + destructors run, even when the panic starts in `main`. + + - With + [`panic = "abort"`](https://doc.rust-lang.org/cargo/reference/profiles.html#panic), + no unwinding takes place. + +- Finally, uncomment the `panic!` inside `Foo::drop` and run it. Ask the class: + which destructors run before the abort? + +
diff --git a/src/idiomatic/leveraging-the-type-system/raii/mutex.md b/src/idiomatic/leveraging-the-type-system/raii/mutex.md new file mode 100644 index 000000000000..ba93a93daadc --- /dev/null +++ b/src/idiomatic/leveraging-the-type-system/raii/mutex.md @@ -0,0 +1,40 @@ +# Mutex and MutexGuard + +In earlier examples, RAII was used to manage concrete resources like file +descriptors. With a `Mutex`, the "resource" is mutable access to a value. You +access the value by calling `lock`, which then returns a `MutexGuard` which will +unlock the `Mutex` automatically when dropped. + +```rust +use std::sync::Mutex; + +fn main() { + let m = Mutex::new(vec![1, 2, 3]); + + let mut guard = m.lock().unwrap(); + guard.push(4); + guard.push(5); + println!("{guard:?}"); +} +``` + +
+ +- A `Mutex` controls exclusive access to a value. Unlike earlier RAII examples, + the resource here is logical: temporary exclusive access to the data inside. + +- This right is represented by a `MutexGuard`. Only one guard for this mutex can + exist at a time. While it lives, it provides `&mut T` access. + +- Although `lock()` takes `&self`, it returns a `MutexGuard` with mutable + access. This works through _interior mutability_, where a type manages its own + borrowing rules internally to allow mutation through `&self`. + +- `MutexGuard` implements `Deref` and `DerefMut`, making access ergonomic. You + lock the mutex, use the guard like a `&mut T`, and the lock is released + automatically when the guard goes out of scope. + +- The release is handled by `Drop`. You never call an explicit unlock function. + The guard’s `Drop` implementation releases the lock automatically. + +
diff --git a/src/idiomatic/leveraging-the-type-system/raii/scope_guard.md b/src/idiomatic/leveraging-the-type-system/raii/scope_guard.md new file mode 100644 index 000000000000..739acad6f5de --- /dev/null +++ b/src/idiomatic/leveraging-the-type-system/raii/scope_guard.md @@ -0,0 +1,60 @@ +# Scope Guards + +A scope guard uses the `Drop` trait to run cleanup code automatically when a +scope exits, even during unwinding. + +```rust,editable,compile_fail +use scopeguard::{ScopeGuard, guard}; +use std::fs::{self, File}; +use std::io::Write; + +fn download_successful() -> bool { + // [...] + true +} + +fn main() { + let path = "download.tmp"; + let mut file = File::create(path).expect("cannot create temporary file"); + + // Set up cleanup immediately after file creation + let cleanup = guard(path, |path| { + println!("download failed, deleting: {:?}", path); + let _ = fs::remove_file(path); + }); + + writeln!(file, "partial data...").unwrap(); + + if download_successful() { + // Download succeeded, keep the file + let path = ScopeGuard::into_inner(cleanup); + println!("Download '{path}' complete!"); + } + // Otherwise, the guard runs and deletes the file +} +``` + +
+ +- This example models a download workflow. We create a temporary file first, + then use a scope guard to ensure that the file is deleted if the download + fails. + +- The guard is placed directly after creating the file, so even if `writeln!()` + fails, the file will still be cleaned up. This ordering is essential for + correctness. + +- The guard's closure runs on scope exit unless it is _defused_ with + `ScopeGuard::into_inner` (removing the value so the guard does nothing on + drop). In the success path, we call `into_inner` so the guard will not delete + the file. + +- This pattern is useful when you want fallbacks or cleanup code to run + automatically but only if success is not explicitly signaled. + +- The `scopeguard` crate also supports cleanup strategies via the + [`Strategy`](https://docs.rs/scopeguard/latest/scopeguard/trait.Strategy.html) + trait. You can choose to run the guard on unwind only, or on success only, not + just always. + +
diff --git a/src/idiomatic/leveraging-the-type-system/typestate-pattern.md b/src/idiomatic/leveraging-the-type-system/typestate-pattern.md index 0e20d9127cc5..b87bb76d7953 100644 --- a/src/idiomatic/leveraging-the-type-system/typestate-pattern.md +++ b/src/idiomatic/leveraging-the-type-system/typestate-pattern.md @@ -1,5 +1,5 @@ --- -minutes: 30 +minutes: 65 --- ## Typestate Pattern: Problem diff --git a/src/idiomatic/welcome.md b/src/idiomatic/welcome.md index 889ba721ab2d..efe7499269bf 100644 --- a/src/idiomatic/welcome.md +++ b/src/idiomatic/welcome.md @@ -32,6 +32,13 @@ decisions within the context and constraints of your own projects. The course will cover the topics listed below. Each topic may be covered in one or more slides, depending on its complexity and relevance. +## Target Audience + +Engineers with at least 2-3 years of coding experience in C, C++11 or newer, +Java 7 or newer, Python 2 or 3, Go or any other similar imperative programming +language. We have no expectation of experience with more modern or feature-rich +languages like Swift, Kotlin, C#, or TypeScript. + ### Foundations of API design - Golden rule: prioritize clarity and readability at the callsite. People will