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
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions src/SUMMARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -437,6 +437,10 @@
- [Semantic Confusion](idiomatic/leveraging-the-type-system/newtype-pattern/semantic-confusion.md)
- [Parse, Don't Validate](idiomatic/leveraging-the-type-system/newtype-pattern/parse-don-t-validate.md)
- [Is It Encapsulated?](idiomatic/leveraging-the-type-system/newtype-pattern/is-it-encapsulated.md)
- [RAII](idiomatic/leveraging-the-type-system/raii.md)
- [Drop Limitations](idiomatic/leveraging-the-type-system/raii/drop_limitations.md)
- [Drop Bomb](idiomatic/leveraging-the-type-system/raii/drop_bomb.md)
- [Scope Guards](idiomatic/leveraging-the-type-system/raii/scope_guards.md)

---

Expand Down
116 changes: 116 additions & 0 deletions src/idiomatic/leveraging-the-type-system/raii.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
---
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 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.

extend this pattern to anything else.

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


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

```

<details>

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


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

[`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).
Comment on lines +83 to +84
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.


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


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

Choose a reason for hiding this comment

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

C++ and Java also have RAII. C++ has always had it, Java - since Java 7 (try-with_resources, released in 2011)

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, that's also where many of us learned about these concepts. Did not mean to imply that. It was mostly about the lock. Your points are well taken though, gonna revisit this with care.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Yeah, sorry, I was a bit terse in my comment. Even for locks though, high-quality APIs for locks in C++ and Java today are RAII-based (std::lock_guard, absl::MutexLock, synchronized(obj) {} in Java).

`lock/unlock` pattern, Rust ensures the lock _cannot_ be forgotten, thanks to
the compiler.
Copy link
Collaborator

Choose a reason for hiding this comment

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

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.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Great suggestion. Will incorporate this feedback.


- 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

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>
Comment on lines +108 to +116
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.

91 changes: 91 additions & 0 deletions src/idiomatic/leveraging-the-type-system/raii/drop_bomb.md
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.

Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
# 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.


```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 roll back!");
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
panic!("Transaction dropped without commit or roll back!");
panic!("Transaction dropped without commit or rollback!");

"rollback" is one word when used as a noun.

}
}
}
```

<details>

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

Choose a reason for hiding this comment

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

I have a hard time understanding this example: if we're implementing a drop that panics if the memory wasn't deallocated, we can tell if the memory was or wasn't deallocated, and we know how to do it properly. So why not have that same drop implementation deallocate the memory instead?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Because it might not have access to the required Token types or other kinds of facilitators required to do so. In most cases it is however possible, as you point out. I had a hard time in deciding whether or not to keep it in, I think I'll leave it out and give room for something else.

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.

Oh I see. Indeed a C API with complex memory management could require something like that.

I think we can raise the abstraction level here a bit - FFI is not essential to this example. You're talking about a case where we simply can't implement drop because we don't have the necessary information, and for some reason we don't want to carry that information in the object being dropped (e.g., for memory efficiency, avoiding circular references or whatever).

Here's an example that might feel a bit forced, but without FFI or unsafe.

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.

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

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


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

Choose a reason for hiding this comment

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

WDYT about demonstrating that in the example above?

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.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Great suggestion. I think I'll go for the latter approach to keep the examples short to read

Comment on lines +49 to +51
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
- 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.
- APIs that are expected to panic like this should document
the cases when a panic will occur under a `Panics` section.

Minor rewording to be more concise.

As a general note, more concise wording in the speaker notes is easier to deal with when teaching, at least in my experience. When I'm teaching I'm quickly scanning the speaker notes to remind myself of things I need to mention to students, so keeping them less wordy is helpful.


Ideally, drop bombs should be used only in internal APIs to catch bugs early,
without placing implicit runtime obligations on library users.
Comment on lines +53 to +54
Copy link
Collaborator

Choose a reason for hiding this comment

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

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.
Comment on lines +56 to +59
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
- 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.
- Drop bombs can be debug-only. If there's a reasonable
default way to cleanup the object, then it may be helpful
to only panic in debug builds but perform the default
cleanup in release builds. In these cases it would also be
useful to log a warning noting that the object wasn't
handled properly.

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.


- Advanced use cases might also rely on the following patterns:

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


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


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

## More to explore

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.
Comment on lines +83 to +86
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 you can remove this paragraph, I think it just repeats things that are already said in the rest of the slide.


There is an open RFC issue and discussion about linear types in Rust:
<https://github.com/rust-lang/rfcs/issues/814>.
Comment on lines +88 to +89
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
There is an open RFC issue and discussion about linear types in Rust:
<https://github.com/rust-lang/rfcs/issues/814>.

I don't think it's helpful to link to that RFC, it's over a decade old and is effectively dead. The comments link to a blog post discussing why linear types can't really be added to Rust.


</details>
84 changes: 84 additions & 0 deletions src/idiomatic/leveraging-the-type-system/raii/drop_limitations.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
# 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
```

<details>

- In the earlier example, our `File` resource owns a file handle provided by the
Copy link
Collaborator

Choose a reason for hiding this comment

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

"earlier"? did you forget to git add a file? (I mean, yes, if we add a file example that I'm suggesting above there would be an earlier File resource example, but right now there isn't, so I'm asking if you accidentally omitted a part of the PR.)

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Earlier as in "above". My language is clearly not yet precise enough. Will fine tune it. And yes with your new suggested flow it will look all different as well.

operating system.

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

Choose a reason for hiding this comment

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

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

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Yeah I meant it more in context of Rust and its choices. Feedback understood, will rewrite this.


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

Choose a reason for hiding this comment

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

and remaining resources will not be cleaned up.

... but does it matter, if the process is being terminated anyway? I think emphasizing the risk of program termination is a better angle here. After all, people are trying to handle the error, not simply terminating the program in an obscure way.

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 external resources (outside of the program)


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

Choose a reason for hiding this comment

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

Why do you call it non-deterministic?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Wrong choice of wording

dropped. And in fact as discussed in previous slide it might never even run at
Copy link
Collaborator

Choose a reason for hiding this comment

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

WDYM by "you cannot control"? You're describing std::mem::drop() in the previous section.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

True. But most of the times you do not call that function and in that case you do not control it. But of course you "can" control it, so my language is not entirely correct here. Thank you for catching all of this. I am very grateful for it.

all, leaving the external resource in an undefined state.

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

termination of the program.

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

</details>
Loading
Loading