Pinning is a very confusing topic I encountered while programming in Rust. I tried hard to learn it, but the guides, articles, and videos I’ve seen are hard to understand. They usually involve having to know another complex concept in Rust, and that just made me go back and forth between other articles, videos, and guides on those other concepts.
In this article, I’ll filter out all those other concepts and focus solely on pinning. After reading this article, you’ll learn to apply it in your code and understand its use in other codes.
First, what is pinning?
Pinning is an essential feature in Rust. It is the process of locking the position of an object in memory. Pinning prevents your code from moving an object while it is using the object.
Pinning is a vital feature when you're working with code that points to other objects. Because objects tend to change their position in memory. And those changes cause any reference to the previous position to become null.
Objects changing memory location affects data structures like linked lists and a lot of asynchronous processes. The null references cause unpredictable behaviors.
How do you pin an object?
Pinning an object is simple. Rust provides a Pin
struct for pinning objects. The struct is a part of Rust’s standard library; you can access it via std::pin::Pin
.
Take a look at this example:
struct MyStruct {
value: u32
}
fn main() {
let my_struct = MyStruct{ value: 10 };
println!("{}", my_struct.value);
}
This example contains a struct, MyStruct
, that the main
function uses in the following way:
Creating an instance of
MyStruct
asmy_struct
.Printing
my_struct.value
to the terminal.
To pin my_struct
, you need to do a few things:
Add a
_pin
field toMyStruct
for storing aPhantomPinned
object.Pass
my_struct
toBox::pin
to pinmy_struct
.
use std::marker::PhantomPinned;
struct MyStruct {
value: u32,
_pin: PhantomPinned,
}
fn main() {
let my_struct = MyStruct {
value: 10,
_pin: PhantomPinned,
};
let pinned_struct = Box::pin(my_struct);
println!("{}", pinned_struct.value);
}
I haven’t discussed Box
, so let’s look into it. Box
lets you manually allocate memory in the heap storage.
Rust has two types of memory that it uses for storing values in your code: stack and heap memory. If Rust can determine the size of an object at compile time it stores that object in the stack memory. If it can't determine the size of the object it stores the data in the heap memory.
That behavior is there because stack memory is rigid but very fast. While heap memory is flexible and extensive, but slow compared to stack memory.
With Box::pin()
you can store an object in the heap memory and pin it in place.
This list contains a few more things you need to know about pinned objects.
Modifying a pinned object
The
_pin
fieldThe risks of using pinned objects
Let’s take a look at all of these in detail!
Modifying pinned objects
One of the things you’ll get stuck in immediately after pinning an object is trying to modify data on it. It can be frustrating, but there’s no need to worry. There’s a way to do it, but it involves some rule-breaking processes. So you have to do it in an unsafe
block.
Let’s revisit the last example we had in the previous section. To follow along, I’ll paste it below:
use std::marker::PhantomPinned;
struct MyStruct {
value: u32,
_pin: PhantomPinned,
}
fn main() {
let my_struct = MyStruct {
value: 10,
_pin: PhantomPinned,
};
let pinned_struct = Box::pin(my_struct);
println!("{}", pinned_struct.value);
}
Let’s say we want to change the value of my_struct.value
to 32. If we just try my_struct.value = 32
, the compiler will generate an error message telling you that it's against the rules to change the value.
To change the value of my_struct.value
, there are a few steps that you must follow:
First, collect a mutable reference to the pinned object with
Pin::as_mut(&mut my_struct)
.Then, use that mutable reference to reference the object stored in the pin with
Pin::get_unchecked_mut(mut_ref)
.Finally, use the reference to the object to modify the object however you like.
Any modifications you make will now reflect in the pinned object.
Let’s see how the code looks after following these steps.
use std::pin::Pin;
struct MyStruct {
value: u32,
_pin: PhantomPinned,
}
fn main() {
let mut my_struct: Pin<Box<MyStruct>> = Box::pin(MyStruct {
value: 10,
_pin: PhantomPinned,
});
println!("{}", my_struct.value);
unsafe {
let mut_ref: Pin<&mut MyStruct> = Pin::as_mut(&mut my_struct);
let mut_pinned: &mut MyStruct = Pin::get_unchecked_mut(mut_ref);
mut_pinned.value = 32;
}
println!("{}", my_struct.value);
}
If you run the code, it displays the value of my_struct.value
in the terminal before and after modifying it.
The _pin
field
If you noticed, you had to add a _pin
field to the struct in the first modifications to the code. Now, you may be asking yourself what the field is and what it does. That’s what we’ll cover in this section.
_pin
is a field you place in the struct you want to pin. It tells the compiler that the struct should be pinned. When you apply the Box::pin()
method to a struct without the _pin
field, the method call may be successful, but it won’t pin the object in the memory. You can test this with this code:
use std::pin::Pin;
struct MyStruct {
value: u32,
}
fn main() {
let mut my_struct: Pin<Box<MyStruct>> = Box::pin(MyStruct {
value: 10,
});
println!("{}", my_struct.value);
my_struct.value = 32; // without `_pin`, this works without any issue
println!("{}", my_struct.value);
}
When you apply the _pin
field to the struct and initialize it with PhantomPinned
, the line 14 (my_struct.value = 32;
) becomes invalid.
use std::pin::Pin;
struct MyStruct {
value: u32,
_pin: PhantomPinned,
}
fn main() {
let mut my_struct: Pin<Box<MyStruct>> = Box::pin(MyStruct {
value: 10,
_pin: PhantomPinned,
});
println!("{}", my_struct.value);
my_struct.value = 32; // This will cause a compilation error
println!("{}", my_struct.value);
}
If you look at the _pin
field, you may also be asking why you need to initialize it with PhantomPinned
. The answer is simple: PhantomPinned
is a type Rust uses to enforce the pinning rules on an object. You should always apply PhantomPinned
to the _pin
field of a struct when you want to pin it. PhantomPinned
doesn’t hold any value in memory and does nothing other than enforce pinning rules on the struct you apply it on.
What do you risk by using pinned objects?
One of the biggest problems with pinned objects is safety. Don’t get me wrong, pinning objects before using them in specific applications promotes safety. Now you might be wondering: I just said that their problem is with safety; what going on? The safety issue with pinned objects lies in using them.
You may have noticed that when modifying the pinned object, my_struct
, we had to wrap the processes in an unsafe
block. Surrounding the expressions with unsafe
had a reason. As it turns out, you should be careful when tampering with a pinned object. If you’re not, you can risk causing undefined behavior and other problems to essential parts of your code.
Our example was simple, so there wasn’t much to risk. But you still need to wrap it in an unsafe
block, even as it is.
Conclusion: why pin an object, then?
Now that you’ve gone through all that, the ultimate question remains. Why bother with pinning objects at all? This question is essential because pinning objects will hold no value without answering them.
To answer these questions, I have compiled a short list that states each of the reasons:
Pinning an object in Rust ensures that the object remains in a fixed location in memory (vital for asynchronous programming).
Pinning can help prevent data races and other concurrency issues from arising when multiple processes access the same data.
Pinning can also help improve performance by reducing the copying and moving of the code when working with asynchronous data.
Pinning ensures that certain types of data are always available in memory, even if the computer swaps out other parts of the program.
Top comments (2)
This is a fantastic article. Thanks for sharing.
Amazing article!