Pin & Unpin explained

Can please anyone explain Pin & Unpin?

I understand perfectly fine why it is needed: If there is a struct that has a and there is a pointer b in the struct that points to a, we cannot "just" move the struct. Because b's value is not going to be updated, which results in dangling reference.

So far so good and simple. In C++ we would say something like this to prevent this:

// Forbid copy:
T(const T &) = delete;
T &operator=(const T &) = delete;

// Forbid move:
T(T &&) = delete;
T &operator=(T &&) = delete;

But in Rust, there are 4 options:

  1. Pin<> + Unpin
  2. Pin<> + !Unpin
  3. Unpin
  4. !Unpin

Rust async book says this:

The Pin type wraps pointer types, guaranteeing that the values behind the pointer won't be moved if it is not implementing Unpin

But why do we then need Pin? It is like saying:

  • If you go to supermarket (== wrap T in Pin<>) and it is night (T is Unpin), then there is moon
  • If you go to supermarket (== wrap T in Pin<>) and it is day (T is !Unpin), then there is no moon.

What is the point to say "If you go to supermarket" if it has no effect? Based on that excerpt from the book, Pin along does not guarantee anything. Ultimately, !Unpin and Unpin is what is important.

And overall, what is the purpose to create a mechanism (Pin) effect of which can be overridden in another place (marker Unpin)? I either want to forbid move or not. Why I need to think about if that type has Unpin marker or not?


From rust async book:

Pointers to Unpin types can be freely placed into or taken out of Pin . For example, u8 is Unpin , so Pin<&mut u8> behaves just like a normal &mut u8 .

What is meant by "placed into or taken out of Pin"? In computers, we have memory and addresses (which are ultimately just numbers). A Pin<> field is likely just a number (an address in memory). How can anyone place pointer in number? Did they mean "change the value of Pin field"? Change pointer? I can change pointer, making it point to another object, but I am not sure what "pointer to an object ca be placed into number".

1 Like

If the pointer is not wrapped in Pin, then even for a !Unpin type, there is no guarantee. As you can see in the APIs of types like Box<T>’s into_inner (or simple dereferencing); or for &mut T with std::mem::replace, which allow you to move the value behind the pointer, regardless of any Unpin constraints.

You need both, a type that doesn’t implement Unpin, and a value of that type behind a Pin-wrapped pointer, such as Pin<Box<T>>, or Pin<&mut T>, to get any guarantee.


This is simply describing the API of Pin: The functions Pin::new and Pin::into_inner exist to allow users to freely convert between P and Pin<P> for pointer types P whose target is Unpin.

There’s no deeper meaning behind “placing into / taking out of Pin” apart from adding or removing the newtype wrapper that Pin is. A Pin<Box<T>> and a Box<T> are largely the same thing; as are Pin<&mut T> vs &mut T. They are exactly the same thing in terms of memory layout / implementation / run-time behavior. Pin is a transparent wrapper around the contained pointer; placing into or taking out of Pin is a no-op at run-time, converting between the “pinned” kind of pointer and the ordinary one (though in case of Unpin, calling Pin<&mut T> or the like “pinned” may be confusing as that’s exactly the case where they aren’t really pinned down in memory at all). The significant difference comes only in terms of API limitations that Pin imposes and that enforce the documented invariants, most importantly the invariant that you cannot move the value from behind such a Pin-wrapped pointer, unless the pointer-to type implements Unpin (or unless you are the implementor of the type and really know what you are doing).

4 Likes

It's the opposite, !Unpin and Unpin only have an effect when the type that implements them is wrapped in a Pin, otherwise they do nothing.

Yes, memory wise a Pin<&mut T> and a &mut T are exactly the same (in fact Pin is #[repr(transparent)] over the pointer it contains). However from a type system point of view having a variable of type Pin<&mut T> and one of type &mut T is different since you can do different operations on them. Hence "placed into a Pin" means creating a value of type Pin from a value of its underlying type, while "taken out of a Pin" means the opposite, getting a value of the underlying type from a Pin. Memory wise both these operations are noops, but the important part is the change of types.


One important detail about Pin is that it is not compiler magic that prevents values from being moved. Instead it is a type-level witness of a promise to not move a value. That is, some user code promises to guarantee that some value will no longer be moved, and thus creates a Pin. And since breaking this promise may lead to undefined behaviour, it has to be done with unsafe. Unpin enters into play when some types (lot of types actually) don't really care about this promise since they don't rely on it. Hence Pin specifies that this promise is void if the pinned type implements Unpin, and thus Pin::new can be a safe function, but only for Unpin types.

11 Likes

Thanks! Your explanation is so much better than in Rust Async book... I wish it were in Rust Async book.

For some context: The reason the API of the Pin struct and Unpin trait exist in Rust in the first place is that in Rust every type can be moved. So a C++-style approach of marking certain types as un-movable is not possible, as in Rust all types have always been movable, and changing this rule is quite nontrivial.

As withoutboats put it in their recent blog post:

Move

Let’s say you want Rust to support types which can’t be invalidated without running their destructor once their address has been witnessed. This is a sort of wonky and specific definition of “immoveable type,” but it happens to fit perfectly for what stackless coroutines and intrusive data structures require.

To support this, you would include a marker trait called Move, which is the set of types that can be moved freely. Unlike Send, Move requires some language support: […]

You’ll notice that Rust doesn’t have a Move trait; instead, it provides the same guarantees using the Pin wrapper around pointer types. Even though the Move trait would have probably been a much easier to use API, it proved difficult to add in a backward compatible way (I’ll explain why in a moment), so instead the Pin API was added and used only in the new interfaces that required these semantics.

I would say that one can put some counter arguments against the claim that a Move trait that allows moving in the first place, and that could be left unimplemented analogously to deleting move constructors, would have been a better approach. Whether such counter arguments hold up is hard to tell, and it’s a bit unnecessary anyways, as there’s lots of issues from the backwards-compatibility alone, so such discussions could only apply to new languages, not Rust itself. Anyways, two arguments I can come up with is:

  • having Unpin and Pin be library-only concepts makes the language Rust simpler. As noted, a built-in mechanism to limit moving via a Move trait would need special language support; one would probably have to fully design such a system first to really know if it’s too complex or not, but having it represented using library / API only, seems like a neat approach, too.
  • having Pin allows us to differentiate between movable and un-movable versions of values of the same type. This plays well with the well Rust represents “constructors”, typically as functions returning a newly constructed value simply by-value. So you can have a function construct some !Unpin future type, pass that along to become part of another containing future without indirections, and without needing to fundamentally change the way constructors work in Rust, either. And the limitations of no longer being allowed to move around the values only appear as soon as you come to the first call to poll, which actually requites the value to be pinned.
2 Likes

Also, if you want to spend a few more hours of a guided exploration of Pin, feel free to take a look at

1 Like

This is a very good point, indeed.

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.