Hacker News new | ask | show | jobs
by Animats 480 days ago
Rust destructors are interesting.

- You can't export a reference to the thing you are dropping. You can do that in C++. This prevents "re-animation", where something destroyed comes back to life or is accessed beyond death. Microsoft Managed C++ (early 2000s), supported re-animation and gave it workable semantics. Bad idea, now dead.

- This is part of why Rust destructors cannot run more than once. Less than once is possible, as mentioned above.

- There's an obscure situation with Arc and destructors. When an Arc counts down to 0, the destructor is run. Exactly once. However, Arc countdown and destructor running are not an atomic operation. It is possible for two threads to see an Arc in a strong_count == 1 state just before the Arc counts down. Never check strong_count to see if you are "the last owner". That creates a race condition.[1] I've seen that twice now. I found race conditions that took a day of running to hit. Use strong_count only for debug print.

- A pattern that comes up in GUI libraries and game programming involves objects that are both in some kind of index and owned by Arcs. On drop, the object should be removed from the index. This is a touchy operation. The index should use weak refs, and you have to be prepared to get an un-upgradable Weak from the index.

- Even worse is the case where dropping an object starts a deletion of something else. If the second deletion can't be completed from within the destructor, perhaps because it requires a network transaction, it's very easy to introduce race conditions.

[1] https://github.com/rust-lang/rust/issues/117485

3 comments

> - You can't export a reference to the thing you are dropping. You can do that in C++. This prevents "re-animation", where something destroyed comes back to life or is accessed beyond death. Microsoft Managed C++ (early 2000s), supported re-animation and gave it workable semantics. Bad idea, now dead.

>

> - This is part of why Rust destructors cannot run more than once. ...

This is a very backwards way to describe this, I think. Managed C++ only supported re-animation for garbage collected objects, where it is still today a fairly normal thing for a language to support. This is why these "destructors" typically go by a different name, "finalizers." Some languages allow finalizers to run more than once, even concurrently, but this is again due to their GC design and not a natural thing to expect of a "destructor."

The design of Drop and unmanaged C++ destructors is that they are (by default) deterministically executed before the object is deallocated. Often this deallocation is not by `delete` or `free`, which could perhaps in principle be cancelled, but by a function return popping a stack frame, or some larger object being freed, which it simply does not make sense to cancel.

> Never check strong_count to see if you are "the last owner".

This made me think of the `im` library[0] which provides some immutable/copy on write collections. The docs make it seem like they do some optimizations when they determine there is only one owner:

> Most crucially, if you never clone the data structure, the data inside it is also never cloned, and in this case it acts just like a mutable data structure, with minimal performance differences (but still non-zero, as we still have to check for shared nodes).

I hope this isn't prone to a similar race condition!

[0] https://docs.rs/im/15.1.0/im/index.html

The way to do this while avoiding race conditions seems to be `Arc::into_inner` or `Arc::get_mut`; for instance, the docs for `Arc::try_unwrap` mention a possible race condition, and recommend using `Arc::into_inner` to avoid it: https://doc.rust-lang.org/std/sync/struct.Arc.html#method.tr...
Managed C++ is pretty much around, kind of, as it got replaced by C++/CLI in .NET 2.0, is still used by many of us instead of dealing with P/Invoke annotations, is required by WPF infrastructure, and currently is on C++20 support level.