Yesterday I got some helpful explanations of Pin
and I spent time studying it. I'm pretty sure I now know all that I need to know at a basic level so I would just like to know if the following understanding is correct.
Basic
Pin
is just an api formalization for semi-safe usage of structs objects that have very unsafe ways to use them. Namely self-referential structs but also other types of data structures like an intrusive linked list with ways to unsafely drop nodes. All the issues are related to moving data that shouldn't be moved in certain situations.
[note to my future self] Typically this movement is heap related because heap data can only be referred to with pointers and when things gets moved on the heap those pointers dangle. Even growing objects on the heap can move them if space is unavailable. Stack variables are updated in place so much less of a problem, typically. Platform specifics non-withstanding. The fields in a struct that are not heap allocated and the struct is on the stack, when it moves, meaning changes address, so do the stack variable fields with it.
API
Okay so the people implementing these unsafe-to-move data structures have to expose a carefully crafted api that still allows mutation, but doesn't dangle the pointer. How this is done is up to the implementers and has nothing to do with Pin
. Where Pin
comes in is it allows these implementers to write an api with Pin<..>
as the signature. Labeling the functions safe to use through the Pin
api itself.
fn init_me(_: Pin<&mut T>)
fn mutate_me(_: Pin<&mut T>)
// etc.
What's nice about this as opposed to just making the implementation of fn init_me(_: &mut T)
api be safe to use in the same way as the Pin
version, is that you get some extra guarantees by forcing your users to rely on Pin
. Such as Pin
not being able to deref (safely) *pinned_var
. So they can't thwart your api. They have to go through it.
Pin Projection
Other convenience is pin projection. Typically these unsafe data structures are behind smart pointers like Box
. So Pin
is almost always a compound type. In the case of Pin<Box<T>>
, Pin
never exposes &T
or&mut T
. You only get access to Pin<&T>
and Pin<&mut T>
.
Unpin
Sometimes you will need to work with either or
- dangerous to use objects
- safe objects
with respect to the move issues I mention at the start of this post
But both these objects will be implementing some common trait. This poses a problem. If the dangerous objects only expose their api using Pin<..>
, we would have to make the trait methods use Pin<..>
. We need trait methods like fn poll(_: Pin<&mut Self>)
even though the safe objects don't care for it. So how do we not require the safe implementer to be created as a Pin<Box<T>>
, which would require a wasteful Box::pin(T)
?
The Box
is the problem there, it is not needed for the safe object. That's where Unpin
comes in. We would still need to know if a particular object is "safe" or not and when we do know, we can mark it as implementing Unpin
(all objects already do impl it and only ones with PhantomPinned
field don't).
We just need to be explicit about marking Unpin
on the trait bound of the safe object in question and if we do that, and the object is indeed Unpin
, we can store the object without Pin
, and when needed, we just call Pin::new(&mut obj)
and get access to those trait methods I described but without having to box. This is a "zero-cost" operation and only exists for the compiler to verify as the compiler.
Pin
having certain deref constraints on it's type has some implications. If we have a struct that contains at least one field with a type that doesn't impl Unpin
, that makes our struct also !Unpin
. Meaning that in a method call context where self is self: Pin<&mut Self>
, we wont be able to derefmut (borrow data in an exclusive dereference &mut self.field1
) any of our fields whether they themselves are Unpin
or not. When in this situation, we have no choice but to access any and all fields with Pin::map_unchecked_mut
.
Unsafe
I said that objects implementing a common trait, with regard to the issue of pinning, can come in two flavors. Safe to use/move regularly and unsafe ones that require being used carefully through their Pin
exposed api. But there is also a third option. One typically unsafe to use except for certain scenarios. See the problem here is overlapping api safety with common trait implementers. Given the following api
trait Trait {
fn poll1(self: Pin<&mut Self>);
fn poll2(self: Pin<&mut Self>);
}
There are three families of implementers
- Totally safe
- Unsafe with all methods
- Unsafe with only
poll1()
method.
Given these overlapping api safety concerns, there is no way to satisfy everyone except to make all the methods demand that they are used with Pin
. The Totally safe
objects don't have to worry about any of this because they impl Unpin
and type machinery at compile time that I mentioned earlier kicks in. Inconvenient to have to + Unpin
and Pin::new(..)
every time, but it's a small price.
The real issue arises with implementers 2
and 3
. The third implementer would really like to use poll1()
without having to be boxed. It's just not necessary because the unsafety of moving does not apply to them for that particular method poll1()
. But 2
can't claim to be Unpin
, because they are unsafe sometimes. In these situations we just have to trust the users of these objects to either err on the side caution and Pin<Box<T>>
all unsafe !Unpin
objects both 1
and 2
. Or if the user is really confident that they hold the third implementer, then they can call the unsafe
api for Pin
such as new_unchecked
, map_unchecked_mut
and get_unchecked_mut
on this third semi-safe implementer. What happens then happens.
I know that was really long-winded. Some technical details I mentioned might be inaccurate. But the point was for me to test if I understand the essence and purpose of Pin
and if I am incorrect, be challenged on that understanding. Pin
is just a formalization of safe api usage patterns for a specific type of problem. There are two types of users of Pin
. The ones who implement the unsafe data structures and want to enforce the api on users. And the users of those data structures who have to go through Pin
.