Forcing the order in which struct fields are dropped
Asked Answered
H

1

19

I'm implementing an object that owns several resources created from C libraries through FFI. In order to clean up what's already been done if the constructor panics, I'm wrapping each resource in its own struct and implementing Drop for them. However, when it comes to dropping the object itself, I cannot guarantee that resources will be dropped in a safe order because Rust doesn't define the order that a struct's fields are dropped.

Normally, you would solve this by making it so the object doesn't own the resources but rather borrows them (so that the resources may borrow each other). In effect, this pushes the problem up to the calling code, where the drop order is well defined and enforced with the semantics of borrowing. But this is inappropriate for my use case and in general a bit of a cop-out.

What's infuriating is that this would be incredibly easy if drop took self instead of &mut self for some reason. Then I could just call std::mem::drop in my desired order.

Is there any way to do this? If not, is there any way to clean up in the event of a constructor panic without manually catching and repanicking?

Hydride answered 9/12, 2016 at 5:15 Comment(1)
Can you give some example code please? It would be easier to formulate a suggestion with a starting point.Shalom
P
27

You can specify drop order of your struct fields in two ways:

Implicitly

I wrote RFC 1857 specifying drop order and it was merged 2017/07/03! According to the RFC, struct fields are dropped in the same order as they are declared.

You can check this by running the example below

struct PrintDrop(&'static str);

impl Drop for PrintDrop {
    fn drop(&mut self) {
        println!("Dropping {}", self.0)
    }
}

struct Foo {
    x: PrintDrop,
    y: PrintDrop,
    z: PrintDrop,
}

fn main() {
    let foo = Foo {
        x: PrintDrop("x"),
        y: PrintDrop("y"),
        z: PrintDrop("z"),
    };
}

The output should be:

Dropping x
Dropping y
Dropping z

Explicitly

RFC 1860 introduces the ManuallyDrop type, which wraps another type and disables its destructor. The idea is that you can manually drop the object by calling a special function (ManuallyDrop::drop). This function is unsafe, since memory is left uninitialized after dropping the object.

You can use ManuallyDrop to explicitly specify the drop order of your fields in the destructor of your type:

#![feature(manually_drop)]

use std::mem::ManuallyDrop;

struct Foo {
    x: ManuallyDrop<String>,
    y: ManuallyDrop<String>
}

impl Drop for Foo {
    fn drop(&mut self) {
        // Drop in reverse order!
        unsafe {
            ManuallyDrop::drop(&mut self.y);
            ManuallyDrop::drop(&mut self.x);
        }
    }
}

fn main() {
    Foo {
        x: ManuallyDrop::new("x".into()),
        y: ManuallyDrop::new("y".into())
    };
}

If you need this behavior without being able to use either of the newer methods, keep on reading...

The issue with drop

The drop method cannot take its parameter by value, since the parameter would be dropped again at the end of the scope. This would result in infinite recursion for all destructors of the language.

A possible solution/workaround

A pattern that I have seen in some codebases is to wrap the values that are being dropped in an Option<T>. Then, in the destructor, you can replace each option with None and drop the resulting value in the right order.

For instance, in the scoped-threadpool crate, the Pool object contains threads and a sender that will schedule new work. In order to join the threads correctly upon dropping, the sender should be dropped first and the threads second.

pub struct Pool {
    threads: Vec<ThreadData>,
    job_sender: Option<Sender<Message>>
}

impl Drop for Pool {
    fn drop(&mut self) {
        // By setting job_sender to `None`, the job_sender is dropped first.
        self.job_sender = None;
    }
}

A note on ergonomics

Of course, doing things this way is more of a workaround than a proper solution. Also, if the optimizer cannot prove that the option will always be Some, you now have an extra branch for each access to your struct field.

Fortunately, nothing prevents a future version of Rust to implement a feature that allows specifying drop order. It would probably require an RFC, but seems certainly doable. There is an ongoing discussion on the issue tracker about specifying drop order for the language, though it has been inactive last months.

A note on safety

If destroying your structs in the wrong order is unsafe, you should probably consider making their constructors unsafe and document this fact (in case you haven't done that already). Otherwise it would be possible to trigger unsafe behavior just by creating the structs and letting them fall out of scope.

Pollen answered 9/12, 2016 at 9:6 Comment(3)
Specifically for this case, the C values are likely to be pointers. That means you can also implement drop to free the pointers only if they are non-NULL. Then, set the pointers to NULL after freeing them, preventing double frees. It's the same idea as Option.Daffy
@Hydride just updated the answer to account for the developments of the language in the last seven months. I hope you find it useful :)Pollen
I just ran into a massive bug related to the field drop order. Rust's behavior is counter-intuitive, and the exact opposite of C++... that's a huge footgun.Mosra

© 2022 - 2024 — McMap. All rights reserved.