Pin use in Futures::poll

It is not enforced by the compiler, exactly. Rather, by creating a Pin<P> (where P is some pointer type), the creator makes a promise that it's not going to move the referent of P. That creation is done using an unsafe function; the reason it is unsafe is that the compiler cannot check the promise that the referent of P won't be moved. (Not every use of Pin requires writing new unsafe code; for example, you can call Box::pin() to create a Pin<Box<T>>. The Box is doing the promising, here.)

Once the Pin is created, the part that the compiler does track is the fact that the type is Pin<P> — ordinary type checking and type inference — which has the effect, due to what functions are available to operate on Pin, of ensuring that the referent can't get “unpinned” and move (unless it is safe to do so).

(I don't have a good feeling why such a promise is important when so many other things we program against are based on the semantics of the API as described in documentation alone - but I digress.)

The key distinction is that the result of violating the Pin contract would be unsoundness (or memory unsafety) due to dangling pointers (pointers that no longer point to an existing allocation with valid contents). One of the foundational principles of Rust is that you will never encounter a dangling pointer unless some unsafe code did something wrong.

  1. The self referential pieces of interest here are actually setup in the first or subsequent calls to poll, not before the first poll. So again, any self references set up in one call to poll would still be valid in subsequent calls to poll for the same future F.

Yes. For a concrete example:

let my_future = async {
    let mut x: Vec<i32> = get_data();
    some_other_async_fn(&mut x).await;
    println!("done!");
};

When my_future, the Future generated by this async block, is in its await, it has a self-reference: the future from some_other_async_fn (which is stored as part of my_future) contains a &mut Vec<i32> pointing at the local variable x (which is also stored as part of my_future). But, before the future has been polled, none of the code in it has run — get_data() hasn't been called and x doesn't exist yet — so there are no self-references and the future is safe to move.

(It's useful that futures are movable before being polled because, for example, any time you write a function that returns a future, that future is being moved.)

  1. ... It sure would be nice to have a picture created of how an entire lot of future locations are fixed by the runtime.

It's not the runtime's business; rather, every future that you write that uses other futures contains those futures. In my above code sample, my_future contains the future that some_other_async_fn returns. It's sort of like if a function pre-allocated all the stack space that would ever be necessary for all the functions that it will call.[1] So, the size_of() that future is exactly that maximum space required, and the async runtime merely needs to allocate space for that (usually inside a Box or similar) and not move it.

This isn't a special thing about futures; it's exactly the same concept as if you write an enum A that contains another enum B — the size of A will be the size required to represent all possible states of A, including "contains a B" together with the B value. Then, given an &mut A, you can write any possible A into it.


  1. "Wouldn't that prevent recursion?" Yes, it does! You will get an error if you write a recursive async function without special measures like Boxing a future. ↩︎

4 Likes