(NOT A CONTRIBUTION)
First, the history. I developed the first version of Pin, which did not contain the drop guarantee. At the time it was also not a wrapper around a pointer, but two types (I think called Anchor and Pin), once of which was basically Pin<Box<T>>
and one of which was basically Pin<&mut T>
.
At the Rust all hands in 2018, Taylor Cramer showed me that if we restated the pin guarantee to also guarantee destructors running, it was an adequate interface to support intrusive data structures. I don't know if Taylor had in mind specifically using intrusive data structures for futures synchronization primitives, but we definitely also had the idea that making this new API useful for more things would be better, because it would provide more utility to users and win more buy in. At the time adding the pinning guarantee to futures to support async/await was very controversial with the libraries that had to implement futures by hand, because they didn't fully appreciate how much better async/await with references would be than without.
So, we rewrote the requirement, and then later called the types PinBox and PinMut, and then even later on I realised we could abstract Pin over the pointer type and then make it a "mode" applied to any pointer, rather than specific types. From that point forward we never reconsidered making the drop guarantee a separate type, and Pin stabilised in that form.
Could it have been a separate guarantee? Yes. And there are certainly arguments from elegance for doing so. But if it had, it wouldn't have appeared in the Future API contract, and therefore tokio wouldn't be able to rely on it to use intrusive linked lists in its synchronization primitives. So it's a trade off and I don't regret the trade off we chose. But let me be clear: we added the guarantee and then tokio took advantage of it, it was not required for tokio to work - the tokio team didn't even want us to add pinning at all at first!
We never considered the API in @Kixunil's post before adding this guarantee, instead we had another API that was all safe code and relied on tying lifetimes together in a weird way. Ralf objected to this because he thought it was possibly exploiting a bug in rustc's implementation and not actually sound. So we settled on the macro and never looked back.
Nowadays, I think it would be worth looking adding a let binding modifier (e.g. let pin foo = ...
) that does essentially what @Kixunil's post does behind the scenes, but correctly guarantees the destructor runs. And if it were possible to also pin project with a binding modifier, that would be really great. If I could change everything I would make Drop take &pin mut self
and make Unpin
unsafe and this would all work out really well, but Drop's API was already stabilised by the time Pin was invented.
This is not really true. The drop requirement was not required by async runtimes, and was added to make Pin useful for other things. I think this comes from misremembering our claim that it was not a general-purpose self-referential type, which it isn't (e.g. the pattern in rental is totally different and not supported by pin). But Pin is a modifier to guarantee that the referenced memory won't be invalidated until after the destructor runs, which is indeed more than we needed to desugar async functions, and it can be used for any code that needs that.
This is also not really true. None of the libraries I'm familiar with work this way. One (rio) is just unsound, the others all use ownership passing to avoid this problem rather than pinning and blocking in the destructor. Even with pinning to make it sound, blocking in the destructor is an awful way to handle this because then if you want to cancel interest you've blocked this thread until the operation completes; you basically have made your code sync. Even if Rust had async destructors, you couldn't do something like race two sources or time out an operation, because you have to wait until the operation completes before doing anything. I would consider a library that works this way unfit for purpose.