Skip to content

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

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open

RAII chapter for idiomatic rust #2820

wants to merge 3 commits into from

Conversation

GlenDC
Copy link
Collaborator

@GlenDC GlenDC commented Jul 15, 2025

This PR adds the RAII chapter for the idiomatic Rust deep dive.

Copy link

google-cla bot commented Jul 15, 2025

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
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
Rust applies RAII automatically for memory management. The `Drop` trait lets you
Rust uses RAII for managing heap memory. The `Drop` trait lets you

Copy link
Collaborator Author

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]);
Copy link
Collaborator

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.

Comment on lines +32 to +36
`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).
Copy link
Collaborator

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
}
Copy link
Collaborator

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).

Copy link
Collaborator Author

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?

Copy link
Collaborator

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.
Copy link
Collaborator

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.

Copy link
Collaborator Author

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.

Copy link
Collaborator

@gribozavr gribozavr Jul 16, 2025

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
Copy link
Collaborator

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).

Copy link
Collaborator Author

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.
Copy link
Collaborator

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.

Copy link
Collaborator Author

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.

Copy link
Collaborator

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
Copy link
Collaborator

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
Copy link
Collaborator

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 and cleanup go together,

  • to ensure that file gets deleted even if we have a problem in writeln!().unwrap().

Copy link
Collaborator

@randomPoison randomPoison left a 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!

Comment on lines +83 to +84
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).
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
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.

Comment on lines +66 to +93
- 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.
Copy link
Collaborator

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).

Comment on lines +108 to +116
## 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>
Copy link
Collaborator

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.

Copy link
Collaborator

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 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.

Comment on lines +45 to +47
- 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.
Copy link
Collaborator

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.

Comment on lines +118 to +122
- 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.
Copy link
Collaborator

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.

Comment on lines +67 to +69
- [`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.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
- [`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.

Comment on lines +63 to +65
- [`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.
Copy link
Collaborator

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.

Comment on lines +25 to +29
let cleanup = guard(path, |path| {
// Errors must be handled inside the guard,
// but cannot be propagated.
let _ = fs::remove_file(path);
});
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
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.

Comment on lines +124 to +132
- 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.
Copy link
Collaborator

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
Copy link
Collaborator

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
Copy link
Collaborator

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
Copy link
Collaborator

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.
Copy link
Collaborator

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().

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants