Require statically droppable but not `'static`?

I'm trying to build a concurrent data structure, and I'd like to lazily free a generic object of type T at some point within lifetime 'lazy that may outlive the lifetime of T itself. For simplicity let's define 'lazy as 'static: the type T stored in the data structure must be droppable at any distant point in time far in the future. However I still want to support non-static references like T = &'a str.

One approach I thought of was creating two methods, one which requires T: 'static, and another which accepts any lifetime but does assert!(!std::mem::needs_drop::<T>());. However this would reject things like Box<&'a str>, which should be ok to drop even after 'a ends (right?).

I'm wondering if there is some way I can express this in a trait bound? In other words, I'd like to express that the type doesn't need to be 'static, but all generic parameters in any drop glue must be #[may_dangle], if that makes sense?

1 Like

Nothing that contains a reference of type &'a str must be kept around for longer than 'a. If you somehow make a Box that violates that constraint, you'll have UB.

1 Like

I'm not sure how to interpret your problem. lifetimes in rust are type level information which only exist at compile time, but the "laziness" you described seems some kind of runtime behavior: it reminds me of finalizers in managed languages, are you making some kind of garbage collector?

that's incorrect. the type system will ensure Box<&'a str> will NOT outlive 'a. if you somehow managed to get a Box<&'a str> that outlives 'a, you must have executed some unsound code and it's already UB.

EDIT:

thanks to @alice comment below, now I think about it, the box can indeed outlive the inner type's lifetime, unless the inner type has custom drop glue (which &str doesn't), and after the inner lifetime ends, although technically the box is still alive, you cannot do anything with it, other than the implicitly dropp when the box reaches the end of the scope. (e.g., you cannot manually call drop() on the box after the inner lifetime ends, after all, the drop() function is just a regular function which actually moves its argument).

fn good() {
	let boxed_ref: Box<&'_ str>;
	{
		let s = String::from("hello");
		boxed_ref = Box::new(&s);
	} //<-- lifetime of `s` ends here
	// technically the box is still alive at this point
	// you can have other code here, but you cannot access the box
} //<-- box dropped here, implicitly

fn bad() {
	let boxed_ref: Box<&'_ str>;
	{
		let s = String::from("hello");
		boxed_ref = Box::new(&s);
	} //<-- lifetime of `s` ends here
	// the next line causes compile error
	drop(boxed_ref); //<-- last USE of `boxed_ref` here
}

// in order for the next to compile, `Drop` implementation for `SomeWrapperType`
// must also follow `Box`, i.e. `unsafe impl` with `#[may_dangle]`
fn possibly_bad() {
	let boxed_ref: Box<SomeWrapperType<'_>>;
	{
		let s = String::from("hello");
		boxed_ref = Box::new(SomeWrapperType::new(&s));
	} //<-- lifetime of `s` ends here
} //<-- box dropped here

The other comments are wrong. The Box type uses the unstable #[may_dangle] annotation, so the compiler will sometimes accept code where Box::drop runs when the reference may be dangling.

That said, I do not think you should attempt to make use of that feature. Instead, I propose that you add a DelayedDrop trait that lets you convert values into a "delayed droppable wrapper" that is 'static and has an unsafe method that returns a reference to the real non-static type. Then, you store that wrapper instead of the Box<&'a str> and use unsafe any time you wish to access the value and know that the lifetime has not expired. When you wish to drop it, you drop the wrapper like normal.

You can then implement that trait for Box<T> where T: Copy. That will cover your reference, and copy types never need drop.

And you might also want to add a method that lets you use your thing with any 'static type, regardless of the trait.

3 Likes

Thanks to everyone for the very insightful answers!

@alice This makes a lot of sense. At this point the biggest question remaining is, is there a way I can do this that supports both &str and String? I am really dreading the possibility of needing to create duplicate functions rather than a single trait bound; things can start to explode really quick when you need a Cartesian product of generic parameters (e.g. consider ConcurrentHashMap<K, V>, we want K and V to support any combination of String and &str).

It's impossible to provide separate impls for T: Copy and T: 'static since the impls may overlap, but perhaps it's possible to get around this via wrapper types? Not happy with the complexity but it's flexible:

pub unsafe trait StaticDrop {}

#[derive(Clone, Copy)]
pub struct CopyWrap<T: Copy>(T);
unsafe impl<T: Copy> StaticDrop for CopyWrap<T> {}

#[derive(Clone, Copy)]
pub struct StaticWrap<T: 'static>(T);
unsafe impl<T: 'static> StaticDrop for StaticWrap<T> {}

