Overriding rustc's automatic type dropping


#1

I have a language feature idea that developed out of a need to make a type always live for as long as possible. I don’t know if the idea is smart and useful, or of the code pattern I used that made this seem desirable is Bad, Actually, and I should structure my own library in such a way as to never need this feature. Anyway.

For a project of mine, I started using the pattern where some open_resource() function returns a handle, whose only job is to call close_resource() when it falls out of scope. Kind of like file objects, except the handle has no way to actually interact with it, so the user might never think to assign the handle to a variable. And that causes the handle to Drop immediately.

As an example, imagine a register_some_callback function that takes a closure, somehow registers it in an event system across an FFI boundary, then returns a CallbackHandle struct whose Drop trait unregisters that same closure from the foreign library’s event system.

let handle = register_some_callback(|| {
    println!("this works great!");
    println!("at the end of this scope, a Drop impl unregisters this callback");
});

register_some_callback(|| {
    println!("this handle gets dropped immediately! :(");
    println!("Definitely not what the programmer intended");
});

And I thought it would be handy if we were able to annotate return values, or the types themselves, with an attribute that prevents being automatically dropped like this?

Now we do have the option of annotating a type with #[must_use] but that only produces a warning, and there’s still a corner case it doesn’t hit.

let handle = register_some_callback(|| {
    println!("works as intended!");
    println!("Though we'll likely get a warning: 'handle not used, consider using _handle'");
    println!("Because handle has no methods to actually use.");
});

register_come_callback(|| {
    println!("Return value not used! Warning works as intended.");
    println!("Though the code is still broken if the programmer ignores it...");
});

let _handle = register_some_callback(|| println!("No warnings, just right. :)"));

let _ = register_some_callback(|| {
    println!("Return value is immediately dropped! AND no warning is produced.");
    println!("Maybe the programmer thought this has the same semantics as assigning to _handle?");
});

Thus, I came to the idea that an annotation like #[no_auto_drop] would be handy for types whose sole purpose is to live its longest possible scope, then presumably Drop with some side effect whose ordering matters.

I looked at the ManuallyDrop trait, and that seems more geared toward giving authors precise control over drop ordering, whereas this is about relieving authors from needing to care whether or not a type’s Drop trait affects their program.

Is this worth drafting up an RFC for? If so, I’d like to, as well as work on the feature. I’ve done a cursory look through rustc code, and I think I’m capable of implementing it with some mentoring.


#2

What you suggest can cause memory leaks when incorrectly used. Worse, the API seems to encourage it in the same way a C API does. The difference is, C has the excuse of being predominantly manually memory managed, and Rust does not. An argument could thus be made that it is a bad API style for Rust.

Moreover, I don’t think that a Drop impl can affect anything other than the data contained in the object being dropped, so I don’t see how the whole unregister-on-drop business would work.

Unless of course the object effectively has a channel’s Sender as a field, and sends a msg at Drop time. But by that point multithreading is just a simple spawn invocation away, so you can a new thread to store the registry and send messages over channels to the new registry thread in order to manipulate it. This would be safe, wouldn’t require new language-level features, and from over here it seems like it might work for you (aside from the FFI issue, which can be worked around with a C API on top of your Rust code).


#3

No, I think this pattern is useful. It’s exactly the pattern that slog_scope::set_global_logger uses. It’s a niche use of a interaction-less RAII guard, but it has its uses for when you do have some state you want to clean up.

To clarify: the proposed attribute would only do the following semantic transformation:

let _ = annotated_fn();
// to
let _unnamable = annotated_fn();

that is, make it so that let _ defers the drop from running until end of scope instead of after this line (for invokers of the annotated function)?

That said, it feels too unimpactful to be worth the complexity of adding it to the language.


No, I think you’ve misunderstood. It’s a normal RAII guard type being described. The annotation only serves to delay the drop to when it would be dropped if it were bound, even if it isn’t. It doesn’t cause any more leak possibilities than any other RAII type; in fact, I’d say it makes it less leak prone, as you can’t mem::forget something you never bound to a namable name.


#4

I see. It seems that I have misunderstood then.

However, wouldn’t the same niche be filled if it were possible to put arbitrary an Dropable value into a top-level static declaration? So for example something like this:

// Any use statements here

struct Foo { 
  // heap-allocated, droppable data
}
impl Drop for Foo {
  fn drop (&mut self) {
    // .. 
  }
}

static foo: Foo = Foo { 
  // ..
};

To date IIRC this kind of code is invalid because (top-level?) statics don’t seem to like heap-allocated data. But what if that were changed? Wouldn’t that solve this issue as well?

Intuitively I’ve wanted to be able to put arbitrary data into a static to store data that should live for the lifetime of the entire program, but for whatever reason cannot be calculated at compile time (because in that case, it usually seems more optimal to do that). So far Rust has prevented that, and I’m not sure I ever found a good motivator for that. So perhaps it’s time to revisit that.


#5

Technically, this is already possible: https://github.com/rust-lang/rust/issues/33156

Yeah, I think this is the actual issue.

I have seen some discussion in the past around manipulating heap-allocated types in const fns and then allowing that “compile time heap” to “leak” into runtime statics. As far as I know, there’s no reason in principle why that cannot or should not be done, it’s just an open design question of what the right way to do it is, and we probably want to get Vec working in const fn at all before discussing how to “leak” it.


#6

This is related to a feature I suggested a while back; a special “unbinding” expression. You can ostensibly do something like this:

macro_rules! erase {
  ($x:ident) => { 
    let $x = {
      struct Unnameable;
      Unnameable
    };
    drop(x);
  }
}

To make this nicer, you might imagine some magic builtin let x = erase!(); which sets x back to its “undefined” state, so you get a “variable not defined” instead of “variable moved from” error. (For completeness, if x is a general pattern, all bindings in the pattern are unset.)

withoutboats’ toy GC uses a similar trick, to ensure that a root cannot be accessed for the duration of the region it lives for.

Of course, what it sounds like we want here is some magic type StackPin<T>: Deref<Target=T> which somehow cannot be moved, and such that any occurence of StackPin::new in an expression is semantically hoisted into an unnameable let before that expression. Unfortunately, this does have an annoying problem:

f(StackPin::new(a), StackPin::new(b));
// which desugaring is correct?

let __a = StackPin::new(a);
let __b = StackPin::new(b);
f(__a, __b)

let __b = StackPin::new(a);
let __a = StackPin::new(b);
f(__a, __b)