Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

What does `Pin<P>` actually do?

Tags:

rust

I'm following the official docs to understand what Pin<P> means. Quoting verbatim from the docs, we use Pin when we want to "prevent(ing) the value referenced by that pointer from being moved unless it implements Unpin." From that, I'm reasoning that pinning primitive types like an usize would be pointless because it implements Unpin and would thereby bypass any guarantees Pin makes.

But I don't understand how Pin makes any guarantees. Does Pin actually do something at the compiler level that forces the T to hold the same address for some lifetime?

I think the answer is no. It seems that Pin doesn't actually do anything except it purposefully restricts the APIs available to retrieve &mut T. In other words, it sounds like using Pin is more of a declarative gesture saying "this T should not be moved unless you know what you're doing". I'm stating this because I don't know why it is so dangerous to expose an &mut T for pinned T's.

Let's say that (for some reason), it is unsafe to retrieve mutuable references. I see that DerefMut is actually implemented for Pin but it comes with the catch that P must deref into something Unpin. Then it the docs suggest that the way to make something not Unpin is to add a PhantomPinned to the struct. Is this the actual 'magic' of pinning?

Tldr: My rough guess on what is pinning:

  1. We want to prevent the moving of data so we use Pin
  2. Pin doesn't actually do anything, it just sets up restrictive APIs to make getting mutuable references unsafe. Presumably, getting mutuable references breaks pinning.
  3. Basically everything is Unpinned, thereby making Pin worthless. We can only enforce the boundaries Pin is trying to setup if we add a PhantomPinned to our struct.
like image 834
Kiwi breeder Avatar asked Jul 27 '26 01:07

Kiwi breeder


2 Answers

But I don't understand how Pin makes any guarantees.

Pin does not contain in its implementation a mechanism which provides a guarantee. Rather, Pin is how that guarantee is expressed. It's a means to communicate that a certain pointer is pinned (and to do some simple manipulations on it).

For example, Box::pin() or Box::into_pin() produce a Pin<Box<T>>. To do that, they call unsafe { Pin::new_unchecked(...) }. Box<T> knows how its own implementation works, so Box<T> knows that it can make that guarantee, so Box<T> provides these safe methods to pin a Box.

Then, later, other code can receive a Pin<Box<T>> (or a Pin<&mut T> created from it with Pin::as_mut()) and be assured that it is sound to use the T in some way that requires pinning.

Pin didn't do any computational work, nor does it know how to ensure something is pinned in any case. It is just a thing that can be constructed at one point and then used at another.

Simple data types like NonZeroU32 are somewhat analogous — that's also a type which conveys a guarantee (the number is not zero) from one point (where it is constructed) to another (where it is used). But one notable difference is that you can check whether a u32 is zero before you use it, but you can't check whether a pointer is pinned before you use it, because pinning is a fact about what isn't going to happen in the future, not something you can check with an algorithm.

I don't know why it is so dangerous to expose an &mut T for pinned T's.

Here's the key fact: &mut T does not, by itself, mean that “you cannot move this T”. It only means “after you are done, there must still be a valid T here”. If you have two &mut Ts, you can swap them with std::mem::swap(). With replace_with you can even move the T out, manipulate it, then move it back.

All of these things violate the pinning guarantee (that the T will have a stable memory location until it is dropped), so they are not allowed by Pin. We could imagine a different language which prohibited these swaps and would not need the distinction between Pin<&mut T> and Pin<T>, but that's not the language Rust is, and it also wouldn't suffice for the distinction made by Pin<Box<T>> vs Box<T> — that you can't even move it if you own it, if it's pinned.

Basically everything is Unpinned, thereby making Pin worthless. We can only enforce the boundaries Pin is trying to setup if we add a PhantomPinned to our struct.

This is more or less true. The point of Unpin-by-default is that generic functions can demand to be given Pin<&mut T> or similar, thus enabling their use with Ts that need to be pinned, without also creating inconvenience when the Pin is not necessary, by making it possible for safe code to make a “meaningless” Pin to satisfy the type that generic function wants, when that would not do any harm. Unpin is the trait that expresses “moving this after it is pinned will not do any harm”.

You make your type !Unpin, by using PhantomPinned or some other type, when you plan to rely (usually in unsafe code) on your struct being pinned.

like image 135
Kevin Reid Avatar answered Jul 29 '26 03:07

Kevin Reid


I think you are generally correct about '''Pin''', from my understanding, the purpose of Pin is to prevent value referenced by the pointer from being moved even if the underlying type is movable, unless the underlying type is Unpin.

Simply put, Pin is a way to ensure that the underlying value is not moved.

Regarding to your point that "everything is Unpinned, thereby making Pin worthless", many types are Unpin by default, but not all types are!

In particular, types that contain raw pointers or references are often !Unpin, because moving them could invalidate the reference. So there are definitely cases where Pin is useful and necessary, even if many types are Unpin.

like image 45
Bryce Avatar answered Jul 29 '26 02:07

Bryce



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!