unsafe impl<T> StaticDrop for &T {}
unsafe impl<T> StaticDrop for &mut T {}
unsafe impl<T: StaticDrop> StaticDrop for Box<T> {}
unsafe impl<T: StaticDrop> StaticDrop for Arc<T> {}
unsafe impl<T: StaticDrop> StaticDrop for Rc<T> {}
unsafe impl<T: StaticDrop> StaticDrop for Cell<T> {}
unsafe impl<P: StaticDrop> StaticDrop for Pin<P> {}

struct ConcurrentHashMap<K: StaticDrop, V: StaticDrop> { /* ... */ }

The most unfortunate part of this approach is that you'd need something like ConcurrentHashMap<CopyWrap<i32>, CopyWrap<i32>> for the common case... another approach could be:

pub unsafe trait StaticDrop {}

#[derive(Clone)]
pub struct StaticWrap<T: 'static>(T);

#[derive(Clone)]
pub struct NoCopy<T>(pub T);

unsafe impl<T: Copy> StaticDrop for T {}                 // e.g. i32, &str
unsafe impl<T: 'static> StaticDrop for StaticWrap<T> {}  // e.g. StaticWrap<String>
// unsafe impl<T> StaticDrop for &mut T {}   // error, despite `impl !Clone for &mut T`
unsafe impl<T> StaticDrop for NoCopy<&mut T> {}
unsafe impl<T: StaticDrop> StaticDrop for NoCopy<Box<T>> {}
unsafe impl<T: StaticDrop> StaticDrop for NoCopy<Arc<T>> {}
unsafe impl<T: StaticDrop> StaticDrop for NoCopy<Rc<T>> {}
unsafe impl<T: StaticDrop> StaticDrop for NoCopy<Cell<T>> {}
unsafe impl<P: StaticDrop> StaticDrop for NoCopy<Pin<P>> {}

This could also be an auto trait (or, more stably, have a derive macro). Do you know of a better/less complex way to do this? Perhaps it is overkill to try and support this anyway. Part of me really just wants to require T: 'static and call it a day.

Edit: maybe #![feature(overlapping_marker_traits)] would help, but it's unclear if this will ever be stabilized.

1 Like

No, my reply isn't "wrong", you are just misunderstanding it. I specifically wrote:

When you deref-move out of a Box, it no longer contains its (former) inner value (regardless of its underlying allocation not having been deallocated). If you somehow do manage to create a Box (or any other container/binding) that thinks it's still got an inner value, but that inner value was in fact invalidated in the meantime, that's UB. #[may_dangle] doesn't change anything about this fact.

Proof.

I think in your example the primary source of unsoundness is the read from self.value in a destructor where its type is marked #[may_dangle]? My belief was that calling std::ptr::drop_in_place on a value whose lifetime has expired is ok, though in general it's still not permissible to read that value or otherwise reference the memory or any of its contents, except via raw pointers. Am I understanding this correctly?

Edit: to be clear, I mean "std::ptr::drop_in_place on a value whose lifetime has expired is ok" if that value has no drop glue.

Edit again: it is really hard to express this precisely. The goal of what I'm trying to do is: drop an object of type T after its lifetime has expired. And I am trying to figure out how to express (as a trait bound) the set of objects for which that is safe to do. It is true for:

  • Objects with 'static lifetime since their lifetime never expires
  • Objects that implement Copy since they have no drop glue at all
  • Types like Arc<T>, whose destructor has #[may_dangle], and yet still soundly drops a value of type T even though T's lifetime may have ended. When I say T's lifetime may have ended, I mean for example T may contain references that are now dangling.

@paramagnetic No, your reply really is wrong. Consider this example:

struct PrintOnDrop(&'static str);

impl Drop for PrintOnDrop {
    fn drop(&mut self) {
        println!("dropping {}", self.0);
    }
}

fn main() {
    let boxed;
    
    {
        let value = PrintOnDrop("value");
        boxed = Box::new((&value, PrintOnDrop("value two")));
    }
    
    println!("After block");
}
dropping value
After block
dropping value two

In this example, when value two is dropped, the reference in the tuple is already dangling. There is no deref-move out of the box. The reason this compiles is that Box is annotated with #[may_dangle].

As for your proof, what goes wrong is that #[may_dangle] destructors may not use the value in any way other than calling its destructor. You're calling into its Debug implementation, which is not allowed and against the rules for #[may_dangle].

3 Likes

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.