As my first excersise to see how the new std::future::Future
and Pin
API works, I decided to implement join
that will poll
two Future
s parallely.
Here is what I end up with:
use std::future::Future;
use std::task::{Poll::{self, Ready, Pending}, LocalWaker};
use std::pin::Pin;
use std::cell::Cell;
pub fn join<T1,T2>(f1: impl Future<Output=T1>, f2: impl Future<Output=T2>)
-> impl Future<Output=(T1,T2)> {
struct JoinFuture<T1,T2,F1,F2>(F1,F2,Cell<Option<T1>>,Cell<Option<T2>>);
impl<T1,T2,F1,F2> JoinFuture<T1,T2,F1,F2> {
fn get_f1<'a>(self: Pin<&'a mut Self>) -> Pin<&'a mut F1> {
unsafe { self.map_unchecked_mut(|v| &mut v.0) }
}
fn get_f2<'a>(self: Pin<&'a mut Self>) -> Pin<&'a mut F2> {
unsafe { self.map_unchecked_mut(|v| &mut v.1) }
}
}
impl<T1,T2,F1,F2> Future for JoinFuture<T1,T2,F1,F2>
where F1: Future<Output=T1>,
F2: Future<Output=T2>,
{
type Output = (T1, T2);
fn poll(mut self: Pin<&mut Self>, lw: &LocalWaker) -> Poll<Self::Output> {
match (self.2.take(), self.3.take()) {
(Some(_), Some(_)) => unreachable!(),
(Some(v1), _) => match self.as_mut().get_f2().poll(lw) {
Ready(v2) => Ready((v1, v2)),
_ => { self.2.set(Some(v1)); Pending }
},
(_, Some(v2)) => match self.as_mut().get_f1().poll(lw) {
Ready(v1) => Ready((v1, v2)),
_ => { self.3.set(Some(v2)); Pending }
},
_ => match (self.as_mut().get_f1().poll(lw), self.as_mut().get_f2().poll(lw)) {
(Ready(v1),Ready(v2)) => Ready((v1, v2)),
(Ready(v1), _) => { self.2.set(Some(v1)); Pending },
(_, Ready(v2)) => { self.3.set(Some(v2)); Pending },
_ => Pending,
}
}
}
}
JoinFuture(f1, f2, Cell::new(None), Cell::new(None))
}
Which I have to
- use
unsafe
blocks, because aPin<&mut T>
only garantee that theT
object will not move until dropped, not its fields, and we have to generatePin
pointers into its fields, which requires additional garantee. - use
Cell
s. Technically I might be able to implement without them, but with their help I can match with(self.2.take(), self.3.take())
with two shared references ofself
.
Overall, this is not too bad. Some concerns:
- Would it be possible to mark a struct to say: a struct maybe
Unpin
and so movable, but its certain field is always moved with the whole struct? This is the garantee that required by theunsafe
block. Something like field attribute#[sticky]
would be good. The currentstd::mark::PhantomPinned
cannot be used here as it would make the struct unmovable. - The
as_mut
method forPin<&mut T>
plays a very simular role that a reborrow does. I remember somebody have aReborrow
trait proposal. Maybe its time to review this and make reborrowing work forPin<&mut T>
or simular types.