diff --git a/src/SUMMARY.md b/src/SUMMARY.md index 1950476a423a..5450b7e687e4 100644 --- a/src/SUMMARY.md +++ b/src/SUMMARY.md @@ -437,6 +437,7 @@ - [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) --- 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/_old/raii/drop_bomb.md b/src/idiomatic/leveraging-the-type-system/_old/raii/drop_bomb.md new file mode 100644 index 000000000000..bed30a6eb5bf --- /dev/null +++ b/src/idiomatic/leveraging-the-type-system/_old/raii/drop_bomb.md @@ -0,0 +1,159 @@ +# Drop Bombs: Enforcing API Correctness + +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, +} + +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 rollback!"); + } + } +} +``` + +
+ +- 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 foreign + language is cleaned up through an explicit call to a safe API function. + + 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 + 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. + + 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 + 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. + + TODO: apply feedback: + + ``` + 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/_old/raii/drop_limitations.md b/src/idiomatic/leveraging-the-type-system/_old/raii/drop_limitations.md new file mode 100644 index 000000000000..12098a2ba2b0 --- /dev/null +++ b/src/idiomatic/leveraging-the-type-system/_old/raii/drop_limitations.md @@ -0,0 +1,126 @@ +# 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. (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): + + > 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. + + 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 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. + 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 + 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. + + (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. + + 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. + + 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. + + 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/_old/raii/scope_guards.md b/src/idiomatic/leveraging-the-type-system/_old/raii/scope_guards.md new file mode 100644 index 000000000000..273b8ac9d638 --- /dev/null +++ b/src/idiomatic/leveraging-the-type-system/_old/raii/scope_guards.md @@ -0,0 +1,190 @@ +# Scope Guards + +A scope guard makes use of the `Drop` trait to run a given closure when it goes +out of scope. + +```rust,editable,compile_fail +use scopeguard::{ScopeGuard, guard}; +use std::{ + fs::{self, File}, + io::Write, +}; + +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(); + + /* + * 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 + 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,editable,compile_fail + 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() + } + } + ``` + + 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) + 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. + + 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: + 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. + + 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/leveraging-the-type-system/raii.md b/src/idiomatic/leveraging-the-type-system/raii.md new file mode 100644 index 000000000000..60a8455d6e0d --- /dev/null +++ b/src/idiomatic/leveraging-the-type-system/raii.md @@ -0,0 +1,123 @@ +--- +minutes: 30 +--- + +# RAII: `Drop` trait + +RAII (Resource Acquisition Is Initialization) means tying 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 +struct FileLock; +pub struct File { + stub: Option, + lock: FileLock, +} +#[derive(Debug)] +pub struct Error; + +impl File { + pub fn open(path: &str) -> Result { + println!("acquire file descriptor: {path}"); + Ok(Self { stub: Some(1), lock: FileLock }) + } + + pub fn read(&mut self) -> Result { + self.stub.take().ok_or(Error) + } + + pub fn close(self) -> Result<(), Error> { + self.lock.release() + } +} + +impl FileLock { + fn release(self) -> Result<(), Error> { + println!("release file descriptor"); + Ok(()) + } +} + +fn main() { + let mut file = File::open("example.txt").unwrap(); + + let mut content = Vec::new(); + while let Ok(byte) = file.read() { + content.push(byte); + } + + println!("content: {content:?}"); +} +``` + +
+ +- 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 reject this: once `file` is moved, it can no + longer be accessed. The borrow checker enforces this statically. + +- 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 { + fn drop(&mut self) { + println!("release file descriptor automatically"); + } + } + ``` + +- If both `drop()` and `close()` are present, the file descriptor is released + twice. To avoid this, remove `close()` and rely solely on `Drop`. + + 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. + +- 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 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`. + +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: + +- Available on nightly: + + +
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