You've already forked comprehensive-rust
mirror of
https://github.com/google/comprehensive-rust.git
synced 2025-12-18 13:40:22 +02:00
RAII chapter for idiomatic rust (#2820)
This PR adds the RAII chapter for the idiomatic Rust deep dive.
This commit is contained in:
committed by
GitHub
parent
e42c8b36d4
commit
05b67d12a5
@@ -441,6 +441,15 @@
|
|||||||
- [Semantic Confusion](idiomatic/leveraging-the-type-system/newtype-pattern/semantic-confusion.md)
|
- [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)
|
- [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)
|
- [Is It Encapsulated?](idiomatic/leveraging-the-type-system/newtype-pattern/is-it-encapsulated.md)
|
||||||
|
- [RAII](idiomatic/leveraging-the-type-system/raii.md)
|
||||||
|
- [Drop Skipped](idiomatic/leveraging-the-type-system/raii/drop_skipped.md)
|
||||||
|
- [Mutex](idiomatic/leveraging-the-type-system/raii/mutex.md)
|
||||||
|
- [Drop Guards](idiomatic/leveraging-the-type-system/raii/drop_guards.md)
|
||||||
|
- [Drop Bomb](idiomatic/leveraging-the-type-system/raii/drop_bomb.md)
|
||||||
|
- [Drop Bomb Forget](idiomatic/leveraging-the-type-system/raii/drop_bomb_forget.md)
|
||||||
|
- [forget and drop functions](idiomatic/leveraging-the-type-system/raii/forget_and_drop.md)
|
||||||
|
- [Scope Guard](idiomatic/leveraging-the-type-system/raii/scope_guard.md)
|
||||||
|
- [Drop Option](idiomatic/leveraging-the-type-system/raii/drop_option.md)
|
||||||
- [Extension Traits](idiomatic/leveraging-the-type-system/extension-traits.md)
|
- [Extension Traits](idiomatic/leveraging-the-type-system/extension-traits.md)
|
||||||
- [Extending Foreign Types](idiomatic/leveraging-the-type-system/extension-traits/extending-foreign-types.md)
|
- [Extending Foreign Types](idiomatic/leveraging-the-type-system/extension-traits/extending-foreign-types.md)
|
||||||
- [Method Resolution Conflicts](idiomatic/leveraging-the-type-system/extension-traits/method-resolution-conflicts.md)
|
- [Method Resolution Conflicts](idiomatic/leveraging-the-type-system/extension-traits/method-resolution-conflicts.md)
|
||||||
|
|||||||
94
src/idiomatic/leveraging-the-type-system/raii.md
Normal file
94
src/idiomatic/leveraging-the-type-system/raii.md
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
---
|
||||||
|
minutes: 15
|
||||||
|
---
|
||||||
|
|
||||||
|
# RAII: `Drop` trait
|
||||||
|
|
||||||
|
RAII (**R**esource **A**cquisition **I**s **I**nitialization) ties the lifetime
|
||||||
|
of a resource to the lifetime of a value.
|
||||||
|
|
||||||
|
[Rust uses RAII to manage memory](https://doc.rust-lang.org/rust-by-example/scope/raii.html),
|
||||||
|
and the `Drop` trait allows you to extend this to other resources, such as file
|
||||||
|
descriptors or locks.
|
||||||
|
|
||||||
|
```rust,editable
|
||||||
|
pub struct File(std::os::fd::RawFd);
|
||||||
|
|
||||||
|
impl File {
|
||||||
|
pub fn open(path: &str) -> Result<Self, std::io::Error> {
|
||||||
|
// [...]
|
||||||
|
Ok(Self(0))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn read_to_end(&mut self) -> Result<Vec<u8>, std::io::Error> {
|
||||||
|
// [...]
|
||||||
|
Ok(b"example".to_vec())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn close(self) -> Result<(), std::io::Error> {
|
||||||
|
// [...]
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() -> Result<(), std::io::Error> {
|
||||||
|
let mut file = File::open("example.txt")?;
|
||||||
|
println!("content: {:?}", file.read_to_end()?);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
<details>
|
||||||
|
|
||||||
|
- Easy to miss: `file.close()` is never called. Ask the class if they noticed.
|
||||||
|
|
||||||
|
- To release the file descriptor correctly, `file.close()` must be called after
|
||||||
|
the last use — and also in early-return paths in case of errors.
|
||||||
|
|
||||||
|
- Instead of relying on the user to call `close()`, we can implement the `Drop`
|
||||||
|
trait to release the resource automatically. This ties cleanup to the lifetime
|
||||||
|
of the `File` value.
|
||||||
|
|
||||||
|
```rust,compile_fail
|
||||||
|
impl Drop for File {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
// libc::close(...);
|
||||||
|
println!("file descriptor was closed");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- Note that `Drop::drop()` cannot return a `Result`. Any failures must be
|
||||||
|
handled internally or ignored. In the standard library, errors during FD
|
||||||
|
closure inside `Drop` are silently discarded. See the implementation:
|
||||||
|
<https://doc.rust-lang.org/src/std/os/fd/owned.rs.html#169-196>
|
||||||
|
|
||||||
|
- When is `Drop::drop` called?
|
||||||
|
|
||||||
|
Normally, when the `file` variable in `main()` goes out of scope (either on
|
||||||
|
return or due to a panic), `drop()` is called automatically.
|
||||||
|
|
||||||
|
If the file is moved into another function (as is this case with
|
||||||
|
`File::close()`), the value is dropped when that function returns — not in
|
||||||
|
`main`.
|
||||||
|
|
||||||
|
In contrast, C++ runs destructors in the original scope even for moved-from
|
||||||
|
values.
|
||||||
|
|
||||||
|
- Demo: insert `panic!("oops")` at the start of `read_to_end()` and run it.
|
||||||
|
`drop()` still runs during unwinding.
|
||||||
|
|
||||||
|
### More to Explore
|
||||||
|
|
||||||
|
The `Drop` trait has another important limitation: it is not `async`.
|
||||||
|
|
||||||
|
This means you cannot `await` inside a destructor, which is often needed when
|
||||||
|
cleaning up asynchronous resources like sockets, database connections, or tasks
|
||||||
|
that must signal completion to another system.
|
||||||
|
|
||||||
|
- Learn more:
|
||||||
|
<https://rust-lang.github.io/async-fundamentals-initiative/roadmap/async_drop.html>
|
||||||
|
- There is an experimental `AsyncDrop` trait available on nightly:
|
||||||
|
<https://doc.rust-lang.org/nightly/std/future/trait.AsyncDrop.html>
|
||||||
|
|
||||||
|
</details>
|
||||||
92
src/idiomatic/leveraging-the-type-system/raii/drop_bomb.md
Normal file
92
src/idiomatic/leveraging-the-type-system/raii/drop_bomb.md
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
---
|
||||||
|
minutes: 15
|
||||||
|
---
|
||||||
|
|
||||||
|
# Drop Bombs: Enforcing API Correctness
|
||||||
|
|
||||||
|
Use `Drop` to enforce invariants and detect incorrect API usage. A "drop bomb"
|
||||||
|
panics if a value is dropped without being explicitly finalized.
|
||||||
|
|
||||||
|
This pattern is often used when the finalizing operation (like `commit()` or
|
||||||
|
`rollback()`) needs to return a `Result`, which cannot be done from `Drop`.
|
||||||
|
|
||||||
|
```rust,editable
|
||||||
|
use std::io::{self, Write};
|
||||||
|
|
||||||
|
struct Transaction {
|
||||||
|
active: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Transaction {
|
||||||
|
fn start() -> Self {
|
||||||
|
Self { active: true }
|
||||||
|
}
|
||||||
|
|
||||||
|
fn commit(mut self) -> io::Result<()> {
|
||||||
|
writeln!(io::stdout(), "COMMIT")?;
|
||||||
|
self.active = false;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Drop for Transaction {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
if self.active {
|
||||||
|
panic!("Transaction dropped without commit!");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() -> io::Result<()> {
|
||||||
|
let tx = Transaction::start();
|
||||||
|
// Use `tx` to build the transaction, then commit it.
|
||||||
|
// Comment out the call to `commit` to see the panic.
|
||||||
|
tx.commit()?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
<details>
|
||||||
|
|
||||||
|
- In some systems, a value must be finalized by a specific API before it is
|
||||||
|
dropped.
|
||||||
|
|
||||||
|
For example, a `Transaction` might need to be committed or rolled back.
|
||||||
|
|
||||||
|
- A drop bomb ensures that a value like `Transaction` cannot be silently dropped
|
||||||
|
in an unfinished state. The destructor panics if the transaction has not been
|
||||||
|
explicitly finalized (for example, with `commit()`).
|
||||||
|
|
||||||
|
- The finalizing operation (such as `commit()`) usually takes `self` by value.
|
||||||
|
This ensures that once the transaction is finalized, the original object can
|
||||||
|
no longer be used.
|
||||||
|
|
||||||
|
- A common reason to use this pattern is when cleanup cannot be done in `Drop`,
|
||||||
|
either because it is fallible or asynchronous.
|
||||||
|
|
||||||
|
- This pattern is appropriate even in public APIs. It can help users catch bugs
|
||||||
|
early when they forget to explicitly finalize a transactional object.
|
||||||
|
|
||||||
|
- If cleanup can safely happen in `Drop`, some APIs choose to panic only in
|
||||||
|
debug builds. Whether this is appropriate depends on the guarantees your API
|
||||||
|
must enforce.
|
||||||
|
|
||||||
|
- Panicking in release builds is reasonable when silent misuse would cause major
|
||||||
|
correctness or security problems.
|
||||||
|
|
||||||
|
- Question: Why do we need an `active` flag inside `Transaction`? Why can't
|
||||||
|
`drop()` panic unconditionally?
|
||||||
|
|
||||||
|
Expected answer: `commit()` takes `self` by value and runs `drop()`, which
|
||||||
|
would panic.
|
||||||
|
|
||||||
|
## More to explore
|
||||||
|
|
||||||
|
Several related patterns help enforce correct teardown or prevent accidental
|
||||||
|
drops.
|
||||||
|
|
||||||
|
- The [`drop_bomb` crate](https://docs.rs/drop_bomb/latest/drop_bomb/): A small
|
||||||
|
utility that panics if dropped unless explicitly defused with `.defuse()`.
|
||||||
|
Comes with a `DebugDropBomb` variant that only activates in debug builds.
|
||||||
|
|
||||||
|
</details>
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
---
|
||||||
|
minutes: 10
|
||||||
|
---
|
||||||
|
|
||||||
|
# Drop Bombs: using `std::mem::forget`
|
||||||
|
|
||||||
|
```rust,editable
|
||||||
|
use std::io::{self, Write};
|
||||||
|
|
||||||
|
struct Transaction;
|
||||||
|
|
||||||
|
impl Transaction {
|
||||||
|
fn start() -> Self {
|
||||||
|
Transaction
|
||||||
|
}
|
||||||
|
|
||||||
|
fn commit(self) -> io::Result<()> {
|
||||||
|
writeln!(io::stdout(), "COMMIT")?;
|
||||||
|
|
||||||
|
// Defuse the drop bomb by preventing Drop from ever running.
|
||||||
|
std::mem::forget(self);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Drop for Transaction {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
// This is the "drop bomb"
|
||||||
|
panic!("Transaction dropped without commit!");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() -> io::Result<()> {
|
||||||
|
let tx = Transaction::start();
|
||||||
|
// Use `tx` to build the transaction, then commit it.
|
||||||
|
// Comment out the call to `commit` to see the panic.
|
||||||
|
tx.commit()?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
<details>
|
||||||
|
|
||||||
|
This example removes the flag from the previous slide and makes the drop method
|
||||||
|
panic unconditionally. To avoid that panic on a successful commit, the commit
|
||||||
|
method now takes ownership of the transaction and calls
|
||||||
|
[`std::mem::forget`](https://doc.rust-lang.org/std/mem/fn.forget.html), which
|
||||||
|
prevents the `Drop::drop()` method from running.
|
||||||
|
|
||||||
|
If the forgotten value owned heap allocated memory that would normally be freed
|
||||||
|
in its `drop()` implementation, one consequence is a memory leak. That is not
|
||||||
|
the case for the `Transaction` in the example above, since it does not own any
|
||||||
|
heap memory.
|
||||||
|
|
||||||
|
We can avoid needing a runtime flag by using `mem::forget()` in a tactical way.
|
||||||
|
When the transaction commits successfully, we can defuse the drop bomb by
|
||||||
|
calling `std::mem::forget` on the value, which prevents its `Drop`
|
||||||
|
implementation from running.
|
||||||
|
|
||||||
|
</details>
|
||||||
69
src/idiomatic/leveraging-the-type-system/raii/drop_guards.md
Normal file
69
src/idiomatic/leveraging-the-type-system/raii/drop_guards.md
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
---
|
||||||
|
minutes: 10
|
||||||
|
---
|
||||||
|
|
||||||
|
# Drop Guards
|
||||||
|
|
||||||
|
A **drop guard** in Rust is a temporary object that performs some kind of
|
||||||
|
cleanup when it goes out of scope. In the case of `Mutex`, the `lock` method
|
||||||
|
returns a `MutexGuard` that automatically unlocks the mutex on `drop`:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
struct Mutex {
|
||||||
|
is_locked: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct MutexGuard<'a> {
|
||||||
|
mutex: &'a mut Mutex,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Mutex {
|
||||||
|
fn new() -> Self {
|
||||||
|
Self { is_locked: false }
|
||||||
|
}
|
||||||
|
|
||||||
|
fn lock(&mut self) -> MutexGuard<'_> {
|
||||||
|
self.is_locked = true;
|
||||||
|
MutexGuard { mutex: self }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Drop for MutexGuard<'_> {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
self.mutex.is_locked = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
<details>
|
||||||
|
|
||||||
|
- The example above shows a simplified `Mutex` and its associated guard.
|
||||||
|
|
||||||
|
- Even though it is not a production-ready implementation, it illustrates the
|
||||||
|
core idea:
|
||||||
|
|
||||||
|
- the guard represents exclusive access,
|
||||||
|
- and its `Drop` implementation releases the lock when it goes out of scope.
|
||||||
|
|
||||||
|
## More to Explore
|
||||||
|
|
||||||
|
This example shows a C++ style mutex that does not contain the data it protects.
|
||||||
|
While this is non-idiomatic in Rust, the goal here is only to illustrate the
|
||||||
|
core idea of a drop guard, not to demonstrate a proper Rust mutex design.
|
||||||
|
|
||||||
|
For brevity, several features are omitted:
|
||||||
|
|
||||||
|
- A real `Mutex<T>` stores the protected value inside the mutex.\
|
||||||
|
This toy example omits the value entirely to focus only on the drop guard
|
||||||
|
mechanism.
|
||||||
|
- Ergonomic access via `Deref` and `DerefMut` on `MutexGuard` (letting the guard
|
||||||
|
behave like a `&T` or `&mut T`).
|
||||||
|
- A fully blocking `.lock()` method and a non-blocking `try_lock` variant.
|
||||||
|
|
||||||
|
You can explore the
|
||||||
|
[`Mutex` implementation in Rust’s std library](https://doc.rust-lang.org/std/sync/struct.Mutex.html)
|
||||||
|
as an example of a production-ready mutex. The
|
||||||
|
[`Mutex` from the `parking_lot` crate](https://docs.rs/parking_lot/latest/parking_lot/type.Mutex.html)
|
||||||
|
is another worthwhile reference.
|
||||||
|
|
||||||
|
</details>
|
||||||
88
src/idiomatic/leveraging-the-type-system/raii/drop_option.md
Normal file
88
src/idiomatic/leveraging-the-type-system/raii/drop_option.md
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
---
|
||||||
|
minutes: 10
|
||||||
|
---
|
||||||
|
|
||||||
|
# Drop: Option
|
||||||
|
|
||||||
|
```rust,editable
|
||||||
|
struct File(Option<Handle>);
|
||||||
|
|
||||||
|
impl File {
|
||||||
|
fn open(path: &'static str) -> std::io::Result<Self> {
|
||||||
|
Ok(Self(Some(Handle { path })))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write(&mut self, data: &str) -> std::io::Result<()> {
|
||||||
|
match &mut self.0 {
|
||||||
|
Some(handle) => println!("write '{data}' to file '{}'", handle.path),
|
||||||
|
None => unreachable!(),
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn close(mut self) -> std::io::Result<&'static str> {
|
||||||
|
Ok(self.0.take().unwrap().path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Drop for File {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
if let Some(handle) = self.0.take() {
|
||||||
|
println!("automatically closing handle for file: {}", handle.path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Handle {
|
||||||
|
path: &'static str,
|
||||||
|
}
|
||||||
|
impl Drop for Handle {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
println!("closed handle for file: {}", self.path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() -> std::io::Result<()> {
|
||||||
|
let mut file = File::open("foo.txt")?;
|
||||||
|
file.write("hello")?;
|
||||||
|
println!("manually closed file: {}", file.close()?);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
<details>
|
||||||
|
|
||||||
|
- In this example we want to let the user call `close()` manually so that errors
|
||||||
|
from closing the file can be reported explicitly.
|
||||||
|
|
||||||
|
- At the same time we still want RAII semantics: if the user forgets to call
|
||||||
|
`close()`, the handle must be cleaned up automatically in `Drop`.
|
||||||
|
|
||||||
|
- Wrapping the handle in an `Option` gives us both behaviors. `close()` extracts
|
||||||
|
the handle with `take()`, and `Drop` only runs cleanup if a handle is still
|
||||||
|
present.
|
||||||
|
|
||||||
|
Demo: remove the `.close()` call and run the code — `Drop` now prints the
|
||||||
|
automatic cleanup.
|
||||||
|
|
||||||
|
- The main downside is ergonomics. `Option` forces us to handle both the `Some`
|
||||||
|
and `None` case even in places where, logically, `None` cannot occur. Rust’s
|
||||||
|
type system cannot express that relationship between `File` and its `Handle`,
|
||||||
|
so we handle both cases manually.
|
||||||
|
|
||||||
|
## More to explore
|
||||||
|
|
||||||
|
Instead of `Option` we could use
|
||||||
|
[`ManuallyDrop`](https://doc.rust-lang.org/std/mem/struct.ManuallyDrop.html),
|
||||||
|
which suppresses automatic destruction by preventing Rust from calling `Drop`
|
||||||
|
for the value; you must handle teardown yourself.
|
||||||
|
|
||||||
|
The [_scopeguard_ example](./scope_guard.md) on the previous slide shows how
|
||||||
|
`ManuallyDrop` can replace `Option` to avoid handling `None` in places where the
|
||||||
|
value should always exist.
|
||||||
|
|
||||||
|
In such designs we typically track the drop state with a separate flag next to
|
||||||
|
the `ManuallyDrop<Handle>`, which lets us track whether the handle has already
|
||||||
|
been manually consumed.
|
||||||
|
|
||||||
|
</details>
|
||||||
122
src/idiomatic/leveraging-the-type-system/raii/drop_skipped.md
Normal file
122
src/idiomatic/leveraging-the-type-system/raii/drop_skipped.md
Normal file
@@ -0,0 +1,122 @@
|
|||||||
|
---
|
||||||
|
minutes: 15
|
||||||
|
---
|
||||||
|
|
||||||
|
# Drop can be skipped
|
||||||
|
|
||||||
|
There are cases where destructors will not run.
|
||||||
|
|
||||||
|
```rust,editable
|
||||||
|
#[derive(Debug)]
|
||||||
|
struct OwnedFd(i32);
|
||||||
|
|
||||||
|
impl Drop for OwnedFd {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
println!("OwnedFd::drop() called with raw fd: {:?}", self.0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Drop for TmpFile {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
println!("TmpFile::drop() called with owned fd: {:?}", self.0);
|
||||||
|
// libc::unlink("/tmp/file")
|
||||||
|
// panic!("TmpFile::drop() panics");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
struct TmpFile(OwnedFd);
|
||||||
|
|
||||||
|
impl TmpFile {
|
||||||
|
fn open() -> Self {
|
||||||
|
Self(OwnedFd(2))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn close(&self) {
|
||||||
|
panic!("TmpFile::close(): not implemented yet");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
let owned_fd = OwnedFd(1);
|
||||||
|
|
||||||
|
let file = TmpFile::open();
|
||||||
|
|
||||||
|
std::process::exit(0);
|
||||||
|
|
||||||
|
// std::mem::forget(file);
|
||||||
|
|
||||||
|
// file.close();
|
||||||
|
|
||||||
|
let _ = owned_fd;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
<details>
|
||||||
|
|
||||||
|
- Drop is not guaranteed to always run. There is a number of cases when drop is
|
||||||
|
skipped: the program can crash or exit, the value with the drop implementation
|
||||||
|
can be leaked etc.
|
||||||
|
|
||||||
|
- In the version that calls
|
||||||
|
[`std::process::exit`](https://doc.rust-lang.org/std/process/fn.exit.html),
|
||||||
|
`TmpFile::drop()` is never run because `exit()` terminates the process
|
||||||
|
immediately without any opportunity for a `drop()` method to be called.
|
||||||
|
|
||||||
|
- You can prevent accidental use of `exit` by denying the
|
||||||
|
[`clippy::exit`](https://rust-lang.github.io/rust-clippy/stable/index.html#exit)
|
||||||
|
lint.
|
||||||
|
|
||||||
|
- If you remove the `std::process::exit(0)` line, each `drop()` method in this
|
||||||
|
simple case will run in turn.
|
||||||
|
|
||||||
|
- Try uncommenting the
|
||||||
|
[`std::mem::forget`](https://doc.rust-lang.org/std/mem/fn.forget.html) call.
|
||||||
|
What do you think will happen?
|
||||||
|
|
||||||
|
`mem::forget()` takes ownership and "forgets" about the value `file` without
|
||||||
|
running its **destructor** `Drop::drop()`. The destructor of `owned_fd` is
|
||||||
|
still run.
|
||||||
|
|
||||||
|
- Remove the `mem::forget()` call, then uncomment the `file.close()` call below
|
||||||
|
it. What do you expect now?
|
||||||
|
|
||||||
|
With the default `panic = "unwind"` setting, the stack still unwinds and
|
||||||
|
destructors run, even when the panic starts in `main`.
|
||||||
|
|
||||||
|
- With
|
||||||
|
[`panic = "abort"`](https://doc.rust-lang.org/cargo/reference/profiles.html#panic)
|
||||||
|
no destructors are run.
|
||||||
|
|
||||||
|
- As a last step, uncomment the `panic!` inside `TmpFile::drop()` and run it.
|
||||||
|
Ask the class: which destructors run before the abort?
|
||||||
|
|
||||||
|
After a double panic, Rust no longer guarantees that remaining destructors
|
||||||
|
will run:
|
||||||
|
|
||||||
|
- Some cleanup that was already in progress may still complete (for example,
|
||||||
|
field destructors of the value currently being dropped),
|
||||||
|
- but anything scheduled later in the unwind path might be skipped entirely.
|
||||||
|
- This is why we say you cannot rely solely on `drop()` for critical external
|
||||||
|
cleanup, nor assume that a double panic aborts without running any further
|
||||||
|
destructors.
|
||||||
|
|
||||||
|
- Some languages forbid or restrict exceptions in destructors. Rust allows
|
||||||
|
panicking in `Drop::drop`, but it is almost never a good idea, since it can
|
||||||
|
disrupt unwinding and lead to unpredictable cleanup. It is best avoided unless
|
||||||
|
there is a very specific need, such as in the case of a **drop bomb**.
|
||||||
|
|
||||||
|
- Drop is suitable for cleaning up resources within the scope of a process, but
|
||||||
|
it is not the right tool for providing hard guarantees that something happens
|
||||||
|
outside of the process (e.g., on local disk, or in another service in a
|
||||||
|
distributed system).
|
||||||
|
|
||||||
|
- For example, deleting a temporary file in `drop()` is fine in a toy example,
|
||||||
|
but in a real program you would still need an external cleanup mechanism such
|
||||||
|
as a temp file reaper.
|
||||||
|
|
||||||
|
- In contrast, we can rely on `drop()` to unlock a mutex, since it is a
|
||||||
|
process-local resource. If `drop()` is skipped and the mutex is left locked,
|
||||||
|
it has no lasting effects outside the process.
|
||||||
|
|
||||||
|
</details>
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
---
|
||||||
|
minutes: 10
|
||||||
|
---
|
||||||
|
|
||||||
|
# forget and drop functions
|
||||||
|
|
||||||
|
Below are the signatures for the
|
||||||
|
[`drop()`](https://doc.rust-lang.org/std/mem/fn.drop.html) and
|
||||||
|
[`forget()`](https://doc.rust-lang.org/std/mem/fn.forget.html) functions:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
// std::mem::forget
|
||||||
|
fn forget<T>(t: T) {
|
||||||
|
let _ = std::mem::ManuallyDrop::new(t);
|
||||||
|
}
|
||||||
|
|
||||||
|
// std::mem::drop
|
||||||
|
fn drop<T>(_x: T) {}
|
||||||
|
```
|
||||||
|
|
||||||
|
<details>
|
||||||
|
|
||||||
|
- Both `mem::forget()` and `mem::drop()` take ownership of the value `t`.
|
||||||
|
|
||||||
|
- Despite having the same function signature, they have opposite effects:
|
||||||
|
|
||||||
|
- `forget()` uses
|
||||||
|
[`ManuallyDrop`](https://doc.rust-lang.org/std/mem/struct.ManuallyDrop.html)
|
||||||
|
to prevent the destructor `Drop::drop()` from being invoked.
|
||||||
|
|
||||||
|
This is useful for scenarios such as implementing a drop bomb or otherwise
|
||||||
|
opting out of destructor behavior.
|
||||||
|
|
||||||
|
Be careful though, since any resources the value exclusively owns such as
|
||||||
|
heap allocated memory or file handles will remain in an unreachable state.
|
||||||
|
|
||||||
|
- `drop()` is a convenience function for disposing of a value. Because `t` is
|
||||||
|
moved into the function, it is automatically dropped which triggers its
|
||||||
|
`Drop::drop()` implementation before the parent function returns.
|
||||||
|
|
||||||
|
</details>
|
||||||
43
src/idiomatic/leveraging-the-type-system/raii/mutex.md
Normal file
43
src/idiomatic/leveraging-the-type-system/raii/mutex.md
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
---
|
||||||
|
minutes: 10
|
||||||
|
---
|
||||||
|
|
||||||
|
# Mutex and MutexGuard
|
||||||
|
|
||||||
|
In earlier examples, RAII was used to manage concrete resources like file
|
||||||
|
descriptors. With a `Mutex`, the "resource" is mutable access to a value. You
|
||||||
|
access the value by calling `lock`, which then returns a `MutexGuard` which will
|
||||||
|
unlock the `Mutex` automatically when dropped.
|
||||||
|
|
||||||
|
```rust
|
||||||
|
use std::sync::Mutex;
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
let m = Mutex::new(vec![1, 2, 3]);
|
||||||
|
|
||||||
|
let mut guard = m.lock().unwrap();
|
||||||
|
guard.push(4);
|
||||||
|
guard.push(5);
|
||||||
|
println!("{guard:?}");
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
<details>
|
||||||
|
|
||||||
|
- A `Mutex` controls exclusive access to a value. Unlike earlier RAII examples,
|
||||||
|
the resource here is logical: temporary exclusive access to the data inside.
|
||||||
|
|
||||||
|
- This right is represented by a `MutexGuard`. Only one guard for this mutex can
|
||||||
|
exist at a time. While it lives, it provides `&mut T` access.
|
||||||
|
|
||||||
|
- Although `lock()` takes `&self`, it returns a `MutexGuard` with mutable
|
||||||
|
access. This works through _interior mutability_, where a type manages its own
|
||||||
|
borrowing rules internally to allow mutation through `&self`.
|
||||||
|
|
||||||
|
- `MutexGuard` implements `Deref` and `DerefMut`, making access ergonomic. You
|
||||||
|
lock the mutex and use the guard like a `&mut T`.
|
||||||
|
|
||||||
|
- The mutex is released by `MutexGuard::drop()`. You never call an explicit
|
||||||
|
unlock function.
|
||||||
|
|
||||||
|
</details>
|
||||||
78
src/idiomatic/leveraging-the-type-system/raii/scope_guard.md
Normal file
78
src/idiomatic/leveraging-the-type-system/raii/scope_guard.md
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
---
|
||||||
|
minutes: 15
|
||||||
|
---
|
||||||
|
|
||||||
|
# Scope Guards
|
||||||
|
|
||||||
|
A scope guard uses the `Drop` trait to run cleanup code automatically when a
|
||||||
|
scope exits, even during unwinding.
|
||||||
|
|
||||||
|
```rust,editable,compile_fail
|
||||||
|
use scopeguard::{ScopeGuard, guard};
|
||||||
|
use std::fs::{self, File};
|
||||||
|
use std::io::Write;
|
||||||
|
|
||||||
|
fn download_successful() -> bool {
|
||||||
|
// [...]
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
let path = "download.tmp";
|
||||||
|
let mut file = File::create(path).expect("cannot create temporary file");
|
||||||
|
|
||||||
|
// Set up cleanup immediately after file creation
|
||||||
|
let cleanup = guard(path, |path| {
|
||||||
|
println!("download failed, deleting: {:?}", path);
|
||||||
|
let _ = fs::remove_file(path);
|
||||||
|
});
|
||||||
|
|
||||||
|
writeln!(file, "partial data...").unwrap();
|
||||||
|
|
||||||
|
if download_successful() {
|
||||||
|
// Download succeeded, keep the file
|
||||||
|
let path = ScopeGuard::into_inner(cleanup);
|
||||||
|
println!("Download '{path}' complete!");
|
||||||
|
}
|
||||||
|
// Otherwise, the guard runs and deletes the file
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
<details>
|
||||||
|
|
||||||
|
- This example models a download workflow. We create a temporary file first,
|
||||||
|
then use a scope guard to ensure that the file is deleted if the download
|
||||||
|
fails.
|
||||||
|
|
||||||
|
- The `scopeguard` crate allows you to conveniently define a single-use
|
||||||
|
`Drop`-based cleanup without defining a custom type with a custom `Drop`
|
||||||
|
implementation.
|
||||||
|
|
||||||
|
- The guard is created directly after creating the file, so even if `writeln!()`
|
||||||
|
fails, the file will still be cleaned up. This ordering is essential for
|
||||||
|
correctness.
|
||||||
|
|
||||||
|
- The `guard()` creates a `ScopeGuard` instance. It a user-defined value (in
|
||||||
|
this case, `path`) and the cleanup closure that later receives this value.
|
||||||
|
|
||||||
|
- The guard's closure runs on scope exit unless it is _defused_ with
|
||||||
|
`ScopeGuard::into_inner` (removing the value so the guard does nothing on
|
||||||
|
drop). In the success path, we call `into_inner` so the guard will not delete
|
||||||
|
the file.
|
||||||
|
|
||||||
|
- A scope guard is similar to the `defer` feature in Go.
|
||||||
|
|
||||||
|
- This pattern is ideal for "cleanup on failure" scenarios, where a cleanup
|
||||||
|
should run by default unless a success path is explicitly taken.
|
||||||
|
|
||||||
|
- This pattern is also useful when you don't control the cleanup strategy of the
|
||||||
|
resource object. In this example, `File::drop()` closes the file but does not
|
||||||
|
delete it, and we can't change the standard library to delete the file instead
|
||||||
|
(nor should we, it is not a good idea anyway).
|
||||||
|
|
||||||
|
- The `scopeguard` crate also supports cleanup strategies via the
|
||||||
|
[`Strategy`](https://docs.rs/scopeguard/latest/scopeguard/trait.Strategy.html)
|
||||||
|
trait. You can choose to run the guard on unwind only, or on success only, not
|
||||||
|
just always.
|
||||||
|
|
||||||
|
</details>
|
||||||
@@ -32,6 +32,13 @@ decisions within the context and constraints of your own projects.
|
|||||||
The course will cover the topics listed below. Each topic may be covered in one
|
The course will cover the topics listed below. Each topic may be covered in one
|
||||||
or more slides, depending on its complexity and relevance.
|
or more slides, depending on its complexity and relevance.
|
||||||
|
|
||||||
|
## Target Audience
|
||||||
|
|
||||||
|
Engineers with at least 2-3 years of coding experience in C, C++11 or newer,
|
||||||
|
Java 7 or newer, Python 2 or 3, Go or any other similar imperative programming
|
||||||
|
language. We have no expectation of experience with more modern or feature-rich
|
||||||
|
languages like Swift, Kotlin, C#, or TypeScript.
|
||||||
|
|
||||||
### Foundations of API design
|
### Foundations of API design
|
||||||
|
|
||||||
- Golden rule: prioritize clarity and readability at the callsite. People will
|
- Golden rule: prioritize clarity and readability at the callsite. People will
|
||||||
|
|||||||
Reference in New Issue
Block a user