Prevent drop at compile time

I want to be able to express that a value of a certain type should never be dropped. One possible motivation is that the cleanup procedure requires error handling. Another motivation is that you want to release multiple resources in bulk (which is the case in my example).

To enforce this, you can panic in the Drop implementation like so:

impl Drop for BufferName {
    fn drop(&mut self) {
        // Only panic if not already panicking. Solve problems one by
        // one, don't create more of them.
        if thread::panicking() == false {
            panic!("Forgot to free {:?}", *self)
        }
    }
}

This is however a run-time solution and I’m curious if there is a compile time solution. Something along the lines of a “prevent_drop” attribute on my type. That would help library users discover un-dropped values at compile time instead of after writing and executing extensive tests.

I do realize this is very niche and probably only works when your type is wrapped in an Option so you can take and forget or something similar.

Full example: https://play.rust-lang.org/?gist=851840ae92e0a9e88daa9f142143931f&version=nightly&mode=debug&edition=2018. Important parts are the tests forget_to_free and gen_and_drop.

1 Like

If you want to prevent drop being called at all, there’s Box::leak.

If you want drop to be called, but only on your terms, then Rust doesn’t have such functionality.

3 Likes

I read this as “I want the compiler to check” if there’s a path to drop, excluding panic or exit. A classic “does not live long enough” error, for a specific lifetime circumstance or requirement.

Prior to yesterday, I might have said something about the 'static lifetime, but now what little I knew about lifetimes is all wrong, and this one is called out explicitly as not actually meaning this.

This facility would indeed be nice, for many reasons. It’s tracked here (or at least I couldn’t find anything more relevant).

1 Like

How critical is it that it’s a hard error? Would an opt-in lint suffice, must_use-style?

1 Like

To me it looks like most use cases are requiring you to call some function that prevents drop from running. A lint would help but not complying means the program will not function, so a hard error would make more sense.

Thanks to a friend who pointed me to incomplete (which currently allows me to safely create undef values :/) I arrived on this thread where @jugglerchris suggested using a link error. It looks promising:

use std::num::NonZeroU32;

pub struct BufferName(NonZeroU32);

impl BufferName {
    pub unsafe fn new(value: u32) -> Option<Self> {
        NonZeroU32::new(value).map(BufferName)
    }

    pub fn as_u32(&self) -> u32 {
        self.0.get()
    }
}

extern "C" {
    fn prevent_drop_BufferName();
}

impl Drop for BufferName {
    #[inline]
    fn drop(&mut self) {
        unsafe {
            prevent_drop_BufferName();
        }
    }
}

#[test]
fn test() {
    let n = unsafe { BufferName::new(1).unwrap() };
    assert_eq!(n.as_u32(), 1);
    ::std::mem::forget(n);
}

compiles but of course only with optimizations!

Playground

I expect the compiler to elide the drop call in any scenario but I have yet to confirm belief. In the case that drop does get called, the link error tells you what type was being dropped but finding the place where it gets dropped can be a little more challenging.

error: linking with `cc` failed: exit code: 1
  |
  = ...
  = note: /playground/target/release/deps/playground-014d023c6d9e2470.playground4-c73c0d6f28ac447285153be845763aee.rs.rcgu.o: In function `core::ops::function::FnOnce::call_once':
          playground4-c73c0d6f28ac447285153be845763aee.rs:(.text._ZN4core3ops8function6FnOnce9call_once17h9a51fd5e3c078478E+0x1): undefined reference to `prevent_drop_BufferName'
          collect2: error: ld returned 1 exit status

Here is a macro to generate the drop prevention code, can be extended to use a run time method if the code is not being optimized. Didn’t do that to keep it straightforward.

macro_rules! prevent_drop {
    ($T:ty, $f:tt) => {
        extern "C" {
            fn $f();
        }

        impl Drop for $T {
            #[inline]
            fn drop(&mut self) {
                unsafe {
                    $f();
                }
            }
        }
    }
}

Usage:

prevent_drop!(BufferName, prevent_drop_BufferName);

I’ve put the macro in a crate https://github.com/mickvangelderen/prevent_drop but want to use it in more complex code before I publish it.