rust and pin
Pin is one of the toughest concepts in Rust, in part due to its unfamiliarity. There was one piece of it that always had me confused. And I’ll be the first admit, after it clicking, I’m rather doubtful of my own intelligence. It’s to do with stack pinning. For a full treatment on Pin, which I happily omit, see any of these wonderful articles.
My foolishness with Pin starts here
1use std::pin::Pin;
2use std::marker::PhantomPinned;
3
4struct P {
5 value: u32,
6 _pin: PhantomPinned,
7}
8
9fn main() {
10 let mut p = P { value: 2, _pin: PhantomPinned };
11 let pinned = unsafe { Pin::new_unchecked(&mut p) };
12
13 let y = pinned; // why is this assignment allowed?
14}
The thinking, albeit flawed, goes like this: I have an object p
which I’ve dutifully pinned to the
stack, called pinned
. If I move pinned
, I’m violating the contract. Reassigning may lead to a
move in actual memory. How then, am I allowed to reassign pinned
?
Well, the answer is obvious. pinned
does not own p
, it only holds a reference i.e the address
to the value P
. So if I move pinned
, I don’t move the underlying value. I just keep moving
around my pointer to P
, no issues.
Armed with this understanding, I have further clarity on the ways in which you can violate the pinning contract. Take this example:
1use std::pin::Pin;
2use std::marker::PhantomPinned;
3
4struct P {
5 value: u32,
6 _pin: PhantomPinned,
7}
8
9fn main() {
10 let mut p = P { value: 2, _pin: PhantomPinned };
11 let pinned = unsafe { Pin::new_unchecked(&mut p) };
12
13 let y = p;
14}
This is wrong. I have an object p
and pinned it, therefore guaranteeing I won’t move it ever
again. I then proceeded to reassign p
, which may lead to a mechanical move in memory. Bad news.
Here’s another:
1use std::pin::Pin;
2use std::marker::PhantomPinned;
3
4struct P {
5 value: u32,
6 _pin: PhantomPinned,
7}
8
9fn main() {
10 let mut p1 = P { value: 2, _pin: PhantomPinned };
11 let mut p2 = P { value: 4, _pin: PhantomPinned };
12 let pinned = unsafe { Pin::new_unchecked(&mut p1) };
13
14 std::mem::swap(&mut p1, &mut p2);
15}
We told the compiler, we’re pinning p1
. And then we swapped p1
and p2
around. Incredible.
Valid? Yes. Right? No.
The simple fix for this is to use the pin!
macro. The “trick” it uses is to shadow the original
value so we don’t have access to the original variable
1use std::pin::Pin;
2use std::marker::PhantomPinned;
3
4struct P {
5 value: u32,
6 _pin: PhantomPinned,
7}
8
9fn main() {
10 let mut p1 = P { value: 2, _pin: PhantomPinned };
11 let mut p2 = P { value: 4, _pin: PhantomPinned };
12 let p1 = unsafe { Pin::new_unchecked(&mut p1) };
13
14 std::mem::swap(&mut p1, &mut p2); // won't compile
15}
1error[E0308]: mismatched types
2 --> src/main.rs:15:26
3 |
415 | std::mem::swap(&mut p1, &mut p2);
5 | -------------- ^^^^^^^ expected `&mut Pin<&mut P>`, found `&mut P`
6 | |
7 | arguments to this function are incorrect
8 |
9 = note: expected mutable reference `&mut Pin<&mut P>`
10 found mutable reference `&mut P`