What happens when I move an object before pinning it?

For example, I create a async future on stack and then assigin it to another variable, and then call Pin::new_uncheck().

Some questions:

  • do I break the safety requirements of pin?
  • should poll() on this pinned object be ok?
async foo() {
}

let f1 = foo(); // Unpin is not implemented for f1
let f2 = f1;  // f1 has been moved to f2, should f2 be valid or invalid?

let p = unsafe { Pin::new_unchecked(&mut f2) };
p.poll(...);  // can this crash? since I have moved an unpin object

The safety invariants for Pin are that you can't move the value after it is pinned. It's fine to move the future around as much as you want beforehand as long as it doesn't get moved after you first pin it.

Put another way, if you can't move a future then how do you send it to an executor like tokio to be executed?

4 Likes

If you want to be certain not to break safety requirements as a user of code involving pinning, the best way is to avoid using unsafe code yourself. Your example code can be written in safe Rust with help of the futures::pin_mut! macro

async foo() {
}

let f1 = foo(); // Unpin is not implemented for f1
let f2 = f1;  // f1 has been moved to f2, should f2 be valid or invalid?

let p = f1;
pin_mut!(p);

p.poll(...);  // can this crash? since I have moved an unpin object

(any by the way, the first thing that this macro does internally is also to move the value once again).

And by the way, the assignment let f1 = foo() also moves the future.


By the way, the reason why futures from async fns need to be pinned is so that the body of those functions can create local references inside of their “stack frame”. The local variables of an async fn become fields of the generated future struct; those structs become self-referential if one field contains a pointer to another field, moving such a struct can invalidate such references.

The reason why you can safely move such a future before pinning it is:

  • you can only start polling the future once it’s pinned
  • before the first poll, the async fn isn’t executed at all. The call to foo() does nothing before you start to .await it (or otherwise poll it), not even proceed to its own first .await point
  • when the async fn isn’t executed yet, the local variables inside of the async fn haven’t been initialized yet, so consequently variables / fields that aren’t initialized cannot get invalidated by moves either
10 Likes

So when I say an object is !Unpin, this object is actually can be moved before call Pin::new_checked on it, and can not be moved only after this.

Many people use self-referenced object to illustrate why pinning is required, but self-referenced object can't be moved immediately after it is created/initialized, so this is not exactly same as objects which are!Unpin.

This is why I get confused when moves an !Unpin object :frowning: .

1 Like

This is a great question, I don't understand either why it's ok to move around a future (shouldn't it invalidate its self-references?) and then pinning is somehow "fixes" it?

Future created by async fn contains self-references, but these self-references is not intialized util polling it.

async fn bar() {
   let x = [0;100];
   tokio::time::sleep(1).await;
   println!("{}", x[0]);
};

// this allocates memory to store state including self-references, but all states
// are uninitialized, that is, x and reference to x (i.e. x[0]) has not be initialized
let f = bar(); 

// this moves f to f2, but since all states are uninitialized, it's ok
let f2 = f;

// poll() will actually run codes of this future, these codes will initialize and 
// modify state of f2, so self-references in f2 will point to f2, and we can't 
// move f2 anymore.
let p = unsafe { Pin::new_uncheck(&mut f2 ) };
p.poll();

// since f2 is !Unpin, Pin will not give out mut reference of f2, and you can't move f2
// in **safe** code. drop() is specail, .i.e, it's safe and you get &mut f2, this is described
// in detials in doc of Pin.
// 
// you can get &mut f2 throug unsafe methods and move f2, this is exactly why these
// methods are marked as unsafe.

self-referenced data structure can also behave similarly:

struct Foo {
    value: i32,
    ref: *const i32,
}

impl Foo {
   fn init(&mut self) {
      self.value = 100;
      self.ref = &self.value;
   }
}

let f = Foo {
    value: 0,
    ref: 0 as *const _,
};

// we create f, but state of f is unitialized, so it's ok to move f
let f2 = f;

// init() actually create self-reference, so f2 can't be moved any more
f2.init()

If you look at the example of a self-referential struct in the Pin documentation, it's not made self-referential until after it is pinned for precisely this reason (particularly see the comment in lines 23 and 24 of the example). Pinning guarantees that a !Unpin type will not be moved after it has been pinned, but says nothing before, so it generally isn't safe to put something that may have already been invalidated into a Pin. Futures make sure the won't have been invalidated by not becoming self-referential until polled (at which point they are already pinned). Similar considerations should be taken into account when creating something that shouldn't be moved, and the function creating it should return a Pin if it needs to be created already immovable in order to be safe (as is the case in the self-referential example).

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.