I'm writing a future combinator that needs to consume a value that it was provided with. With futures 0.1, Future::poll
took self: &mut Self
, which effectively meant that my combinator contained an Option
and I called Option::take
on it when the underlying future resolves.
The Future::poll
method in the standard library takes self: Pin<&mut Self>
instead, so I've been reading about the guarantees required in order to safely make use of Pin
.
From the pin
module documentation on the Drop
guarantee (emphasis mine):
Concretely, for pinned data you have to maintain the invariant that its memory will not get invalidated from the moment it gets pinned until when drop is called. Memory can be invalidated by deallocation, but also by replacing a
Some(v)
byNone
, or callingVec::set_len
to "kill" some elements off of a vector.
And Projections and Structural Pinning (emphasis mine):
You must not offer any other operations that could lead to data being moved out of the fields when your type is pinned. For example, if the wrapper contains an
Option<T>
and there is a take-like operation with typefn(Pin<&mut Wrapper<T>>) -> Option<T>
, that operation can be used to move aT
out of a pinnedWrapper<T>
-- which means pinning cannot be structural.
However, the existing Map
combinator calls Option::take
on a member value when the underlying future has resolved:
fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<T> {
match self.as_mut().future().poll(cx) {
Poll::Pending => Poll::Pending,
Poll::Ready(output) => {
let f = self.f().take()
.expect("Map must not be polled after it returned `Poll::Ready`");
Poll::Ready(f(output))
}
}
}
The f
method is generated by the unsafe_unpinned
macro and looks roughly like:
fn f<'a>(self: Pin<&'a mut Self>) -> &'a mut Option<F> {
unsafe { &mut Pin::get_unchecked_mut(self).f }
}
It appears that Map
violates the requirements that are described in the pin
documentation, but I believe that the authors of the Map
combinator know what they are doing and that this code is safe.
What logic allows them to perform this operation in a safe manner?
edit: This answer is incorrect. It remains here for posterity.
Let's begin by recalling why
Pin
was introduced in the first place: we want to statically ensure that self-referential futures cannot be moved, thus invalidating their internal references.With that in mind, let's take a look at the definition of
Map
.Map
has two fields, the first one stores a future, the second stores a closure which maps the result of that future to another value. We wish to support storing self-referential types directly infuture
without placing them behind a pointer. This means that ifFut
is a self-referential type,Map
cannot be moved once it is constructed. That is why we must usePin<&mut Map>
as the receiver forFuture::poll
. If a normal mutable reference to aMap
containing a self-referential future was ever exposed to an implementor ofFuture
, users could cause UB using only safe code by causing theMap
to be moved usingmem::replace
.However, we don't need to support storing self-referential types in
f
. If we assume that the self-referential part of aMap
is wholly contained infuture
, we can freely modifyf
as long as we don't allowfuture
to be moved.While a self-referential closure would be very unusual, the assumption that
f
be safe to move (which is equivalent toF: Unpin
) is not explicitly stated anywhere. However, we still move the value inf
inFuture::poll
by callingtake
! I think this is indeed a bug, but I'm not 100% sure. I think thef()
getter should requireF: Unpin
which would meanMap
can only implementFuture
when the closure argument is safe to be moved from behind aPin
.It's very possible that I'm overlooking some subtleties in the pin API here, and the implementation is indeed safe. I'm still wrapping my head around it as well.
It is all about structural pinning.
First, I will use the syntax
P<T>
to mean something likeimpl Deref<Target = T>
— some (smart) pointer typeP
thatDeref::deref
s to aT
.Pin
only "applies" to / makes sense on such (smart) pointers.Let's say we have:
The initial question is
This requires the basic projection
P<Wrapper<Field>> -> P<Field>
, which is only possible for:shared references (
P<T> = &T
). This is not a very interesting case given thatPin<P<T>>
alwaysderef
s toT
.unique references (
P<T> = &mut T
).I will use the syntax
&[mut] T
for this type of projection.The question now becomes:
The point that may be unclear from the documentation is that it is up to the creator of
Wrapper
to decide!There are two possible choices for the library author for each struct field.
There is a structural
Pin
projection to that fieldFor instance, the
pin_utils::unsafe_pinned!
macro is used to define such a projection (Pin<&mut Wrapper<Field>> -> Pin<&mut Field>
).For the
Pin
projection to be sound:the whole struct must only implement
Unpin
when all the fields for which there is a structuralPin
projection implementUnpin
.unsafe
to move such fields out of aPin<&mut Wrapper<Field>>
(orPin<&mut Self>
whenSelf = Wrapper<Field>
). For instance,Option::take()
is forbidden.the whole struct may only implement
Drop
ifDrop::drop
does not move any of the fields for which there is a structural projection.the struct cannot be
#[repr(packed)]
(a corollary of the previous item).In your given
future::Map
example, this is the case of thefuture
field of theMap
struct.There is no structural
Pin
projection to that fieldFor instance, the
pin_utils::unsafe_unpinned!
macro is used to define such a projection (Pin<&mut Wrapper<Field>> -> &mut Field
).In this case, that field is not considered pinned by a
Pin<&mut Wrapper<Field>>
.whether
Field
isUnpin
or not does not matter.unsafe
to move such fields out of aPin<&mut Wrapper<Field>>
. For instance,Option::take()
is allowed.Drop::drop
is also allowed to move such fields,In your given
future::Map
example, this is the case of thef
field of theMap
struct.Example of both types of projection