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 allows developers to pin an object to a position in memory so that your code can't move it anywhere.
This feature is vital when working with objects that reference other objects that tend to change their position in memory regardless of how you want it. When building data structures like linked lists or working with asynchronous code, this can affect your code and cause undefined behaviors.
How do you pin an object?
Pinning an object is simple. Rust provides a Pin
struct that allows you to pin objects. The struct is a part of Rust’s standard library; you can access it via std::pin::Pin
.
Let’s visit this example below:
struct MyStruct {
value: u32
}
fn main() {
let my_struct = MyStruct{ value: 10 };
println!("{}", my_struct.value);
}
This example contains a simple struct that we use in the main
function. In main
, we created an instance of the struct with value
as 10, and then we print that value to the console.
We can pin my_struct
with Box::pin()
. As we’ll do in the code block below.
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);
}
I haven’t discussed Box
, so let’s look into it. Box
is a struct that allows you to allocate memory in the heap memory.
Rust has two types of memory that it uses for storing values in your code: stack and heap memory. Rust stores data of predetermined size in the stack and data whose sizes are determined at runtime in the heap. Stack memory has a limited storage ability but is faster than heap memory. But heap memory is more extensive and flexible than stack memory.
Box::pin()
allocates memory in the heap and pins it in place.
The following list contains the things you need to know about pinned values.
- Modifying a pinned object
- The
_pin
field - What you risk by using pinned objects
Let’s take a look at all of these in detail!
Changing data on a pinned object
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 be afraid. There’s a way to do it, but it involves some rule-breaking processes. We’ll be doing those processes 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::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);
}
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 it won’t work.
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, we added a _pin
field to the struct in the modifications we made to our code. Now, you may ask yourself what it 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. You can apply the Box::pin()
method to a struct without the _pin
field, but it won’t pin it in the memory.
You can test the previous paragraph yourself 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 a struct. 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 that you apply it to.
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 had a reason. As it turns out, you must be extremely 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 tasks 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!