Why does Future::poll() consume the Pin?

Feeling generally comfortable with pinning and futures, but there's one point niggling at me. Future::poll() accepts a Pin-- not a &Pin, not a &mut Pin or anything like that:

fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output>;

To me, this guarantees that the thing being pointed to won't move during the execution of poll(), but I don't see how it guarantees it won't move in between calls to poll().

In this code snippet, I pin a mutable reference, consume the pin, move the thing being referenced, construct another pin... no problem:

fn foo<T: Display>(x: Pin<&mut T>) {
    println!("foo({})", x)    
    let mut x = 1;
    let px: Pin<&mut i32> = Pin::new(&mut x);
    let mut y = 2;
    std::mem::swap(&mut x, &mut y);
    let rx: Pin<&mut i32> = Pin::new(&mut x);

This produces: "foo(1)" and "foo(2)".

[Rust Playground]

What am I missing, here? In what way does the signature of poll() prevent an async runtime from moving the future implementation?

Addition: Reading further (e.g. [Pin use in Futures::poll - #7 by kpreid]), it seems it doesn't: the function signature requires you to get a Pin. If the pointee is Unpin, this is no big deal-- you can safely do just what I did above. If the pointee is Unpin, however, you have to get the Pin via new_unchecked() which makes you promise that "...the data will not be moved or its storage invalidated until it gets dropped." So in the second case, by obtaining the Pin, I'm committing to never moving the pointee, right?

IOW-- what is the contract offered by Pin::new_unchecked()? Can I move the pointee after my Pin is dropped?

i32 is Unpin, which cancels the pinning guarantees. Or phrased differently, the guarantees only apply to types that do not implement Unpin.

Most of the documentation here assumes the data might not be Unpin (as you should have just used new if it was Unpin). You can move the pointee if it is Unpin.

A value, once pinned, must remain pinned forever (unless its type implements Unpin).

You don't even have to drop the Pin in that case.

    let mut x = 1;
    let mut px: Pin<&mut i32> = Pin::new(&mut x);
    *px = 42;

The signature of poll() requires the async runtime to “agree to” the Pin contract by creating a Pin<&mut F>. If F: !Unpin, then the async runtime will either…

  • be unable to move the future (because it is owned by a Pin<Box<F>>, perhaps), or
  • it will be using Pin::new_unchecked() and therefore be responsible for making sure to uphold the Pin contract as per the documentation.

If poll() didn't require Pin<&mut F> but only &mut F, then the future would not have any guarantee that it is pinned, so none of the above would be relevant.

The “it gets dropped” you quoted means “the data”, or “the pointee”, not “the Pin”. Pin just communicates pinnedness to some recipient; the existence of a specific value of type Pin has no inherent significance.

You can move a value if:

  • it has never been pinned,
  • it implements Unpin, or
  • you know something about its type (probably because you defined it) that tells you it is currently okay to move. (I'm not aware of any examples of this and I'm mentioning it just for completeness.)

Also, Pin<P> itself is a wrapper struct around a pointer, and you can reborrow a Pin<&mut T> into a new Pin<&mut T> using Pin::as_mut. (because &mut T as DerefMut.)

That's why it's not a big deal that the function signature consumes a Pin object: the caller can just make more.

I think you might incorrectly think Pin is some kind of "guard" type, while in reality it's a "marker" type. dropping a Pin has no effect whatsoever. using Pin in function signature simply establish an contract between the caller and the callee, where the caller must keep the promise that:

A value, once pinned, must remain pinned forever (unless its type implements Unpin ).

if the caller failed to provide such guarantee, then they have a incorrect (unsound) use of Pin type. that's basically the safety requirement of Pin::new_unchecked()

1 Like

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.