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 7 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
1 change: 1 addition & 0 deletions src/SUMMARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -437,6 +437,7 @@
- [Semantic Confusion](idiomatic/leveraging-the-type-system/newtype-pattern/semantic-confusion.md)
- [Parse, Don't Validate](idiomatic/leveraging-the-type-system/newtype-pattern/parse-don-t-validate.md)
- [Is It Encapsulated?](idiomatic/leveraging-the-type-system/newtype-pattern/is-it-encapsulated.md)
- [RAII](idiomatic/leveraging-the-type-system/raii.md)

---

Expand Down
166 changes: 166 additions & 0 deletions src/idiomatic/leveraging-the-type-system/_old/raii.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
---
minutes: 30
---

# RAII and `Drop` in Practice

RAII (_Resource Acquisition Is Initialization_) means tying the lifetime of a
resource to the lifetime of a value.

Rust uses RAII for managing heap memory. The `Drop` trait lets you extend this
pattern to anything else.

```rust
use std::sync::Mutex;

fn main() {
let mutex = Mutex::new(vec![1, 2, 3]);

{
let mut data = mutex.lock().unwrap();
data.push(4); // lock held here
} // lock automatically released here
}
```

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

TODO: consider devoting 1-2 slides to demonstrate the relevant snippets of
Mutex and MutexGuard API

- You may recall from
[the Memory Management segment](../../memory-management/drop.md) that the
[`Drop` trait](https://doc.rust-lang.org/std/ops/trait.Drop.html) lets you
define what should happen when a resource is dropped.

- In
[the Blocks and Scopes chapter](../../control-flow-basics/blocks-and-scopes.md),
we saw the most common situation where a resource is dropped: when the scope
of its _owner_ ends at the boundary of a block (`{}`).

- The use of
[`std::mem::drop(val)`](https://doc.rust-lang.org/std/mem/fn.drop.html)
allows you to _move_ a value out of scope before the block ends.

- There are also other scenarios where this can happen, such as when the value
owning the resource is "shadowed" by another value:

```rust
let a = String::from("foo");
let a = 3; // ^ The previous string is dropped here
// because we shadow its binding with a new value.
```

- Recall also from [the Drop chapter](../../memory-management/drop.md) that
for a composite type such as a `struct`, all its fields will be dropped when
the struct itself is dropped. If a field implements the `Drop` trait, its
`Drop::drop` _trait_ method will also be invoked.

- In any scenario where the stack unwinds the value, it is guaranteed that the
[`Drop::drop`](https://doc.rust-lang.org/std/ops/trait.Drop.html#tymethod.drop)
method of a value `a` will be called.

- This holds true for happy paths such as:

- Exiting a block or function scope.

- Returning early with an explicit `return` statement, or implicitly by
using [the Try operator (`?`)](../../error-handling/try.md) to
early-return `Option` or `Result` values.

- It also holds for unexpected scenarios where a `panic` is triggered, if:

- The stack is unwound on panic (which is the default), allowing for
graceful cleanup of resources.

(TODO: we might want to refactor this to make clear this also happens in
normal function returns)

This unwind behavior can be overridden to instead
[abort on panic](https://github.com/rust-lang/rust/blob/master/library/panic_abort/src/lib.rs),
in which case no destructors will run.

- No panic occurs within any of the `drop` methods invoked before reaching
the `drop` call of the object `a`.

- Note that
[an explicit exit of the program](https://doc.rust-lang.org/std/process/fn.exit.html),
as sometimes used in CLI tools, terminates the process immediately. In other
words, the stack is not unwound in this case, and the `drop` method will not
be called.

TODO: apply feedback:

```
I think this whole point can be pulled out into its own slide.
Talking about when Drop runs and when it doesn't is worth covering
directly. I think you'd also want to talk about forget on that slide,
and maybe briefly note that leaking destructors is not unsafe
(unless you plan to cover them elsewhere).
```

- `Drop` is a great fit for use cases like `Mutex`.

When the guard goes out of scope,
[`Drop::drop`](https://doc.rust-lang.org/std/ops/trait.Drop.html#tymethod.drop)
is called and unlocks the mutex automatically.

In contrast to C++ or Java, where you often have to unlock manually or use a
`lock/unlock` pattern, Rust ensures the lock _cannot_ be forgotten, thanks to
the compiler.

TODO: revisit references to C++ and Java, be careful in wording. E.g. C++ and
Java their mutexes are also RAII based
([std::lock_guard](https://en.cppreference.com/w/cpp/thread/lock_guard.html),
[absl::MutexLock](https://github.com/abseil/abseil-cpp/blob/master/absl/synchronization/mutex.h#L583),
`synchronized(obj) {}` in Java).

TODO: incorporate @gribozavr's feedback here:

```
It can't be forgotten, but the MutexGuard can be forgot()'en intentionally, or leaked - like any other value.

It is a good tie-in to discuss use cases for drop: it is good for cleaning up things within the scope of a process, but not the right tool for guaranteeing that something happens outside of the process (e.g., on local disk, or in another service in a distributed system).

For example, it is a bad idea to rely exclusively on drop to clean up temporary files: if the program terminates in a way that skips running drop, temporary files will persist, and eventually the computer will run out of space. This can happen if the program crashes or leaks the value whose drop is responsible for deleting the file. In addition to a drop implementation within the program, one also needs a classic unix-style temp file reaper that runs as a separate process.
```

- In other scenarios, the `Drop` trait shows its limitations. Next, we'll look
at what those are and how we can address them.

TODO: apply feedback from @gribozavr when refactoring the RAII content:

```
First, a custom File type that wraps a file descriptor. A file descriptor is a classic OS-level resource. We could show how to implement a simple read-only file type a with a minimal API: open() and read() to read a single byte. Then show how to implement Drop. Discuss when the drop() function runs, and how it isn't run when values are moved (contrast with C++ where the destructor always runs at the end of the scope, even for moved-from values). Show the forget() function, discuss its signature and what it means.

In other words, use this simple File type as an opportunity to do a 5-minute refresher on drop and move semantics. I see you're already doing it with instructor notes like "for a composite type such as a struct, all its fields will be dropped" and by mentioning the std::mem::drop() function. Let's lean more into it and make sure that during this discussion we have an example of a drop implementation on the screen.

Then we move on to Mutex. There we would focus on explaining the idea that for a mutex the "resource" is more abstract. In case of a mutex, the resource is exclusive access to the wrapped value. Thus, we need a second type - a MutexGuard - to represent that.

The mutex example is perfect to facilitate the drop x panic discussion. Maybe draft an extra slide that shows what happens by default with a naive drop implementation (the drop simply runs, no special code is needed for that), and then discuss why panics poison the mutex in Rust (there is a good chance that the code was mutating the shared data, so its invariants might be broken).
```

Also apply feedback from @djimitche:

```
It's probably only necessary to include one "callback" to Fundamentals --
the important point is to that this slide is a quick review of previous
content, and if students need a deeper refresher they can find that
content in the Fundamentals course.

That said, these speaker notes are pretty long! Is it possible to trim
this down to just call out the bits necessary for the RAII patterns
introduced here, leaving the rest to the students' memory of Fundamentals?
```

</details>
159 changes: 159 additions & 0 deletions src/idiomatic/leveraging-the-type-system/_old/raii/drop_bomb.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
# Drop Bombs: Enforcing API Correctness

Use `Drop` to enforce invariants and detect incorrect API usage. A "drop bomb"
panics if not defused.

---
TODO: apply feedback

I think this slide should also mention that drop bombs are useful in cases
where the finalizing operation (e.g. commit and rollback in the slide's
example) needs to return some kind of output, which means it can't be
handled normally in Drop (realistically, commit and rollback would want to
return Results to indicate if they succeeded or not). This is one of the
limitations of Drop mentioned on the previous slide, so it'd be worth
noting that this pattern is a common way to work around that limitation.
---

```rust
struct Transaction {
active: bool,
}

impl Transaction {
fn start() -> Self {
Self { active: true }
}

fn commit(mut self) {
self.active = false;
// Dropped after this point, no panic
}

fn rollback(mut self) {
self.active = false;
// Dropped after this point, no panic
}
}

impl Drop for Transaction {
fn drop(&mut self) {
if self.active {
panic!("Transaction dropped without commit or rollback!");
}
}
}
```

<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 foreign
language is cleaned up through an explicit call to a safe API function.

TODO: this is a bit far-fetched, better examples suggested by reviewers:

```
Let's imagine what an implementation of an SSH server could look like. An SshServer object
that manages all connections, and an SshConnection object that represents a single connected user.
The connection needs to be unregistered from the server object before it can be dropped -
or else we would be leaking resources like socket descriptors.

Say, for efficiency the connection does not have a pointer back to the server -
so the connection can't deregister itself within its drop.
Therefore, SshConnection::drop should panic to catch coding bugs.
Instead, we would provide an API like SshServer::deregister(&mut self, conn: SshConnection)
that consumes the connection, deregisters and correctly destroys it.

-- @randomPoison agrees, this can be dropped:

I don't think we need to mention FFI at all here, I think the decision
of whether or not to use a drop bomb has more to do with whether or not
there's a reasonable way to cleanup in Drop or if the finalizing
operation needs to return anything (see also my comment on the file).
That situation may come up in FFI, but it's not the FFI specifically
that makes a drop bomb necessary.
```

more feedback:

```
It's worth noting that a very common reason you can't drop the thing is that
the deallocation method is async and there's no async drop.
Database transactions are an excellent example of this, in fact --
it's typically not possible to just rollback a transaction in drop().
```

- APIs that are expected to panic like this should document the cases when a
panic will occur under a `Panics` section.

(^ TODO: this was reworded to be more minimal. Shorter the speaker notes the
better, to make it easier to skim through as instructor) Original:
> Similar to unsafe code, it is recommended that APIs with expectations like
> these are clearly documented under a Panic section. This helps ensure that
> users of the API are aware of the consequences of misuse.

TODO: apply feedback:

```
Either edit the example directly (for it to appear with the comment from the beginning),
or add a suggested comment here for the instructor to type or paste.
```

TODO: Also more feedback:

```
Why should this only be used in internal APIs? For example, a library
providing a transactional API (like in the example on this slide) would
want to use this pattern in its public API to help users catch cases
where they forget to finish the transaction.
```

- If there is a way to restore the system to a valid state using a fallback in
the Drop implementation, it is advisable to restrict the use of drop bombs to
Debug mode. In Release mode, the Drop implementation could fall back to safe
cleanup logic while still logging the incident as an error.

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

- [`Option<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.

TODO: apply feedback

```
I think we should provide an example of this.
I find that it's a common pattern when doing complex logic in Drop.
This might even be worth pulling out into its own slide.
```

- [`ManuallyDrop`](https://doc.rust-lang.org/std/mem/struct.ManuallyDrop.html):
A zero-cost wrapper that disables the automatic drop behavior of a value,
making manual cleanup required and explicit. This requires unsafe code to
use, though, so it's recommended to only use this if strictly necessary.

- The [`drop_bomb` crate](https://docs.rs/drop_bomb/latest/drop_bomb/) provides
a way to enforce that certain values are not dropped unless explicitly
defused. It can be added to an existing struct and exposes a `.defuse()`
method to make dropping safe. The crate also includes a `DebugDropBomb`
variant for use in debug-only builds.

TODO: apply feedback:

```
I don't love the wording "it is advisable to restrict the use of drop
bombs to Debug mode". I think the decision of whether to panic in
release builds is heavily dependent on the specifics of the API in
question, and panicking in release builds is an entirely valid decision
imo.
```

</details>
Loading
Loading