I'm not sure if someone discussed the idea before, but I'm not aware of such discussions in the AsyncDrop thread.
A problem with AsyncDrop is that a signature like follows:
trait AsyncDrop {
fn poll_drop(self: Pin<&mut Self>, cx: &Context<'_>) -> Poll<()>;
}
requires the object to track whether poll_drop
was called or whether the object was dropped internally and dynamically, and the implementor is obliged to "fuse" poll_drop
manually.
However, there is a way to enforce typestate statically with a trick on lifetimes of mutable references! This means that code that attempts to call poll
after poll_drop
simply doesn't compile.
use std::marker::PhantomData;
struct DropOnce<'a> {
_data: PhantomData<&'a mut ()>,
}
impl<'a> DropOnce<'a> {
fn poll(&'a mut self) -> &'a mut Self {
println!("poll");
self
}
fn start_drop(&'a mut self) -> Dropping<'a> {
Dropping(self)
}
}
struct Dropping<'a>(&'a mut DropOnce<'a>);
impl<'a> Dropping<'a> {
fn poll(&'a mut self) -> &'a mut Self {
println!("dropping");
self
}
}
fn main() {
let mut x = &mut DropOnce { _data: PhantomData };
for i in 0..3 { x = x.poll(); }
let mut y = &mut x.start_drop();
for i in 0..3 { y = y.poll(); }
//x.poll(); // fails borrow checker
}
It works by fixing the lifetime 'a
on the struct's signature. In this way, at most one instance of &'a mut Self
can exist in the program at any time (&mut
refs also have ownership), and if we consume it, it's gone. If we wrap it in Dropping
, there is no one else who can retrieve it.
Based on this idea, I propose a new StronglyTypedFuture
trait (intentionally ugly name) that encodes future states as typestates instead of runtime states, so that:
- The state of the future (whether it is running, completed or being dropped) is a part of the future's type
- Futures are fused statically, and attempting to poll a future that has completed is a compiler error
- Once a future is turned into a dropping future, attempting to poll the original future is a compiler error
- Attempting to poll a dropping future that has completed is also a compiler error
enum StronglyTypedPoll<P, T> {
Pending(P),
Ready(T),
}
trait StronglyTypedFuture<'a> {
type Output;
fn poll<'b>(self: Pin<&'a mut Self>, cx: &Context<'b>) -> StronglyTypedPoll<Pin<&'a mut Self>, Self::Output>;
}
// Every other method need to ensure that the reference they get from &'b mut self
// matches 'a: 'b in order to ensure that such method cannot be called after a call to
// async_drop. This is currently enforced by the compiler if a field with lifetime 'a
// exists in the struct, but will not be the case if my proposal about expired references
// was implemented.
// (see https://internals.rust-lang.org/t/proposal-about-expired-references/11675/11)
trait StronglyTypedAsyncDrop<'a> {
type DropFuture: StronglyTypedFuture<'a, Output = ()>;
fn async_drop(self: Pin<&'a mut Self>) -> StronglyTypedFuture<'a, Output = ()>;
}