`Pin<&T>` concerns

Description of Pin::map_unchecked says:

You must guarantee that the data you return will not move so long as the argument value does not move (for example, because it is one of the fields of that value), and also that you do not move out of the argument you receive to the interior function.

But both interior function's parameter and return value are shared references, which as explained in Pin::get_ref don't allow to move the data out of:

This is safe because it is not possible to move out of a shared reference. It may seem like there is an issue here with interior mutability: in fact, it is possible to move a T out of a &RefCell<T>. However, this is not a problem as long as there does not also exist a Pin<&T> pointing to the same data, and RefCell<T> does not let you create a pinned reference to its contents. See the discussion on “pinning projections” for further details.

Questions:

  1. Does the Pin::map_unchecked description warns about situations like turning Pin<&Struct> into Pin<&Field> and then moving Struct (which could cause problems if pinning is structural)?
  2. Pin::get_ref mentions that moving T out of &RefCell<T> is fine, because there's no way to obtain Pin<&T> while having &RefCell<T>. As I understand, that kind of move would only affect Pin<&T> breaking its invariants and Pin<&RefCell<T>> would not care, because it simply pins RefCell, not T. Is this correct?

The warning is more targeted at turning Pin<&Struct> into Pin<&Field> and then moving Field.

1 Like

I looked at it once again and yes, the part about not moving returned data without moving argument is pretty understable and simple, but I think the marked part below is just a documentation mistake:

This function is unsafe. You must guarantee that the data you return will not move so long as the argument value does not move (for example, because it is one of the fields of that value), and also that you do not move out of the argument you receive to the interior function.

There's no way to move out of shared reference.
I think they just simply copied the whole function description from Pin::map_unchecked_mut, which is exactly the same. The difference is there is a way to move out of the argument (because we receive &mut T, not &T). Please correct me if I'm wrong, otherwise I'll suggest an update to that doc.

There are ways, though. Interior mutability makes it possible. Perhaps documentation could be clearer about this. It's just assumed that types built on UnsafeCell are well known to have this capability.

There are ways, though. Interior mutability makes it possible.

Isn't this case explained in Pin::get_ref docs?

Gets a shared reference out of a pin.

This is safe because it is not possible to move out of a shared reference. It may seem like there is an issue here with interior mutability: in fact, it is possible to move a T out of a &RefCell<T>. However, this is not a problem as long as there does not also exist a Pin<&T> pointing to the same data, and RefCell<T> does not let you create a pinned reference to its contents. See the discussion on “pinning projections” for further details.

So it looks like it doesn't matter if it can move or not, meanwhile docs of Pin::map_unchecked say that I have to guarantee the lack of moving of arg (which is immutable reference).

I believe the difference is that the "pinning projections" link shows how an imaginary method on RefCell that could lead to UB by creating mutable aliases, but it doesn't address the issue of holding a Pin<&Field> and then moving Field as @alice described. And that is possible through RefCell::replace among others. Quoting OP:

Pin::map_unchecked does allow exactly this (AFAIK). See: https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=8e534b883bae91be268ce7d643f822a6

1 Like

Thanks for your example, but the thing that is still bugging me out is:

you do not move out of the argument you receive to the interior function

In your example we first get Pin<&NonCopy> and then the value is moved. Ofcourse it is also UB, but the method's description implies that the move (should not) occurs within the closure's body (and the Pin<&NonCopy> is created after the closure's job is done).

I may simply misunderstand it, but if that's the case, I think the description should be clearer.