-
Notifications
You must be signed in to change notification settings - Fork 1.8k
RAII chapter for idiomatic rust #2820
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
Thanks for your pull request! It looks like this may be your first contribution to a Google open source project. Before we can look at your pull request, you'll need to sign a Contributor License Agreement (CLA). View this failed invocation of the CLA check for more information. For the most up to date status, view the checks section at the bottom of the pull request. |
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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Rust applies RAII automatically for memory management. The `Drop` trait lets you | |
Rust uses RAII for managing heap memory. The `Drop` trait lets you |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you. I admire your skill to find the most precise language a lot. It's clear I'll learn a lot from you on this project.
use std::sync::Mutex; | ||
|
||
fn main() { | ||
let mux = Mutex::new(vec![1, 2, 3]); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
"mutex" for the variable name? I don't find abbreviations like "mux" idiomatic.
`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). |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What's the expectation for the instructor - just talk through this or open the docs browser and show the APIs? I think we could use 1-2 slides that demonstrate just the relevant snippets of the Mutex and MutexGuard API.
let mut data = mux.lock().unwrap(); | ||
data.push(4); // lock held here | ||
} // lock automatically released here | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I feel like Mutex might be too complex for a first contact with RAII.
Furthermore, don't assume the audience has mastered Drop as a part of the foundations class, it is more of a whirlwind tour. It also could have been weeks since they took the foundations class.
Here's my suggestion. How about we do two examples.
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).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I like this flow a lot. Seems I can do more slides than I thought. Would you still keep the slide about scopeguard at the end?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, absolutely, it should be a part of the explanation of how Mutex uses RAII.
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. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't understand this point. One would apply a timeout in the same way as usual. Where we have a limitation is that drop()
can't communicate with the caller: it can neither receive an argument (e.g., a timeout value) or communicate back that the timeout happened (the whole discussion about failures in drop). But certainly drop()
is free to call a low-level I/O function with a timeout value - hence I'm confused about what this paragraph is trying to communicate.
The only limitation of drop I can think of that can make I/O difficult is that drop is not async. There's the unstable AsyncDrop for that.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I meant that as a user of the API you have no control over it.
As in:
- when you can
Foo::close
you can wrap around that logic however you want. Not just error handling, but also impose a timeout, retries, etc... - that is not something you can do "as the user of the API" for something that happens invisible in the background by the creator of the API in Drop.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Note that the chapter does not discuss poisoned mutexes at the moment (I'm requesting that to be added in my comments above).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes I read that and I agree. This will be reworked with that in mind.
# Drop Bombs: Enforcing API Correctness | ||
|
||
Use `Drop` to enforce invariants and detect incorrect API usage. A "drop bomb" | ||
panics if not defused. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For the members of the audience familiar with C++, consider mentioning that in C++ a private destructor is used to achieve a similar effect. So if your C++ API design intuition tells you to make the destructor not accessible to the user, use this pattern in Rust.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sure thing, C++ was my first primary language for many years. Wasn't always certain exactly about what languages I should refer and which not.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
From an internal (sorry!) design doc for this class:
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.
let path = "temp.txt"; | ||
let mut file = File::create(path).expect("cannot create file"); | ||
|
||
// Write something to the file |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
// Write something to the file | ||
writeln!(file, "temporary data").unwrap(); | ||
|
||
// Create a scope guard to clean up the file unless we defuse it |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this should be put right after let mut file
, in order to:
-
emphasize that
file
andcleanup
go together, -
to ensure that
file
gets deleted even if we have a problem inwriteln!().unwrap()
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks pretty good, there's lots of good stuff in this chapter 😁 I've got various feedback below, but it's definitely off to a good start!
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). |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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). | |
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. |
I think it'd be good to call out that Drop
won't run on abort.
- 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. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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).
## 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`. | ||
|
||
</details> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This seems unnecessary. I don't think we need to redirect students to a book about concurrency, that's pretty tangential to RAII and we're already talking about Mutex
in the slides.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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 Result
s 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.
- 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. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This seems like a weird example to me. I've worked with C libraries that have custom allocation and deallocation functions, and in those cases the straightforward solution was to create a custom Box
-like smart pointer and do the deallocation in Drop
.
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.
- 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. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think this needs to be mentioned. Unless we're diving into scopeguard
's sources and discussing how and why to use ManuallyDrop
, I don't think this is worth diving into.
- [`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. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
- [`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. | |
- [`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. |
I think mentioning ManuallyDrop
is fine since it's relevant, but we should be clear to students that it's not the first tool to reach for.
- [`Option<T>` 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. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
let cleanup = guard(path, |path| { | ||
// Errors must be handled inside the guard, | ||
// but cannot be propagated. | ||
let _ = fs::remove_file(path); | ||
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
let cleanup = guard(path, |path| { | |
// Errors must be handled inside the guard, | |
// but cannot be propagated. | |
let _ = fs::remove_file(path); | |
}); | |
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); | |
}); |
I think adding a println!
here would make this easier to demonstrate in class. Right now it's hard to observe when the cleanup happens because the only observable effect is removing the file, which we can't really show directly in class. With the println!
we can more directly see when the cleanup does and doesn't happen.
- 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. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
[`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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is probably better described as a "segment" rather than a "chapter". However, these interlinks have been hard to keep updated in the past -- it may be better to just say "from the Fundamentals course".
[`Drop` trait](https://doc.rust-lang.org/std/ops/trait.Drop.html) lets you | ||
define what should happen when a resource is dropped. | ||
|
||
- In |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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?
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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The stack doesn't unwind things -- the stack is unwound in handling a panic or normal function return.
|
||
- 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. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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()
.
This PR adds the RAII chapter for the idiomatic Rust deep dive.