Safety of a future containing !Unpin types

Hello! I'm making a future to asynchronously return a value of some type, and I'd like to optimize it so that if the value is ready to be returned when the future is created. The future looks, as a minimal example, like this:

use std::future::Future;
use std::pin::Pin;
use std::task::{Context, Poll};

pub enum Foo<T> {
    Pending,
    Ready(Option<T>),
    Complete,
}

impl<T> Foo<T> {
    pub fn pending() -> Self {
        Self::Pending
    }
    
    pub fn ready(output: T) -> Self {
        Self::Ready(Some(output))
    }
}

impl<T> Future for Foo<T> {
    type Output = T;

    fn poll(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<Self::Output> {
        let me = unsafe { self.get_unchecked_mut() };
        match me {
            Self::Pending => todo!("This is a toy example"),
            Self::Ready(output) => {
                let output = output.take().expect("F in ready state twice");
                *me = Self::Complete;
                Poll::Ready(output)
            }
            Self::Complete => panic!("Future polled again after completing"),
        }
    }
}

Is this safe? I believe it is because T will be one of:

  1. A type that is Unpin,
  2. A Pinned pointer to a type that is Unpin,
  3. A type that is !Unpin but is NOT currently pinned, or
  4. A Pinned pointer to a type that is !Unpin.

Scenarios 1 and 2 are trivially safe, and I believe scenarios 3 and 4 are safe as well. In scenario 3, the type is not currently pinned and cannot rely on the guarantees Pin offers. In scenario 4, the pin contains a pointer to the !Unpin type but does not contain the type itself. It's not safe to move the struct, but moving the Pin is safe.

The only situation I am worried about is a Pin that contains not a pointer to a !Unpin type but a !Unpin type itself. That is, not a Pin<&mut PhantomPinned> but a Pin<PhantomPinned>. I don't believe that this pattern is possible/allowable because of things like std::future::Ready being Unpin for all T, not only for T: Unpin, but I don't know for certain that it's impossible.

Assuming that you don’t also somewhere else in your code create some Pin<&mut T> (or similar) pointer to the contents of the Option, this is completely safe – the basic premise of “structural pinning” is that the author of a type can freely decide which fields are considered (structually) pinned, and which are considered always unpinned.

You do however not need to use any unsafe then. You can simply adjust the Unpin implementation with impl<T> Unpin for Foo<T>, lifting the T: Unpin constraint that the implicit, automatic implementation of Unpin had.

Rust Playground

(yes, unlike Send/Sync, the trait Unpin isn’t actually unsafe to implement)[1]


If you ever do want structural pinning, it’s still possible to avoid manual use of unsafe by relying on safe abstractions such as macro from the crate pin_project - Rust


  1. this is a subtle, and sometimes confusing design decision. Of course the correctness of a Unpin implementation can still be relevant to unsafe code; the actual appearance of unsafe would then however be in usage of API such as Pin::new_unchecked and/or Pin::get_unchecked_mut ↩︎

2 Likes

Thank you for the clarification! I was fairly confident that it would work, but wasn't sure if there was some edge case I was missing. Thank you also for the extra information on manually implementing Unpin — I had no idea that it was safe to implement! My default expectation would have been that it was not.