The current failurepanic semantics in Rust are rather bad – destructors get called for all objects, which may contain subobjects that were currently mutably borrowed and are in an invalid state.
This unfortunately means that every object that can be mutably borrowed must be in a safe state on every possibly-panicking method.
For example, when a PriorityQueue comparator fails, the queue is left in an invalid state, and further operations can potentially cause unsafety via out-of-bound array access.
Some people say that the best way to deal with this is to abolish unwinding. However, the situation isn’t as horrible as it seems, as the only reason to run destructors on failure is to release resources, and the kinds of resources that need manual release are (a) held by unsafe code and (b) traceable relatively easily, even through unwinding.
I would propose adding something an UnsafeFinalizer trait, that is unsafe to implement, and is run on panic in lieu of drop.
Interesting. I have but not quite in this direction. I simply wanted to expand the Drop trait to have both a “normal drop” method, as we do now, and a “drop on panic” method. The compiler would invoke the appropriate one. I don’t know why these methods have to be unsafe – as arielb1 suggests, the type may opt to do unsafe things within the drop trait, but it doesn’t seem inherently unsafe to distinguish your action on panic from the action on normal termination.
It might be better for compile time to have two traits, or at least give the compiler some way to distinguish when the action on normal flow is a no-op so it can avoid generating any code at all. I’m not sure how often that comes up. Certainly it does when creating resources, but often the normal path then involves a move anyway.
Is it possible to distinguish between these two cases in Rust, given that "normal" drops can be performed during unwinding too? C++ has some problems with it (link).
I guess it depends on what you mean by “normal” and “during unwinding”. My interpretation was that a distinct destructor would be executed whenever the scope is exited abruptly. The normal destructor would still be used for normal exits that occur during unwinding (e.g., during the execution of an abrupt dtor).
What is the distinction between Drop and UnsafeFinalizer?
I understand it that UnsafeFinalizers would be called from unsafe code and Drop from safe code. So Drop is used on consistent objects and UnsafeFinalizer on potentionally inconsistent objects? Is that correct?
Therefore UnsafeFinalizers has to be implemented only for objects that are inconsistent during unwinding? That would mean that objects that are correctly encapsulated and does not use unsafe code does have to implement only Drop?
This exact approach was rejected in C++ with an explanation "this could involve overheads on programs that did not use the feature, because the implementation would have to be ready to answer the query at any time" (see the paper). I don't know the details of unwinding mechanism and exact reasons for the overhead, but at least it sounds worrisome enough to be taken into account.
Sorry for the broken post. I keep forgetting that discuss requires top-posting. In any case, I think what I said was: I’ll have to look into the C++ paper in detail, however, our implementation currently does allow you to query the unwinding state, though I think the API is experimental. It costs a TLS slot.
Ah, I remember what else I wanted to say. Merely that unwinding in Rust is much more of a last resort failure than it is in C++. But honestly all of this seems a bit orthogonal to the idea of invoking a distinct dtor on the ‘abnormal exit’ path. Anyway, I’ll read the paper.
We don’t want to redirect all destructions that occur during unwinding to the UnsafeFinalizer, only the destructions that are done by the unwinding itself (in landing pads), and these called from the UnsafeFinalizer drop glue (transitively).