Use ManuallyDrop in Rust to control drop order of structure fields

Create: February 6, 2023

First, for some context: I wrote this blog post more than a year ago but have yet to make it public since my mental health greatly degraded while I was still editing it. Now that I want to go back into blogging, I decide to release this post in its current form, though I lost interest in editing it.

As programmers, "clean-up" always feels like the boring part of our code. Fortunately, in Rust, we destructors that help us to automatically write correct, ergonomic clean-up code. However, in the rare case where the order of destruction matters, compiler-generated destructor calls can be a hindrance.

In this post, I will show you how to use the Rust standard library ManuallyDrop type to explicitly control how and when structure fields get dropped.

The problem

Rust drops fields in the declaration order1. For example, considering the following structure:

struct Name {
    first: String,
    last: String,
}

In this case, first will be the first field to destroyed, followed by last.

This behavior, though unintuitive (especially if you come from a C++ background2), is fine for most of the time, as we programmers often don't care about exactly order of things get freed. For example, in the above example, there are no dependencies between first and last fields, so destroying either of them first is fine.

However, there are cases where destuction-order do matters. Unless we are ok to order fields in our struct in a gratuitous reverse order, it is probably not worth relying on the compiler-generated destruction order. Worse, in some instances, we may want to interleave user code execution and the fields being dropped inside the drop method, and the compiler's automatically generated destructor calls failed us here.

A Vulkan example

I met the problem when doing some Vulkan exploration in Rust via the excellent ash crate. I use another crate, gpu-allocator, to manage GPU memory.

The topic of this post is not about the Vulkan API. If you are not familiar with it, the only thing you need to know is that the time of destruction is very important in this API, and many entities in Vulkan have implicit dependencies. Before destroying anything in Vulkan, we need to ensure that the corresponding resource is not used on the GPU. Also, we can't destroy an object if any other objects depend on it.

It is common in Vulkan applications to have a Renderer structure like the following:

use gpu_allocator::vulkan::{Allocator};

pub struct Renderer {
    entry: ash::Entry,
    instance: ash::Instance,
    ...
    physical_device: vk::PhysicalDevice,
    device: vk::Device,
    ...
    allocator: Allocator,
    ...
}

Since the allocator depends on instance and device, those two must be destroyed after the allocator. On the other hand, certain other resources, such as buffers allocated by the allocator, need to be destroyed before the allocator gets destroyed.

The ash crate abandons using destructors and RAII all thgether but instead relies on manual resource managements inside unsafe blocks. In ash, we are expected to write code like the following:

unsafe {
    instance.destroy_instance(None);
}

Even though non-idiomatic in Rust, ash's approach is fine with me. RAII combines with the Vulkan API pretty poorly for multiple reasons 3. However, gpu_allocator, despite depending on the ash crate, decide to implement the Drop trait for its Allocator. And we are in a situation where we must call the destructor of the allocator in the middle of a chain of calls to ash's clean-up functions.

In C++, we just manually call the destructors when meeting such a situation, and everything will work fine. Can we do that in Rust? Here is a try:

impl Drop for Renderer {
    fn drop(&mut self) {
        // ...

        // ... All the allocation must be freed BEFORE the allocator get dropped
        self.allocator.free(allocation).unwrap();
        unsafe { self.device.destroy_buffer(self.buffer, None) };

        // We try to drop our allocator here
        self.allocator.drop();

        // device and instance must be destroyed AFTER the allocator
        unsafe {
            self.device.destroy_device(None);
            self.instance.destroy_instance(None);
        }
    }
}

However, Rust forbids explicit destructor calls, and the above program fails to compile. The Rust compiler then gives this "helpful" message:

consider using drop function: drop(self.allocator).

So let's try that:

impl Drop for Renderer {
    fn drop(&mut self) {
        // ...

        std::mem::drop(self.allocator);

        // ...
    }
}

Unfortunately, the above solution doesn't compile either, and it gives the "cannot move out of self.allocator which is behind a mutable reference" error. The reason is that the drop method takes self by reference, while std::mem::drop tries to borrow its field self.allocator. Even though we know that our renderer will get destroyed after the drop method, from the Rust type system's perspective, std::mem::drop is trying to "steal" one of its fields.

Using Option to circumvent the drop order

If you are an experienced Rust programmer, you have probably already come up with an idiomatic way to our problem: making the allocator field into an Option.

pub struct Renderer {
    ...
    allocator: Option<Allocator>,
    ...
}

Then when we want to drop self.allocator, we can assign it to None.

impl Drop for Renderer {
    fn drop(&mut self) {
        ...
        self.allocator = None;
        ...
    }
}

Alternatively, we can combine a std::mem::drop with a .take() call: std::mem::drop(self.allocator.take()).

The Option solution works, and many people will probably be happy with it. However, I am dissatisfied for several reasons. Probably most importantly, the usage of Option pollutes all the self.allocator usage with extra .unwrap() ceremony.

The Option usage here also violates Rust's "zero-overhead abstration" principle, as we add runtime overhead purely to satisfy the type system. Though I consider this point less important since the performance overhead of using Option here is neglectable in the large picture on things.

Introducing ManuallyDrop

std::mem::ManuallyDrop is a type wrapper that inhibits the Rust compiler from automatically calling the underlying type's destructor.

Let's apply ManuallyDrop to our allocator:

pub struct Renderer {
    ...
    allocator: ManuallyDrop<Allocator>,
    ...
}

What makes ManuallyDrop a much more ergonomic choice than Option is its implementation of Deref, which makes it possible for us to use methods of Allocator directly. A bonus point is that, unlike Option, ManuallyDrop doesn't add any performance overhead.

When we are done, we can use ManuallyDrop::drop to drop our field. This function specifically takes a mutable reference, so we won't have the same problem we faced with the standard drop function before:

impl Drop for Renderer {
    fn drop(&mut self) {
        ...
        unsafe {
            ManuallyDrop::drop(&mut self.allocator);
        }
        ...
    }
}

Notice that ManuallyDrop::drop is an unsafe function since it makes self.allocator in a "zombie" state that shouldn't be touched. But since the whole Renderer structure will no longer exist after being dropped, it is not likely to pose any problem in practice.

Drawback of ManuallyDrop

ManuallyDrop is perfect for the above use case, but it can cause some hassle when we can't automatically dereference it. This case will only happen on assignment, so if we need to assign a new value to a ManuallyDrop variable, we need to dereference it with * explicitly:

let mut x = ManuallyDrop::new(42);
*x = 55;

This slightly annoying syntax is only a minor issue, but it does break the illusion that ManuallyDrop<T> behaves exactly like the underlying type T. However, the annoyance can add up if we have a lot of fields of type ManuallyDrop, for example, when generating binding with C++ libraries. The good news is that there is a proposal that aims to provide a more ergonomic solution in the future.

Conclusion

ManuallyDrop is a seldom necessary utility in normal Rust code. Nevertheless, it is currently the ideal solution when we want explicit control of how structure fields get dropped.


  1. Destructors - The Rust Reference: "The fields of a struct are dropped in declaration order."
  2. C++ calls destructor in reverse declaration order order
  3. In Vulkan, we don't deleting an object until we are sure that the GPU is not using it. Deleting objects out-of-order is a big issue, and it may even crash your driver.