paint-brush
Pin Safety: Understanding Pinning in Rust Futuresby@rolaman
767 reads
767 reads

Pin Safety: Understanding Pinning in Rust Futures

by Roman LaptevJune 26th, 2023
Read on Terminal Reader
Read this story w/o Javascript
tldt arrow

Too Long; Didn't Read

Pinning is a part of Rust that helps with memory management. Pinning allows us to do more with Rust, while still keeping our programs safe. In this article, we'll dissect pinning into small, digestible pieces. We'll explain its importance, show you how to use it, and offer tips for avoiding common errors.
featured image - Pin Safety: Understanding Pinning in Rust Futures
Roman Laptev HackerNoon profile picture


Pinning is a part of Rust that helps with memory management, especially when dealing with concurrent or 'asynchronous' programming. When we pin something, we're telling Rust: "This thing right here, it's not going to move around anymore."


This might not sound like a big deal, but in a language like Rust, which is all about speed and safety, it can be very important. Pinning allows us to do more with Rust, while still keeping our programs safe.


In this article, we'll dissect pinning into small, digestible pieces. We'll explain its importance, show you how to use it, and offer tips for avoiding common errors. Whether you're new to Rust or seeking to learn something new, we've got you covered. So let's delve into pinning in Rust together!

Future trait

First, let's review what a Future in Rust is.


A Future is a core concept that underpins asynchronous programming in Rust. Simply put, a Future is a value that might not have been computed yet. It's a task waiting to be completed in the future, hence the name. This is especially useful for operations that may take some time, such as loading data from a file, making a network request, or performing a complex calculation.

Using Future allows your program to carry on with other work instead of waiting for a long task to finish, thereby improving efficiency. When the Future is ready to produce a value, it can be 'polled' to check if the value is now available.


Here's what the basic structure of a Future looks like in Rust:


pub trait Future {
    type Output;

    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output>;
}


This trait comprises of an Output type and a poll method. The Output type is the type of value that the Future will produce when it's ready. The poll method is used to check if the Future is ready to produce its value.


You may notice the Pin and Context types in the poll method. The Context is a toolbox that enables a Future to operate more efficiently. It allows the Future to sleep and wake up when ready, and it provides information that assists the Future in interacting with the system it's running on.

Pin struct

Let's explore the Pin struct in the Future trait using an example where the Future returns a u8 value.


use std::future::Future;
use std::task::{Context, Poll};
use std::pin::Pin;

struct MyStruct {
    field: u8,
}

impl Future for MyStruct {
    type Output = u8;

    fn poll(self: Pin<&mut Self>, _cx: &mut Context) -> Poll<Self::Output> {
        Poll::Ready(self.field)
    }
}


Unfortunately, this simple code will not work because the poll function takes self: Pin<&mut Self>, but we're trying to access self.field as if self was &mut Self. This is where pinning becomes essential.


Let's correct this:

use std::future::Future;
use std::task::{Context, Poll};
use std::pin::Pin;

struct MyStruct {
    field: u8,
}

impl Future for MyStruct {
    type Output = u8;

    fn poll(mut self: Pin<&mut Self>, _cx: &mut Context) -> Poll<Self::Output> {
        Poll::Ready(self.as_mut().field)
    }
}


In this example, self.as_mut().field works because Pin::as_mut gives us a Pin<&mut T>, allowing us to access field securely. So why do we need the Pin wrapper for self at all?

The Pin wrapper is used in the poll method of the Future trait in Rust for a very specific reason: safety when dealing with self-referential or movable types. When we talk about a Future, it often encapsulates some sort of operation that can't be completed immediately — it might take some time, might involve IO operations or might depend on other Futures.


During the lifecycle of a Future, it's polled multiple times until it's ready to yield a result. Between these polls, the Future could be moved around in memory, and this is where the Pin comes in.


Imagine a Future that has a reference to its own internal data. In Rust, when we move a value, we're essentially copying its data to a new location and then invalidating the old location. If the Future were to move in memory while holding a reference to its own internal data, that reference could become invalid and lead to undefined behavior, which is a big no-no in Rust. Pin ensures that the Future is "pinned" in place and won't be moved in memory, allowing us to safely hold these internal or "self-referential" pointers. So, Pin's role is about ensuring safety and soundness when dealing with these Futures or any self-referential structures, in an asynchronous context.


This is why we use Pin<&mut Self> in the signature of the poll method. It signifies that the Future will not and cannot move around in memory anymore, ensuring that any internal references remain valid.


Let's consider another example where we need to mutate data inside the Future.

use std::future::Future;
use std::pin::Pin;
use std::task::{Context, Poll};

struct MyFuture {
    some_data: i32,
}

impl Future for MyFuture {
    type Output = i32;

    fn poll(mut self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<Self::Output> {
				// Gets a mutable reference
        let self_mut = self.as_mut().get_mut();
				// Change data
        self_mut.some_data += 1;
        if self_mut.some_data > 10 {
            Poll::Ready(self_mut.some_data)
        } else {
            Poll::Pending
        }
    }
}


In this example, we've created a Future that increments its internal some_data field each time it's polled. This Future isn't considered 'ready' until some_data has been incremented to a value greater than 10. Essentially, this Future is 'counting to 10', asynchronously, before it yields its final result. Pin::as_mut and Pin::get_mut methods are used to get a mutable reference to the data inside the Pin. This allows you to mutate the data without violating the guarantee that the data will not be moved.


Bear in mind that handling Pin can be complicated, especially when you have futures that hold self-referential pointers or non-movable data. Always ensure you're upholding Rust's safety guarantees.

Summary

In this article, we've explored the concept of pinning in Rust, specifically in the poll method of Rust's Future trait. Pinning, by ensuring that a Future doesn't change its memory location, aids in maintaining safety during asynchronous programming.


We demonstrated a basic Future, where the poll method returned an immediately completed value, highlighting the role of Pin. Then we delved into manipulating data within a Pin using Pin::as_mut and Pin::get_mut, emphasizing how to work with a Pin while keeping Rust's safety rules. Understanding pinning in Rust, though complex, opens up new paths in writing safe, efficient Rust code.