Posting to internals forum on recommendation from quindot here.
I posted about a question about ?Move
backwards compatibility on Reddit and didn't get a good answer. So I'm going to apply Cunningham's_Law and assume I'm right.
I'll assume P: !Move + !Unpin
and U: Move + Unpin
for the remainder of this post.
Summary
?Move
is backwards compatible just like Pin
is. The definition used by [1] and [2] both work for this explanation, but I'll go with: T: !Move
means "safe code may never move T
and unsafe code may only move T
if the Move
type's address has not been witnessed.
The short description is:
- Replace all instances of
Pin<Ptr>
withPtr
and!Move
trait as needed. Replace all instances with-> P
which is later pinned toPin<&mut P
with-> U
whereU
has a method later used to initializeP
with an out parameter (e.g.into_future<P: ?Move + Future>(self) -> super P
[3]). - Add two new
Deref
traits corresponding toDeref
andDerefMut
whereTarget: ?Move
.
Not breaking
[2:1] argues that Move
is unusable because of backward incompatibility. Most examples given don't actually present issues though.
Traits which can present a problem must have the following characteristics:
- Returns a type which mentions an associated type without moving the associated type (like
&P
or&mut P
).- If the associated type is moved by value, it can't be
!Move
anyway. - This eliminates the arithmetic operators which return associated type by value.
- This eliminates
Iterator
which return associated type by value.
- If the associated type is moved by value, it can't be
- The type doesn't interact with or use
Pin
.- This proposal replaces
Pin
with theMove
trait, so traits which explicitly rely onPin
will be substituted with equivalent?Move
where needed. - This eliminates the normal
Future
andIntoFuture
types.
- This proposal replaces
- The trait is implemented on
Pin
currently.- Traits like
Index{,Mut}
aren't implemented forPin
showing a clear lack of need for, or inability to use, stable addresses. As{Mut,Ref}
are not implemented forPin
and instead are inherent impls onPin
. Normal reference reborrowing on&T
and&mut T
(with the addition of the two new traits) can replace these.- This eliminates all traits that aren't implemented for
Pin<Ptr>
wherePtr
'sTarget: !Unpin
leavingDeref
.
- Traits like
To fix the inability to use Deref
and DerefMut
, create two new traits DerefNotMove
and DerefMutNotMove
(bikeshed) which are the same as Deref
and DerefMut
but their associated type is ?Move
. These new traits will be added to deref coercion.
- The traits could be made so that implementing
Deref
preventsDerefNotMove
andDerefNotMove
, is implemented for allDeref
, and only theDeref{,Mut}
is special to the compiler. - Or
Deref
andDerefMut
could become aliases toDerefNotMove<Target: Move>
and a limited specialization would allow implementing both depending onT: Move
/T: !Move
. - Or they could simply be additional special traits.
Comparison to Pin
Any api which wishes to take advantage of the pinned typestate and stable addresses must either accept the parameter as Pin
or bound by ?Move
, but both require a change. For example if the api takes an argument &mut T
it doesn't support a T
with a stable address and can't rely on T
having a stable address. If T: !Unpin
is pinned then it can't be used in api's that require &mut T
. This is the same as with Move
.
In the world of Pin
if T
is the type you return from a function, you need a new type Pin<&mut T>
with T: Future
to poll it.
In the world of Move
if T
is the type you return from a function, you need a new type P: Future
to poll it. The difference is how T
is converted to the new type. Pin
requires a new_unchecked
after custom setup code for T
. Move
requires putting that custom code in a into_future<T: IntoFuture, P: ?Move + Future>(T) -> super P
.
Using Index
as an example, to use stable addresses with IndexMut
requires IndexMut<Output = Pin<&mut P>>
and method signature becomes index(&self, index: Idx) -> &mut Pin<&mut P>
. To do the same with Index<Output = &mut P>
is also allowed (since &mut P: Move
) and the signature becomes index(&self, index: Idx) -> &mut &mut P
which is the same thing as the Pin
version.
Function traits are also the same. You can't return P
from a function without using super
which changes the function signature. This is the same as Pin<&mut T>
. You can't return, by regular move, a type T
after its pinned. To return a &P
or &mut P
is not problem since &P: Move
and &mut P: Move
, just like Pin<&P>
and Pin<&mut P>
.
New traits
If Iterator
took self
by Pin
instead, this would require a new trait. This is what's happening with the AsyncIterator
/Stream
proposal. If Iterator
bounded self
with ?Move
this would require a new trait. Both approaches require a new trait.
A similar argument can be made for every new trait which takes a Pin
.
Migrating
The following implementations can ease transitioning to Move
based APIs.
// It is safe to pin or remove a pin on an immovable type. The type can't move so the `Pin` does nothing.
impl<Ptr> Pin<Ptr>
where
Ptr: core::ops::Deref,
<Ptr as DerefNotMove>::Target: !Move,
{
/// Constructs a new `Pin<Ptr>` around a pointer to some data of a type that doesn't implement `Move`.
fn new_immovable(ptr: Ptr) -> Pin<Ptr> {
unsafe { Pin::new_unchecked(ptr) }
}
/// Unwraps this Pin<Ptr>, returning the underlying pointer.
/// Doing this operation safely requires that the data pointed at by this pinning pointer doesn't implement `Move` so that we can ignore the pinning invariants when unwrapping it.
fn into_inner_immovable(pin: Pin<Ptr>) -> Ptr {
unsafe { Pin::into_inner_unchecked(pin) }
}
}
#[repr(transparent)]
struct PinFutureFromMoveFuture<T: !Move>(T);
impl<T: core::mov::Future> core::future::Future for PinFutureFromMoveFuture<T> {
type Output = T::Output;
fn poll(
self: Pin<&mut Self>,
cx: &mut Context<'_>,
) -> Poll<Self::Output> {
// Safety: T: !Move so `poll` can't move out of the `Pin`.
let me = unsafe { self.get_unchecked_mut() };
me.0.poll(cx)
}
}
#[repr(transparent)]
struct MoveFutureFromPinFuture<'a, T>(Pin<&'a mut T>);
impl<'a, T: core::future::Future> core::mov::Future for MoveFutureFromPinFuture<T> {
type Output = T::Output;
// Required method
fn poll(
self: &mut Self,
cx: &mut Context<'_>
) -> Poll<Self::Output> {
self.0.as_mut().poll(cx)
}
}
async fn desugaring
Like in [1:1], an async fn
returns an IntoFuture<IntoFuture: ?Move>
instead of a Future
. The IntoFuture
itself is Move
. .await
first calls into_future
and then polls.
Stable rust out parameters.
Also there's a way to implement a usable desugaring of -> super
on stable Rust today.
[3:1] describes most of the process. The bit that's missing is that .assume_init()
which moves the value, is not necessary (though it would be much better). Instead use an owned reference (&own
) similar to the Stackbox type and cast the ptr/reference as needed. After all, what else can an owned !Move
value be used for beyond delayed Drop
or reference? This gets even messier (but still possible?) with enums since you can't easily get the address of the variant's fields while the out pointer is uninitialized.
Some code related to this idea is here in case it helps understand what I'm saying.
The thing I would like feedback on the most is if this idea is not crazy, and whether ?Move
is in fact not necessarily backward incompatible.