(Pre-)RFC for ExplicitDrop

I have a language features / stdlib thing that I'm thinking of proposing an RFC for, but would like to get some feedback on.

The problem is; I have some resource that don't live on the CPU timeline (they live on a GPU timeline); so using RAII or rust lifetime tracking in general makes relatively little sense for them. The concept of a "scope" in rust doesn't apply to them.

Right now these resources are explicitly free'd through some mechanism; however, they can't implement Drop properly (because scope doesn't apply & deleting the resource in Drop would be a race condition with the GPU).

The proposal is relatively straight forward; have a trait like ManuallyDrop , called ExplicitDrop which would invoke a compile error whenever the compiler would've otherwise dropped the object (e.g. I can only move it around, and take borrows of it). To get rid of the object and to clean it up, an explicit call to drop would be needed.

2 Likes

Sounds like linear types. I suggest reviewing the previous work on that.

(for example, https://gankra.github.io/blah/linear-rust/)

7 Likes

I wonder if the link failure trick that https://docs.rs/no-panic/0.1.13/no_panic/ uses could also be applied to destructors while still permitting some kind of explicit drop. Has anyone ever messed with that?

(regarding "deep" or "first-class" support for must-use-once/linear types, I still basically agree with that classic Gankra post that it's more trouble than it's worth, but "shallow" options that are just a notch up from ManuallyDrop may be worth considering)

1 Like

That post highlights the problem quite nicely, and references the solution I'm currently thinking of implementing (assert / panic in Drop) which is a bit less then ideal.

Yup, here.

4 Likes

There’s a micro crate with cool readme for that: https://github.com/matklad/drop_bomb

Though, most of the time it makes sense to just write the thing yourself.

1 Like

So it looks like there's a need for this, both from this thread, talking to a few other developers and the fact that some crates already exist to address this need. For example: I found the drop_bomb during already during research for this, as well as https://crates.io/crates/humpty_dumpty which tries to do a similar thing as a linting extension.

It looks like the community already has put some thought behind this, are there any existing RFC's that I should be aware of for similar things?

Like Jasper, I've also hit this problem, while working with the Vulkan GPU API.

I implement drop traits everywhere I can. But some things can only have one valid instance (for instance, the swapchain). Here, it is impossible to use drop traits, because you would need to create a new instance before the older one can be dropped.

The alternative, of course, is to wrap the swapchain in an Option, and then set it to None, forcing a drop. This feels feels too icky, because IMO it diverges from the semantic meaning of an Option, and because you have to unwrap it everywhere.

I just end up defining a destroy() function, and having to remember to manually call it. It would be useful to get a compiler error if I forget to destroy these resources.

To that end, Jasper's proposal sounds interesting. I'm looking forward to seeing where it goes.

3 Likes

This is going to cause problems with error handling:

fn foo() -> Result<(), QuxError> {
    let bar = SomeExplicitDropType::new();
    bar.baz(qux()?);
    bar.explicit_drop();
    Ok(())
}

If qux() returns an error, bar will not be explicitly dropped - so the compiler will not allow it. Any usage of ? in the presence of ExplicitDrop types will fail to compile.

I think this is fine, (these are the exact cases we want to fail to compile). Rewritting these portions shouldn't be that much of a burden, esp if it's only a small portion of your codebase.

How will this interact with putting these types in other types? i.e.

struct Foo;
struct Bar(Foo);

struct ExplicitDrop for Foo {
    fn explicit_drop(self) {}
}

fn bar(b: Bar) {
    // what happens here?
}

How about generic functions?

fn generic<T>(t: T) {}

fn foo() {
    generic(Foo);
}

fn bar() {
    generic(Bar(Foo));
}
1 Like

I suppose what is needed for proper generics while keeping backwards compatibility is that actually “implicit drop” capability becomes a trait (ImplicitDrop, say -- the blog post on linear types calls it Leave), which is however automatically put into generic bounds, much like Sized is. This would still require any generic function that wants to support the new types without implicit drop capabilities to update its type parameters to T: ?ImplicitDrop; that’s a backwards-compatible change, too.

Furthermore, this trait would probably best be an auto trait so that structs containing any !ImplicitDrop fields are themselves !ImplicitDrop.

2 Likes

A point not mentioned here yet is: How is this supposed to interoperate with panics? Usually during stack unwinding, everything gets dropped. I can thing of several approaches:

  • It is okay for an “explicit drop”-value to be dropped in a panic, since panics are special
  • It is only okay to abort, since panics could be caught. Enforcing this might be best done with some sort of static analysis on whether panics might be possible.
  • An “explicit drop”-value on the stack in a function makes it implicitly convert all panics going through it into aborts. (Much like if the drop implementation panics.)

I guess the last option might be the best one, since it fits into the picture of what currently happens when a Drop impl panics.

1 Like

Yes, the last option seems best.

Doing this as an auto trait would make a lot of sense; as does flipping it to ImplicitDrop.

In this case bar would need to drop the Foo in Bar explicitly.

I'm not sure about the semantic differences between panic and `abort, but I think your last option might be the right one - IMHO it's fine to rely on OS cleanup for these things but I don't know the Rust policy on this.

I would like to throw in the question whether an even better approach would be not to require calls to drop to be explicit but instead disallow calling drop (implicitly or explicitly) entirely. This would mean that the only way to get rid of a value is destructuring. Additionally, explicit drop could be allowed in any place where destructuring is possible (due to all the fields being visible). Going this route, even implicit drops may be allowed when all the fields are visible (which is usually only possible internally in the implementation of the type). In the last case, we should disallow (or warn about) types being !ImplicitDrop even though all their fields are (globally) public and themselves ImplicitDrop, since in this case an implicit (or explicit) drop would actually still be possible. [With an auto trait ImplicitDrop this means that the negative !ImplicitDrop impl is explicit; the error (or warning) would point to that impl.]

Note that with the changes in the previous paragraph, the name ImplicitDrop wouldn’t make any sense anymore, since the trait would be about whether the type is droppable at all.

Another thing I’ve just noticed: Logically ImplicitDrop would have to be a supertrait of Copy. I’m not sure about the backwards compatibility issues around actually making it a supertrait.

I have a feeling this may be a bit overkill, of have implications that are harder to foresee, do you have any examples of what this would look like? I'm trying to gouge what kind of impact this would have on my particular example.

For this use-case you can use ManuallyDrop, linear types are not required.

You could even make a safe wrapper for this pattern using procedural macros. (Something similar to how PinnedDrop works from the pin_project crate)

But destructuring is also possible with private fields with "..".

Good point. At least as long as they don’t implement Drop.

I think what we need in this discussion is some concrete examples to find out which approach is best.

May you be able to make your example more concrete? I’m actually not quite sure how exactly you envisioned your ExplicitDrop to be used in the first place. In particular I don’t know if such a type explicitly implements Drop or not and if yes, how, and what an explicit call to drop would do, etc..

I've created a playground for it (that obviously doesn't run) https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=a20b087f8ab289a3ecfc00609cf38c5d in this particular case, the expectation would be that MyObject derives ExplicitDrop.