Skip to content

ACP: Add core::mem::WithDrop to associate a custom drop fn with some data #622

@orlp

Description

@orlp

Proposal

Problem statement

It is a common need to require something to run when a value gets destroyed. For example to clean up some resource, or to restore some invariant. This is especially important when writing panic-safe code.

Rust has a tool in the language for this purpose: the Drop trait. This allows you, for a specific type, to specify what should happen when a value of this type gets destroyed. However, this only allows one such implementation per type. This leads to a common but unwieldy pattern where one defines a dummy type wrapping some data for the sole purpose of adding a Drop implementation to the data.

This pattern gets doubly annoying when generics are involved, as an inner struct definition may not access the generics of its surroundings, forcing you to repeat all those again.

Motivating examples or use cases

There are many such cases of this, even in the standard library. For example in library/alloc/src/collections/btree/mem.rs:

struct PanicGuard;
impl Drop for PanicGuard {
    fn drop(&mut self) {
        intrinsics::abort()
    }
}
let guard = PanicGuard;
...
mem::forget(guard);

Or compiler/rustc_serialize/src/opaque.rs:

struct SetOnDrop<'a, 'guarded> {
    decoder: &'guarded mut MemDecoder<'a>,
    current: *const u8,
}
impl Drop for SetOnDrop<'_, '_> {
    fn drop(&mut self) {
        self.decoder.current = self.current;
    }
}

Or library/std/src/sys/pal/unix/sync/condvar.rs:

struct AttrGuard<'a>(pub &'a mut MaybeUninit<libc::pthread_condattr_t>);
impl Drop for AttrGuard<'_> {
    fn drop(&mut self) {
        unsafe {
            let result = libc::pthread_condattr_destroy(self.0.as_mut_ptr());
            assert_eq!(result, 0);
        }
    }
}

Or an example of repeating generics from library/alloc/src/vec/drain.rs:

struct DropGuard<'r, 'a, T, A: Allocator>(&'r mut Drain<'a, T, A>);

impl<'r, 'a, T, A: Allocator> Drop for DropGuard<'r, 'a, T, A> {
    fn drop(&mut self) {
        // ...
    }
}

Solution sketch

I propose we add a parallel to the core::mem::ManuallyDrop type to control the drop implementation of a specific value, rather than a type. This type which I propose we call WithDrop (also in core::mem) is similar to ManuallyDrop, but rather than foregoing the drop it calls a specific function on Drop with the value instead. To be concrete:

#[derive(Clone)]
pub struct WithDrop<T, F: FnOnce(T) = fn(T)> {
    value: ManuallyDrop<T>,
    on_drop: ManuallyDrop<F>,
}

impl<T, F: FnOnce(T)> WithDrop<T, F> {
    /// Wrap a value so that it is passed to `on_drop` when this `WithDrop` is dropped.
    #[must_use]
    pub const fn new(value: T, on_drop: F) -> Self {
        Self {
            value: ManuallyDrop::new(value),
            on_drop: ManuallyDrop::new(on_drop),
        }
    }
    
    /// Return the contained value without calling `on_drop`.
    pub fn into_inner(self) -> T {
        unsafe {
            let mut slf = ManuallyDrop::new(self);
            let ret = ManuallyDrop::take(&mut slf.value);
            ManuallyDrop::drop(&mut slf.on_drop);
            ret
        }
    }
}

impl<T, F: FnOnce(T)> Drop for WithDrop<T, F> {
    fn drop(&mut self) {
        unsafe {
            let val = ManuallyDrop::take(&mut self.value);
            let on_drop = ManuallyDrop::take(&mut self.on_drop);
            on_drop(val);
        }
    }
}

impl<T, F: FnOnce(T)> Deref for WithDrop<T, F> {
    type Target = T;

    #[inline(always)]
    fn deref(&self) -> &T {
        &self.value
    }
}

impl<T, F: FnOnce(T)> DerefMut for WithDrop<T, F> {
    #[inline(always)]
    fn deref_mut(&mut self) -> &mut T {
        &mut self.value
    }
}

impl<T: fmt::Debug, F: FnOnce(T)> fmt::Debug for WithDrop<T, F> {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_tuple("WithDrop").field(&self.value).finish()
    }
}

With this type the above patterns are easy to implement. For example:

let abort_on_panic = WithDrop::new((), |_| intrinsics::abort());
...
abort_on_panic.into_inner();

Because WithDrop implements Deref and DerefMut you can still easily access the underlying data:

let mut file = std::fs::File::create("foo.txt")?;
...
{
    let mut f = WithDrop::new(&mut file, |f| f.flush().unwrap());
    write!(f, "important data")?;
    may_panic();
    write!(f, "more important data")?;
}

Alternatives

There are two main alternatives to this proposal. One is some kind of defer keyword, e.g. discussed here. That would require full-on language support, and is also limited to strictly scoping-based cleanup. The latter means that even if we later end up accepting some kind of defer language mechanic in Rust, WithDrop isn't necessarily obsolete as it allows moving the entire WithDrop around to do non-scoped per-value cleanup.

The second alternative is simply for a user to implement the above, or use one of the crates that provides this or something similar. The most popular crate providing something similar is the scopeguard crate. While acceptable to some both feel unsatisfying to me. Adding a custom dummy Drop struct is annoying but just isn't annoying enough for me to justify adding a dependency on a microcrate. I personally feel if the need is fairly universal† and the implementation simple and uncontroversial it should be part of the standard library.

†The scopeguard crate has 385 million downloads all-time. It is in the top 50 most downloaded crates of all time.

While this proposal is inspired by scopeguard, I simplified it to its core: simply associating a drop function with some data. I did not include the macros it contains or the panicking strategies it exposes. I find the latter simply unnecessary as you can call std::thread::panicking yourself in the drop handler to do something different based on whether or not we're unwinding. To be specific for the former, scopeguard also contains the defer! macro (and variants for panicking strategies), which is (paraphrased) defined as

macro_rules! defer {
    ($($t:tt)*) => {
        let _guard = WithDrop::new((), |()| { $($t)* });
    };
}

While it is able to solve some of the same problems as this proposal I believe a macro like this is less flexible than the type (it's limited to the current scope, might run into borrowing issues, can't cancel the defer like WithDrop::into_inner can), and clashes with a potential future defer language feature. Should the defer language feature ever officially be considered and properly denied I wouldn't be fully opposed to adding it in addition to WithDrop, but I think it makes for a poor alternative.


As for alternatives within this proposal, I considered the names CustomDrop or OnDrop as well. I preferred WithDrop, but I'm open to suggestions.

Links and related work

Defer discussion: https://internals.rust-lang.org/t/a-defer-discussion/20387
scopeguard: https://docs.rs/scopeguard/latest/scopeguard

Metadata

Metadata

Assignees

No one assigned

    Labels

    ACP-acceptedAPI Change Proposal is accepted (seconded with no objections)T-libs-apiapi-change-proposalA proposal to add or alter unstable APIs in the standard libraries

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions