Rust Project Goals: Immobile types and guaranteed destructors
20 points by noncrab
20 points by noncrab
I’m glad to see this, as I’ve been reading about async Rust (haven't done any concurrent programming in Rust yet) and it sounds like a bit of a mess, with more than one guide concluding something like:
Async Rust … comes at the cost of additional inconvenience and hazards. … I recommend using ordinary (synchronous) Rust, with multithreading where you need concurrency, unless you have a reason to do otherwise.
I’m struggling to understand why forget() is not unsafe. It breaks the contracts of anything using RAII, and in the case of futures it means an entire async call chain can just abort with no chance to clean up. That’s worse than a panic, where at least you have the option to run destructors. I really don’t fancy writing and debugging async code in such an environment…
I’m struggling to understand why forget() is not unsafe.
It traces back to the "leakpocalypse" back in the early days of Rust, when an API (an ealier version of thread::scope, I think it was) almost made it to v1.0 freeze before being discovered to be unsound and they realized that, on a theoretical level, they fundamentally cannot guarantee Drop will be run to a degree needed for soundness to depend on it.
(For example, they can't prove, at compile time that your use of Rc/Arc is free of reference cycles.)
...so they decided that leaking memory was not unsafe.
Even in GCed languages, you get memory leaks all the time... by having something accidentally hold onto a reference it should have let go of.
It's like how safe Rust only guarantees your code is free of a subset of race conditions known as data races, because that's what's attainable in the general case.
Rust is basically pushing up against the state of the art and, when a language like Haskell is further along in some way, it tends to be because they only know how to do what that language is doing under a very different execution model. (eg. a garbage-collecting virtual machine)
...and one of the things they point to about the current state of Drop is that Rust can't magically protect your computer from you tripping over the cord and you should be designing your system to be safe from that case anyway. (Or, if you want to be a little more silly and flippant, the Rust developers can't afford to ship out a free UPS and lockstep-redundant computer to everyone who downloads the toolchain.)
Is async drop in line with Rust's "pit of success" design? Yes... but RAII is fundamentally a leaky abstraction.
First, complaints about Rust's async are likely to sound pretty bad, while in reality it's a very good implementation given its constraints.
It's the best that is currently available if you refuse to have per-Future heap allocation overhead. All languages where async is more ergonomic do heap allocations per promise/per await, and Rust can too to make dealing with async easier, but Rust devs refuse the easy path and bend backwards to have (amortized) zero-cost Futures. It's the same story as the borrow checker: it's clunkier and harder than a GC language, but still really good for a no-GC language.
There are devs who think async is pointless, functions having "color" is a design flaw, and code should look like sync calls and threads. This criticism is a fundamental disagreement, and won't go away no matter how polished Rust makes its async.
Pre 1.0 Rust tried to keep forget() unsafe (early even have separate heap per thread), but reference cycles can leak memory too. Sending an object to a channel that isn't read from "leaks" it too. If you have shared ownership, then assigning to a global variable or a thread running loop {} "leaks" too. Blocking just forget() wouldn't close all the other forget-like loopholes. Rust is being honest here that the type system doesn't guarantee that destructors will run.
Rust's unsafe is for memory safety, not for bad ideas. std::fs::remove_dir_all("/") is a "safe" function.
In practice leaks aren't an issue, because forget() is a special function that you have to call deliberately. Regular code with automatic cleanup works fine. Refcounted types are also foolproof as long as they're not recursive types with interior mutability.
The !Forget types will have to be more restricted and less usable than leakable types, so it would be inconvenient language default, but should be acceptable for special types like scoped async spawn handles.
forget is safe because it is possible to implement it with safe code, so marking it unsafe would essentially just be lying.
Specifically, you can do things like move an object into an Rc cycle, or move an object into a thread that then hangs, or a number of other things.
Just before Rust 1.0, forget was marked unsafe, and there were standard library APIs relying on this. When this problem was discovered, it was decided that rather than try to plug every possible hole, those APIs would simply have to change to accommodate reality.
This event is known as the leakpocalypse: https://faultlore.com/blah/everyone-poops/
Plugging every possible hold would have meant onerous things like a Leak trait that a type must implement in order to be stored in an Rc. The "guaranteed destructors" work here is going to have to reckon with that somehow.
The correct (but possibly unsatisfying) answer is that "safe" has a specific meaning in Rust, defined around here, and which I heuristically remember as memory safety: "if some x has type T, it is impossible for x to not have type T at runtime". This effectively covers uninitialized variables, array bounds, use after free, casting, data races, and more.
Other stuff is just out of scope. (For a strawman example: even though deleting a file can break your system, std::fs::remove_file is not marked unsafe. Or that many functions like .unwrap() can panic, killing your process.) Note also the following section, "Behavior not considered unsafe", which lists things like deadlocks and leaks as not part of safety.
PS lobsters frontpage has a nice article about this right now, "Practical Memory Safety"
Reading the title, I was excited, but seems this doesnt enable linear types which are the blocker for me. Though immovable types seems a lot simpler than pin, which is quite nice. I'm skeptical about guaranteed destructors, but also dont really care, since if linear types are added I'm likely to just ban use of types with destructors in my codebases.