diff --git a/src/SUMMARY.md b/src/SUMMARY.md index 1950476a423a..04267a6c8dee 100644 --- a/src/SUMMARY.md +++ b/src/SUMMARY.md @@ -437,6 +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) + - [Mutex](idiomatic/leveraging-the-type-system/raii/mutex.md) + - [Drop Bomb](idiomatic/leveraging-the-type-system/raii/drop_bomb.md) + - [Scope Guard](idiomatic/leveraging-the-type-system/raii/scope_guard.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..00698815ef7f --- /dev/null +++ b/src/idiomatic/leveraging-the-type-system/raii.md @@ -0,0 +1,113 @@ +--- +minutes: 30 +--- + +# RAII: `Drop` trait + +RAII (**R**esource **A**cquisition **I**s **I**nitialization) ties the lifetime +of a resource to the lifetime of a value. + +[Rust uses RAII to manage memory](https://doc.rust-lang.org/rust-by-example/scope/raii.html), +and the `Drop` trait allows you to extend this to other resources, such as file +descriptors or locks. + +```rust,editable +pub struct File(std::os::fd::RawFd); + +impl File { + pub fn open(path: &str) -> Result { + // [...] + 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(()) +} +``` + +
+ +- This example shows how easy it is to forget releasing a file descriptor when + managing it manually. The code as written does not call `file.close()`. Did + anyone in the class notice? + +- To release the file descriptor correctly, `file.close()` must be called after + the last use — and also in early-return paths in case of errors. + +- Instead of relying on the user to call `close()`, we can implement the `Drop` + trait to release the resource automatically. This ties cleanup to the lifetime + of the `File` value. + + ```rust,compile_fail + impl Drop for File { + fn drop(&mut self) { + println!("release file descriptor automatically"); + } + } + ``` + +- Note that `Drop::drop` cannot return errors. Any fallible logic must be + handled internally or ignored. In the standard library, errors returned while + closing an owned file descriptor during `Drop` are silently discarded: + + +- If both `drop()` and `close()` exist, the file descriptor may be released + twice. To avoid this, remove `close()` and rely solely on `Drop`. + +- When is `Drop::drop` called? + + Normally, when the `file` variable in `main` goes out of scope (either on + return or due to a panic), `drop()` is called automatically. + + If the file is moved into another function, for example `read_all()`, the + value is dropped when that function returns — not in `main`. + + In contrast, C++ runs destructors in the original scope even for moved-from + values. + +- The same mechanism powers `std::mem::drop`: + + ```rust + pub fn drop(_x: T) {} + ``` + + You can use it to force early destruction of a value before its natural end of + scope. + +- Insert `panic!("oops")` at the start of `read_to_end()` to show that `drop()` + still runs during unwinding. Rust guarantees this unless the panic strategy is + set to `abort`. + +- There are cases where destructors will not run: + - If a destructor itself panics during unwinding, the program aborts + immediately. + - If the program exits with `std::process::exit()` or is compiled with the + `abort` panic strategy, destructors are skipped. + +### 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..4e20a7818954 --- /dev/null +++ b/src/idiomatic/leveraging-the-type-system/raii/drop_bomb.md @@ -0,0 +1,123 @@ +# 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 { + /// Begin a [`Transaction`]. + /// + /// ## Panics + /// + /// Panics if the transaction is dropped without + /// calling [`Self::commit`] or [`Self::rollback`]. + fn start() -> Self { + Self { active: true } + } + + fn commit(mut self) -> io::Result<()> { + writeln!(io::stdout(), "COMMIT")?; + self.active = false; + Ok(()) + } + + fn rollback(mut self) -> io::Result<()> { + writeln!(io::stdout(), "ROLLBACK")?; + self.active = false; + Ok(()) + } +} + +impl Drop for Transaction { + fn drop(&mut self) { + if self.active { + panic!("Transaction dropped without commit or rollback!"); + } + } +} + +fn main() -> io::Result<()> { + let tx = Transaction::start(); + + if some_condition() { + tx.commit()?; + } else { + tx.rollback()?; + } + + // Uncomment to see the panic: + // let tx2 = Transaction::start(); + + Ok(()) +} + +fn some_condition() -> bool { + // [...] + true +} +``` + +
+ +- This pattern ensures that a value like `Transaction` cannot be silently + dropped in an unfinished state. The destructor panics if neither `commit()` + nor `rollback()` has been called. + +- 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 a value can be safely cleaned up in `Drop`, consider falling back to that + behavior in Release mode and panicking only in Debug. This decision should be + made based on the guarantees your API provides. + +- Panicking in Release builds is a valid choice if silent misuse could lead to + serious correctness issues or security concerns. + +## Additional Patterns + +- [`Option` with `.take()`](https://doc.rust-lang.org/std/option/enum.Option.html#method.take): + A common pattern inside `Drop` to move out internal values and prevent double + drops. + + ```rust,compile_fail + impl Drop for MyResource { + fn drop(&mut self) { + if let Some(handle) = self.handle.take() { + // do cleanup with handle + } + } + } + ``` + +- [`ManuallyDrop`](https://doc.rust-lang.org/std/mem/struct.ManuallyDrop.html): + Prevents automatic destruction and gives full manual control. Requires + `unsafe`, so only use when strictly necessary. + +- [`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. + + See a working example in + [the Rust playground](https://play.rust-lang.org/?version=stable&mode=debug&edition=2024&gist=3223f5fa5e821cd32461c3af7162cd55). + +
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..1f7838f68e15 --- /dev/null +++ b/src/idiomatic/leveraging-the-type-system/raii/mutex.md @@ -0,0 +1,131 @@ +# Mutex + +In earlier examples, RAII was used to manage concrete resources like file +descriptors. With a `Mutex`, the resource is more abstract: exclusive access to +a value. + +Rust models this using a `MutexGuard`, which ties access to a critical section +to the lifetime of a value on the stack. + +```rust +#[derive(Debug)] +struct Mutex { + value: std::cell::UnsafeCell, + // [...] +} + +#[derive(Debug)] +struct MutexGuard<'a, T> { + value: &'a mut T, + // [...] +} + +impl Mutex { + fn new(value: T) -> Self { + Self { + value: std::cell::UnsafeCell::new(value), + // [...] + } + } + + fn lock(&self) -> MutexGuard { + // [...] + let value = unsafe { &mut *self.value.get() }; + MutexGuard { value } + } +} + +impl<'a, T> std::ops::Deref for MutexGuard<'a, T> { + type Target = T; + fn deref(&self) -> &T { + self.value + } +} + +impl<'a, T> std::ops::DerefMut for MutexGuard<'a, T> { + fn deref_mut(&mut self) -> &mut T { + self.value + } +} + +impl<'a, T> Drop for MutexGuard<'a, T> { + fn drop(&mut self) { + // [...] + println!("drop MutexGuard"); + } +} + +fn main() { + let m = Mutex::new(vec![1, 2, 3]); + + let mut guard = m.lock(); + guard.push(4); + guard.push(5); + println!("{guard:?}"); +} +``` + +
+ +- A `Mutex` controls exclusive access to a value. Unlike earlier RAII examples, + the resource here is not external but logical: the right to mutate shared + data. + +- This right is represented by a `MutexGuard`. Only one can exist at a time. + While it lives, it provides `&mut T` access — enforced using `UnsafeCell`. + +- Although `lock()` takes `&self`, it returns a `MutexGuard` with mutable + access. This is possible through interior mutability: a common pattern for + safe shared-state mutation. + +- `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`. There is no need to call a separate unlock + function — this is RAII in action. + +## Poisoning + +- If a thread panics while holding the lock, the value may be in a corrupt + state. + +- To signal this, the standard library uses poisoning. When `Drop` runs during a + panic, the mutex marks itself as poisoned. + +- On the next `lock()`, this shows up as an error. The caller must decide + whether to proceed or handle the error differently. + +- See this example showing the standard library API with poisoning: + + +### Mutex Lock Lifecycle + +```bob ++---------------+ +----------------------+ +| Mutex | lock | MutexGuard | +| Unlocked | ------> | Exclusive Access | ++---------------+ +----------------------+ + + ^ | drop + | no | + +---------------+ | + | | + | V + ++---------------+ yes +-------------------+ +| Mutex | <---- | Thread panicking? | +| Poisoned | +-------------------+ ++---------------+ + + | + | lock + | + v + ++------------------+ +| Err ( Poisoned ) | ++------------------+ +``` + +
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..94fd32f4981f --- /dev/null +++ b/src/idiomatic/leveraging-the-type-system/raii/scope_guard.md @@ -0,0 +1,61 @@ +# Scope Guards + +A scope guard uses the `Drop` trait to ensure cleanup code runs automatically +when a scope exits — even if due to an error. + +```rust,editable,compile_fail +use scopeguard::{ScopeGuard, guard}; +use std::{ + fs::{self, File}, + 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 complete!"); + } + // Otherwise, the guard runs and deletes the file +} +``` + +
+ +- This example simulates an HTTP download. 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 defused with + `ScopeGuard::into_inner`. In the success path, we defuse it to preserve 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/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