diff --git a/src/idiomatic/leveraging-the-type-system/raii.md b/src/idiomatic/leveraging-the-type-system/raii.md index 61e7c36f..f8a20293 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. -The book demonstrates, among other topics, how `Drop` and RAII work together in -constructs like `Mutex`. +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/raii/drop_bomb.md index b6ee94cb..8c6d17a6 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. + TODO: this is a bit far-fetched, better examples suggested by reviewers: - Ideally, drop bombs should be used only in internal APIs to catch bugs early, - without placing implicit runtime obligations on library users. + ``` + 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 + TODO: apply feedback: -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: -. + ``` + 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 ef021356..8747ff25 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 93e2c2c4..273b8ac9 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 889ba721..de4184a8 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