You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
nomicon/raii.md

424 lines
15 KiB

10 years ago
% The Perils Of RAII
10 years ago
Ownership Based Resource Management (AKA RAII: Resource Acquisition Is Initialization) is
10 years ago
something you'll interact with a lot in Rust. Especially if you use the standard library.
Roughly speaking the pattern is as follows: to acquire a resource, you create an object that
manages it. To release the resource, you simply destroy the object, and it cleans up the
resource for you. The most common "resource"
this pattern manages is simply *memory*. `Box`, `Rc`, and basically everything in
`std::collections` is a convenience to enable correctly managing memory. This is particularly
important in Rust because we have no pervasive GC to rely on for memory management. Which is the
point, really: Rust is about control. However we are not limited to just memory.
Pretty much every other system resource like a thread, file, or socket is exposed through
this kind of API.
10 years ago
# Constructors
Unlike C++, Rust does not come with a slew of builtin
10 years ago
kinds of constructor. There are no Copy, Default, Assignment, Move, or whatever constructors.
This largely has to do with Rust's philosophy of being explicit.
Move constructors are meaningless in Rust because we don't enable types to "care" about their
location in memory. Every type must be ready for it to be blindly memcopied to somewhere else
in memory. This means pure on-the-stack-but-still-movable intrusive linked lists are simply
not happening in Rust (safely).
Assignment and copy constructors similarly don't exist because move semantics are the *default*
in rust. At most `x = y` just moves the bits of y into the x variable. Rust does provide two
facilities for going back to C++'s copy-oriented semantics: `Copy` and `Clone`. Clone is our
10 years ago
moral equivalent of a copy constructor, but it's never implicitly invoked. You have to explicitly
10 years ago
call `clone` on an element you want to be cloned. Copy is a special case of Clone where the
10 years ago
implementation is just "copy the bits". Copy types *are* implicitly
10 years ago
cloned whenever they're moved, but because of the definition of Copy this just means *not*
10 years ago
treating the old copy as uninitialized -- a no-op.
10 years ago
While Rust provides a `Default` trait for specifying the moral equivalent of a default
constructor, it's incredibly rare for this trait to be used. This is because variables
10 years ago
[aren't implicitly initialized][uninit]. Default is basically only useful for generic
programming. In concrete contexts, a type will provide a static `new` method for any
10 years ago
kind of "default" constructor. This has no relation to `new` in other
languages and has no special meaning. It's just a naming convention.
# Destructors
10 years ago
What the language *does* provide is full-blown automatic destructors through the `Drop` trait,
which provides the following method:
```rust
fn drop(&mut self);
```
This method gives the type time to somehow finish what it was doing. **After `drop` is run,
10 years ago
Rust will recursively try to drop all of the fields of `self`**. This is a
10 years ago
convenience feature so that you don't have to write "destructor boilerplate" to drop
children. If a struct has no special logic for being dropped other than dropping its
children, then it means `Drop` doesn't need to be implemented at all!
10 years ago
**There is no stable way to prevent this behaviour in Rust 1.0**.
10 years ago
Note that taking `&mut self` means that even if you *could* suppress recursive Drop,
Rust will prevent you from e.g. moving fields out of self. For most types, this
10 years ago
is totally fine.
10 years ago
For instance, a custom implementation of `Box` might write `Drop` like this:
```rust
struct Box<T>{ ptr: *mut T }
impl<T> Drop for Box<T> {
fn drop(&mut self) {
unsafe {
(*self.ptr).drop();
heap::deallocate(self.ptr);
}
}
}
```
and this works fine because when Rust goes to drop the `ptr` field it just sees a *mut that
has no actual `Drop` implementation. Similarly nothing can use-after-free the `ptr` because
10 years ago
the Box is immediately marked as uninitialized.
10 years ago
However this wouldn't work:
```rust
struct Box<T>{ ptr: *mut T }
impl<T> Drop for Box<T> {
fn drop(&mut self) {
unsafe {
(*self.ptr).drop();
heap::deallocate(self.ptr);
}
}
}
struct SuperBox<T> { box: Box<T> }
impl<T> Drop for SuperBox<T> {
fn drop(&mut self) {
unsafe {
// Hyper-optimized: deallocate the box's contents for it
// without `drop`ing the contents
heap::deallocate(self.box.ptr);
}
}
}
```
10 years ago
After we deallocate the `box`'s ptr in SuperBox's destructor, Rust will
10 years ago
happily proceed to tell the box to Drop itself and everything will blow up with
use-after-frees and double-frees.
Note that the recursive drop behaviour applies to *all* structs and enums
regardless of whether they implement Drop. Therefore something like
```rust
struct Boxy<T> {
data1: Box<T>,
data2: Box<T>,
info: u32,
}
```
will have its data1 and data2's fields destructors whenever it "would" be
dropped, even though it itself doesn't implement Drop. We say that such a type
*needs Drop*, even though it is not itself Drop.
Similarly,
```rust
enum Link {
Next(Box<Link>),
None,
}
```
10 years ago
will have its inner Box field dropped *if and only if* an instance stores the Next variant.
10 years ago
In general this works really nice because you don't need to worry about adding/removing
10 years ago
drops when you refactor your data layout. Still there's certainly many valid usecases for
10 years ago
needing to do trickier things with destructors.
10 years ago
The classic safe solution to overriding recursive drop and allowing moving out
of Self during `drop` is to use an Option:
10 years ago
```rust
struct Box<T>{ ptr: *mut T }
impl<T> Drop for Box<T> {
fn drop(&mut self) {
unsafe {
(*self.ptr).drop();
heap::deallocate(self.ptr);
}
}
}
struct SuperBox<T> { box: Option<Box<T>> }
impl<T> Drop for SuperBox<T> {
fn drop(&mut self) {
unsafe {
// Hyper-optimized: deallocate the box's contents for it
// without `drop`ing the contents. Need to set the `box`
10 years ago
// field as `None` to prevent Rust from trying to Drop it.
10 years ago
heap::deallocate(self.box.take().unwrap().ptr);
}
}
}
```
10 years ago
However this has fairly odd semantics: you're saying that a field that *should* always
be Some may be None, just because that happens in the destructor. Of course this
conversely makes a lot of sense: you can call arbitrary methods on self during
the destructor, and this should prevent you from ever doing so after deinitializing
the field. Not that it will prevent you from producing any other
10 years ago
arbitrarily invalid state in there.
10 years ago
On balance this is an ok choice. Certainly what you should reach for by default.
However, in the future we expect there to be a first-class way to announce that
a field shouldn't be automatically dropped.
# Leaking
Ownership based resource management is intended to simplify composition. You
acquire resources when you create the object, and you release the resources
when it gets destroyed. Since destruction is handled for you, it means you
can't forget to release the resources, and it happens as soon as possible!
Surely this is perfect and all of our problems are solved.
Everything is terrible and we have new and exotic problems to try to solve.
Many people like to believe that Rust eliminates resource leaks, but this
is absolutely not the case, no matter how you look at it. In the strictest
sense, "leaking" is so abstract as to be unpreventable. It's quite trivial
to initialize a collection at the start of a program, fill it with tons of
objects with destructors, and then enter an infinite event loop that never
refers to it. The collection will sit around uselessly, holding on to its
precious resources until the program terminates (at which point all those
resources would have been reclaimed by the OS anyway).
10 years ago
We may consider a more restricted form of leak: failing to drop a value that
10 years ago
is unreachable. Rust also doesn't prevent this. In fact Rust has a *function
for doing this*: `mem::forget`. This function consumes the value it is passed
*and then doesn't run its destructor*.
In the past `mem::forget` was marked as unsafe as a sort of lint against using
it, since failing to call a destructor is generally not a well-behaved thing to
do (though useful for some special unsafe code). However this was generally
determined to be an untenable stance to take: there are *many* ways to fail to
call a destructor in safe code. The most famous example is creating a cycle
of reference counted pointers using interior mutability.
It is reasonable for safe code to assume that destructor leaks do not happen,
as any program that leaks destructors is probably wrong. However *unsafe* code
cannot rely on destructors to be run to be *safe*. For most types this doesn't
matter: if you leak the destructor then the type is *by definition* inaccessible,
10 years ago
so it doesn't matter, right? For instance, if you leak a `Box<u8>` then you
waste some memory but that's hardly going to violate memory-safety.
10 years ago
However where we must be careful with destructor leaks are *proxy* types.
These are types which manage access to a distinct object, but don't actually
own it. Proxy objects are quite rare. Proxy objects you'll need to care about
10 years ago
are even rarer. However we'll focus on three interesting examples in the
10 years ago
standard library:
* `vec::Drain`
* `Rc`
10 years ago
* `thread::scoped::JoinGuard`
10 years ago
## Drain
`drain` is a collections API that moves data out of the container without
consuming the container. This enables us to reuse the allocation of a `Vec`
10 years ago
after claiming ownership over all of its contents. It produces an iterator
10 years ago
(Drain) that returns the contents of the Vec by-value.
Now, consider Drain in the middle of iteration: some values have been moved out,
and others haven't. This means that part of the Vec is now full of logically
uninitialized data! We could backshift all the elements in the Vec every time we
remove a value, but this would have pretty catastrophic performance consequences.
Instead, we would like Drain to *fix* the Vec's backing storage when it is
dropped. It should run itself to completion, backshift any elements that weren't
removed (drain supports subranges), and then fix Vec's `len`. It's even
unwinding-safe! Easy!
Now consider the following:
```
let mut vec = vec![Box::new(0); 4];
{
// start draining, vec can no longer be accessed
let mut drainer = vec.drain(..);
// pull out two elements and immediately drop them
drainer.next();
drainer.next();
// get rid of drainer, but don't call its destructor
mem::forget(drainer);
}
// Oops, vec[0] was dropped, we're reading a pointer into free'd memory!
println!("{}", vec[0]);
```
This is pretty clearly Not Good. Unfortunately, we're kind've stuck between
a rock and a hard place: maintaining consistent state at every step has
an enormous cost (and would negate any benefits of the API). Failing to maintain
consistent state gives us Undefined Behaviour in safe code (making the API
unsound).
So what can we do? Well, we can pick a trivially consistent state: set the Vec's
len to be 0 when we *start* the iteration, and fix it up if necessary in the
destructor. That way, if everything executes like normal we get the desired
behaviour with minimal overhead. But if someone has the *audacity* to mem::forget
us in the middle of the iteration, all that does is *leak even more* (and possibly
leave the Vec in an *unexpected* but consistent state). Since we've
accepted that mem::forget is safe, this is definitely safe. We call leaks causing
more leaks a *leak amplification*.
## Rc
Rc is an interesting case because at first glance it doesn't appear to be a
proxy value at all. After all, it manages the data it points to, and dropping
all the Rcs for a value will drop that value. leaking an Rc doesn't seem like
it would be particularly dangerous. It will leave the refcount permanently
incremented and prevent the data from being freed or dropped, but that seems
just like Box, right?
Nope.
Let's consider a simplified implementation of Rc:
```rust
struct Rc<T> {
ptr: *mut RcBox<T>,
}
struct RcBox<T> {
data: T,
ref_count: usize,
}
impl<T> Rc<T> {
fn new(data: T) -> Self {
unsafe {
// Wouldn't it be nice if heap::allocate worked like this?
let ptr = heap::allocate<RcBox<T>>();
ptr::write(ptr, RcBox {
data: data,
ref_count: 1,
});
Rc { ptr: ptr }
}
}
fn clone(&self) -> Self {
unsafe {
(*self.ptr).ref_count += 1;
}
Rc { ptr: self.ptr }
}
}
impl<T> Drop for Rc<T> {
fn drop(&mut self) {
unsafe {
let inner = &mut ;
(*self.ptr).ref_count -= 1;
if (*self.ptr).ref_count == 0 {
// drop the data and then free it
ptr::read(self.ptr);
heap::deallocate(self.ptr);
}
}
}
}
```
This code contains an implicit and subtle assumption: ref_count can fit in a
`usize`, because there can't be more than `usize::MAX` Rcs in memory. However
this itself assumes that the ref_count accurately reflects the number of Rcs
in memory, which we know is false with mem::forget. Using mem::forget we can
overflow the ref_count, and then get it down to 0 with outstanding Rcs. Then we
can happily use-after-free the inner data. Bad Bad Not Good.
This can be solved by *saturating* the ref_count, which is sound because
decreasing the refcount by `n` still requires `n` Rcs simultaneously living
in memory.
10 years ago
## thread::scoped::JoinGuard
10 years ago
The thread::scoped API intends to allow threads to be spawned that reference
data on the stack without any synchronization over that data. Usage looked like:
```rust
let mut data = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
{
let guards = vec![];
for x in &mut data {
// Move the mutable reference into the closure, and execute
// it on a different thread. The closure has a lifetime bound
// by the lifetime of the mutable reference `x` we store in it.
// The guard that is returned is in turn assigned the lifetime
// of the closure, so it also mutably borrows `data` as `x` did.
// This means we cannot access `data` until the guard goes away.
let guard = thread::scoped(move || {
*x *= 2;
});
// store the thread's guard for later
guards.push(guard);
}
// All guards are dropped here, forcing the threads to join
// (this thread blocks here until the others terminate).
// Once the threads join, the borrow expires and the data becomes
// accessible again in this thread.
}
// data is definitely mutated here.
```
In principle, this totally works! Rust's ownership system perfectly ensures it!
...except it relies on a destructor being called to be safe.
```
let mut data = Box::new(0);
{
let guard = thread::scoped(|| {
// This is at best a data race. At worst, it's *also* a use-after-free.
*data += 1;
});
// Because the guard is forgotten, expiring the loan without blocking this
// thread.
mem::forget(guard);
}
// So the Box is dropped here while the scoped thread may or may not be trying
// to access it.
```
10 years ago
10 years ago
Dang. Here the destructor running was pretty fundamental to the API, and it had
to be scrapped in favour of a completely different design.
10 years ago
[uninit]: uninitialized.html