Why is std::pin::Pin so weird?

Values of certain Rust types need to be pinned, which prevents them from moving in memory. This is expressed via the std::pin::Pin wrapper type and is typically encountered in the form of a function accepting Pin<&mut T> instead of &mut T. Pinning makes using many “normal” programming techniques difficult and has strange side-effects that do not have an obvious connection to the purpose of pinning.

This article briefly introduces what pinning is, explores what is happening in common confusing situations and attempts to explain the strange side-effects by identifying the reasons they occur. It is a collection of lessons learned to try shorten readers’ path to pinning mastery.

Note: for complete code samples that you can compile, refer to pins-in-rust/examples at main · sandersaares/pins-in-rust. Snippets below may omit less relevant content in the interest of readability.

Why does pinning exist?

It is possible to define a Rust type that stops working correctly when it is moved to a different location in memory. The easiest way to do this is to create a type that holds a pointer or reference to itself. Let’s define such a type. We start with a type that does not use pinning. This first example is intentionally wrong – the code compiles but contains undefined behavior, which is always a programming error.

// BAD CODE: This example is intentionally wrong.
pub struct BagOfApples {
count: usize,
// In the example code, this self-reference is mostly useless.
// This is just to keep the example code simple – the emphasis is
// on the effects of pinning, not why a type may be designed to need it.
self_reference: *mut BagOfApples,
}
impl BagOfApples {
pub fn new() -> Self {
BagOfApples {
count: 0,
// We cannot set this here because we have not yet
// created the BagOfApples – there is nothing to reference.
self_reference: ptr::null_mut(),
}
}
/// Call this after creating a BagOfApples to initialize the instance.
pub fn initialize(&mut self) {
self.self_reference = self;
}
pub fn count(&self) -> usize {
assert!(
!self.self_reference.is_null(),
"BagOfApples is not initialized"
);
// SAFETY: Simple read-only access to the count field, which
// is safe. We do it via the pointer for example purposes.
unsafe { (*self.self_reference).count }
}
}
// BAD CODE: This example is intentionally wrong.
view raw 01.rs hosted with ❤ by GitHub

This article will not examine why or how types that require pinning are useful – we just accept the fact that such types exist and must sometimes be used in the code we write. Accordingly, the example types in this article are not “meaningful” but merely contrived stand-ins that require pinning.

If used in a simple manner, the code above does technically work:

let mut bag = BagOfApples::new();
bag.initialize();
println!("Apple count: {}", bag.count());
view raw 02.rs hosted with ❤ by GitHub

However, if we were to move this type to a different location in memory, the pointer in self_reference would become invalid because it would be pointing to a location that is no longer occupied by the BagOfApples. Attempting to read or write through an invalid pointer is a programming error (even if it sometimes appears to work).

// BAD CODE: This example is intentionally wrong.
let mut bag = BagOfApples::new();
bag.initialize();
// We move the bag into a box, which is a different memory location.
// Invalid code: it is not legal to move the bag after creation because it is
// self-referential. BagOfApples is an unsound type because it allows this.
let boxed_bag = Box::new(bag);
// This could result in invalid memory access, causing undefined behavior.
// It may (appear to) work depending on circumstances and what compiler
// optimizations are applied in specific situations but this does not make it valid.
println!("Apple count: {}", boxed_bag.count());
// BAD CODE: This example is intentionally wrong.
view raw 03.rs hosted with ❤ by GitHub

In its current form, the BagOfApples type we created is unsound, meaning that it exposes a safe API that makes it possible for the user of the type to cause undefined behavior, for example by moving the value between calling initialize() and count().

There are two ways to solve this soundness problem:

  1. We can mark the initialize() function as unsafe and declare in the function comments that the user is now responsible for ensuring that the value is never moved. This would be technically correct because it would make the invalid memory access the fault of the user – the code would no longer be unsound. However, this design choice is not particularly helpful because the only way to identify when the value is moved is by carefully reviewing the code. A well-designed API does not burden the programmer with extra code review duties.
  2. We can require that the type be pinned before initialize() or count() can be called. Pinning is a compile-time signal that says “this value is not allowed to move”.

As the former option would be poor API design, let’s use the latter and modify our type to require pinning. There are two required modifications.

First, we must add a member of type PhantomPinned to BagOfApples. This is a marker for the compiler, stating that values of our type must be pinned.

pub struct BagOfApples {
count: usize,
// In the example code, this self-reference is mostly useless.
// This is just to keep the example code simple – the emphasis is
// on the effects of pinning, not why a type may be designed to need it.
self_reference: *mut BagOfApples,
_require_pin: std::marker::PhantomPinned,
}
pub fn new() -> Self {
BagOfApples {
count: 0,
// We cannot set this here because we have not yet
// created the BagOfApples – there is nothing to reference.
self_reference: ptr::null_mut(),
_require_pin: std::marker::PhantomPinned,
}
}
view raw 04.rs hosted with ❤ by GitHub

Then we need to change the initialize() method to accept a pinned reference.

/// Call this after creating a BagOfApples to initialize the instance.
pub fn initialize(mut self: Pin<&mut Self>) {
// SAFETY: BagOfApples requires pinning and we do not allow
// the obtained pointer to be exposed outside this type, so
// we know it always points to a valid value and therefore it
// is safe to store the pointer. We have also reviewed the code of
// this function to ensure that we do not move the BagOfApples
// instance via the reference we obtain from here nor via the pointer.
let self_mut = unsafe { self.as_mut().get_unchecked_mut() };
self_mut.self_reference = self_mut;
}
view raw 05.rs hosted with ❤ by GitHub

The details of how this method is implemented are described in a later chapter. The main thing to note here is that the parameter changed from &mut self to mut self: Pin<&mut Self>. This change is a signal to the compiler that the method can only be called on pinned values. It is also a hint of the enforcement mechanism we will soon see in action – pinning is enforced by the compiler when methods of our pinning-required type are called.

The code using our type must now also change but we will come back to that shortly. Before that, let’s ask an obvious question.

Why is this code with Pin better?

The purpose of pinning is to cause a compile-time error whenever the possibility exists for a pinning-required type to move in memory.

In other words, our previous “bad” example code that tried to move the BagOfApples no longer compiles. That is the entire point of pinning!

let mut bag = BagOfApples::new();
bag.initialize();
// ^ ERROR
// no method named `initialize` found for struct `better::BagOfApples` in the current scope
// method not found in `BagOfApples`
// lib.rs(66, 5): method `initialize` not found for this struct
// lib.rs(114, 9): consider pinning the expression: `let mut pinned = std::pin::pin!(`, `);
view raw 06.rs hosted with ❤ by GitHub

Success! Pinning works! To be clear, this is the only thing that pinning does – it causes compile-time errors. As long as the value in question is never actually moved, there is no difference at runtime between using a pinned type and using a non-pinned type.

The weirdness of Pin starts here. While sometimes the pinning-related compile-time errors genuinely help the programmer by turning undefined behavior into a compile time error, at other times the compile errors appear even with “logically correct” code and cause frustration, requiring us to jump through extra hoops to make the compiler happy. Let’s examine some typical scenarios.

The “better” code does not compile – help!

The first problem pinning creates for us is obvious – we get a compile error when we try to call initialize() which means two things:

  • the “bad code” example that caused undefined behavior by moving the BagOfApples is now broken; that’s good!
  • the original simple example that does not move BagOfApples is now also broken because it too calls initialize(); not so good!

This is expected. Pinning not only causes compile errors when at risk of undefined behavior but also requires correct code to be written differently. Think of it as extra work you must do to help the compiler figure out when the moves may happen.

Let’s revisit the phrasing at the start of the previous chapter: “the purpose of pinning is to cause a compile-time error whenever the possibility exists for a pinning-required type to move in memory”. All the side-effects and weirdness that come with Pin are a result of this statement. Pinning does not prevent merely the moves themselves, pinning prevents situations where moves are at all possible! This means pinning can get in the way of things when we try doing something that is not necessarily related to moving a value, merely because such an act could theoretically allow the value to be moved if we chose to.

Let’s modify the code so it can successfully use the BagOfApples in the scenario where no moving of the value occurs:

let mut bag = Box::pin(BagOfApples::new());
bag.as_mut().initialize();
println!("Apple count: {}", bag.count());
view raw 07.rs hosted with ❤ by GitHub

The changes here are:

  1. We wrap the BagOfApples with a Box::pin() call. This pins the value and returns a Pin<Box<BagOfApples>>.
  2. We call .as_mut() before calling .initialize() to obtain the form of self that matches the parameter in the updated initialize() method, giving us a Pin<&mut BagOfApples>.

This example uses Box::pin() to pin the value. There are different ways to pin values but the details of how to achieve pinning are outside the scope of this article. Box::pin() is merely the most straightforward and general-purpose way to achieve pinning – it is a reasonable default choice if unsure.

Warning: avoid the pin! macro unless you have a very good understanding of what it does. This macro is sometimes over-eagerly suggested by the compiler as the “solution” to pinning-related errors (as you may already have noticed in the compiler error listed in the previous chapter). However, this macro is only applicable under specific conditions and often not the right tool for the job.

Why did initialize() become so weird when count() did not need to change?

You may already have forgotten about count() but here it still is in its original form, compiling just fine with no hint of Pin. A breath of fresh uncomplicated air but why?

pub fn count(&self) -> usize {
assert!(
!self.self_reference.is_null(),
"BagOfApples is not initialized"
);
// SAFETY: Simple read-only access to the count field, which
// is safe. We do it via the pointer for example purposes.
unsafe { (*self.self_reference).count }
}
view raw 08.rs hosted with ❤ by GitHub

This is because it is impossible to move something through a shared reference (&self) because such references by design provide a read-only view of the value. Therefore, a pinned value can always be dereferenced via a shared reference.

Only functions that take exclusive references (&mut self) or owned values (self) have to change for pinned types, as only those functions could possibly move the value.

Non-pinned valuePinned value
Shared reference&T&T
or optionally Pin<&T>
Exclusive reference&mut TPin<&mut T>
Owned valueTPin<Box<T>> or similar

Given that a Pin can always be dereferenced into a shared reference, the author is not aware of situations where one would be required to use Pin<&T> over &T. If you know of such cases, please leave a comment!

Pinned types have a wild childhood

Let’s immediately create a seeming contradiction with the above table and add a &mut self function to our pinning-required example type. For example, we can define a set_capacity() that updates a capacity field on the struct:

pub struct BagOfApples {
count: usize,
capacity: usize,
// …
}
impl BagOfApples {
// …
pub fn set_capacity(&mut self, capacity: usize) {
self.capacity = capacity;
}
// …
}
view raw 09.rs hosted with ❤ by GitHub

So far so good but of course we have not yet called this method so perhaps that is just a coincidence – recall that pinning is primarily enforced when methods are called. Indeed, if we just add this method call into our code, we get a familiar looking error:

let mut bag = Box::pin(BagOfApples::new());
bag.as_mut().initialize();
// error[E0596]: cannot borrow data in dereference of `Pin<Box<BagOfApples>>` as mutable
bag.set_capacity(123);
// error[E0596]: cannot borrow data in dereference of `Pin<&mut BagOfApples>` as mutable
bag.as_mut().set_capacity(123);
view raw 10.rs hosted with ❤ by GitHub

A common “technique” people tend to apply when overwhelmed by pinning related errors is to try spam .as_mut() everywhere to try resolve them. As you can see above, it does not matter if we try it with or without the .as_mut() – either way the compiler tells us “no” because it cannot give us a &mut self from the pinned value. After all, an exclusive &mut self reference would allow the value to be moved! Pinning is working as intended by blocking that from happening.

However, we do not have to pin our BagOfApples immediately! We can create it as a regular non-pinned value, call set_capacity(), and only then pin it and operate on it as a pinned value.

let mut bag = BagOfApples::new();
bag.set_capacity(123);
let mut bag = Box::pin(bag);
bag.as_mut().initialize();
println!("Apple count: {}", bag.count());
view raw 11.rs hosted with ❤ by GitHub

That works just fine – great success! This example demonstrates that pinning is a part of the value’s lifecycle – there is a wild non-pinned childhood even for every pinning-required type. How to make use of this lifecycle phase is up to the author of the type to decide, by defining methods that accept &mut self, which are intended to be called in this phase.

Every instance of a Rust type starts out as non-pinned. A value only becomes pinned when you use Box::pin() or some other pinning mechanism to wrap it with Pin.

Can the Pin wrapper be removed once a type is pinned?

Not in safe code – once something is pinned, it stays pinned and remains inside the Pin wrapper forever. However, it is still possible and often even appropriate to get a regular &mut exclusive reference to the value temporarily. There are two ways to do this and both come with strings attached.

Let’s set the scene by defining a trait for a bag of fruit. We define just one function to set the bag capacity. We predict that implementations may need to be pinned, so we use a pinned form for the self parameter.

pub trait BagOfFruit {
fn set_capacity(self: Pin<&mut Self>, capacity: usize);
}
view raw 12.rs hosted with ❤ by GitHub

Next, we implement a BagOfBananas, which is a type that requires pinning (just for example purposes).

pub struct BagOfBananas {
capacity: usize,
_require_pin: std::marker::PhantomPinned,
}
view raw 13.rs hosted with ❤ by GitHub

Now we need to implement BagOfFruit::set_capacity() for this type. No matter how we approach the situation, to set the value of the capacity field, we need either an owned value (mut BagOfBananas) or an exclusive reference (&mut BagOfBananas). Pinning is a mechanism whose entire purpose is to prevent us from getting either of those once we have pinned the value (i.e. added the Pin wrapper type)! Given that self does indeed have the Pin wrapper in set_capacity(), we appear to be stuck.

With the BagOfBananas type, there is only one way out of this – we must make a trade with the compiler. In return for the compiler giving us a &mut BagOfBananas we must promise to take over some of its responsibilities and ensure that the value does not get moved while we have the exclusive reference.

impl BagOfFruit for BagOfBananas {
fn set_capacity(mut self: Pin<&mut Self>, capacity: usize) {
// SAFETY: BagOfBananas requires pinning. We have reviewed the code
// in this function to ensure that we do not move the BagOfBananas
// instance via the reference we obtain from here.
let self_mut: &mut BagOfBananas = unsafe {
self.as_mut().get_unchecked_mut()
};
self_mut.capacity = capacity;
}
}
view raw 14.rs hosted with ❤ by GitHub

The function get_unchecked_mut() returns a regular &mut exclusive reference but is marked unsafe. This means that calling it is only possible inside an unsafe block and requires the caller to promise that the value will not get moved. The safety comment explains how we ensure that we fulfill this promise. Each unsafe function has a chapter in its documentation to describe what is the promise that must be upheld to use that function – different unsafe functions require different promises from the programmer.

To be clear, unsafe blocks do not allow the Rust programming rules to be violated, they merely transfer some of the responsibility from the compiler to the programmer. The pinned value is still not allowed to move but some of the guardrails preventing that are lowered temporarily.

The above is the general-purpose mechanism to get a &mut exclusive reference to a pinned value. However, there also exists a special case if the type in question satisfies certain criteria (which was not the case with BagOfBananas).

A fact that the preceding chapters glossed over is that you can pin anything, including values of types that do not require pinning. For example, you can pin an integer:

let pinned = Box::pin(42);
println!("Pinned integer: {}", *pinned);
view raw 15.rs hosted with ❤ by GitHub

Doing this in the above example is pointless – an integer does not require pinning, so the only thing you get from pinning an integer is extra complexity in usage of the value due to the special constraints Pin forces on you.

However, circumstances may force you to do this. For example, if you are the author of a BagOfOranges type which does not require pinning, you may still want to implement the BagOfFruit trait that is designed to be used with pinned values (because some types of bags may need it).

While you could create this BagOfOranges type using the same patterns as BagOfBananas above, treating it as a pinning-required type even if it does not actually require pinning, there is also a simpler way:

pub struct BagOfOranges {
capacity: usize,
}
impl BagOfFruit for BagOfOranges {
fn set_capacity(mut self: Pin<&mut Self>, capacity: usize) {
// This type does not require pinning, so we can simply dereference.
let self_mut: &mut BagOfOranges = &mut self;
self_mut.capacity = capacity;
}
}
view raw 16.rs hosted with ❤ by GitHub

Wait, what?! The compiler suddenly allows us to simply create a &mut exclusive reference. No unsafe block required! No compile error!

Did you notice the other difference? This is the key to what makes it possible: there is no PhantomPinned member in the struct! This member, present in BagOfBananas we defined earlier, was a marker to the compiler, indicating that the type requires pinning.

That is the special condition that applies here: you can dereference a Pin<&mut T> into a regular &mut T if the compiler can identify that the type does not actually require pinning. In the Rust ecosystem this is expressed as implementing the Unpin auto trait, with the presence of PhantomPinned (or a field of some other type that requires pinning) removing this auto trait implementation.

Non-pinned valuePinned value
(type requires pinning)
Pinned value
(type does not require pinning)
T implements UnpinNoYes
Shared reference&T&T
or optionally Pin<&T>
&T
or optionally Pin<&T>
Exclusive reference&mut TPin<&mut T>Pin<&mut T> or &mut T
Owned valueTPin<Box<T>> or similarPin<Box<T>> or similar

This finding also extends the above table with a new column, to distinguish what can be done with pinned types that do or do not require pinning.

How does any of this actually prevent moving the value?

The above chapters describe a great deal of ceremony around the use of pinning but it may not be obvious how any of this achieves the goal of preventing the value from being moved.

The mechanism achieves its goal by the fact that all standard library functions that could move a value take an exclusive reference (&mut). It is not possible to get an exclusive reference to a pinned value. Therefore, a pinned value cannot be moved by any standard library code.

For code that you may write yourself or that some 3rd party crate may contain, the possibility exists to create a custom function that moves a pinned value. However, in this case it would be an intentional act and the author of this code would be responsible for ensuring that the move does not violate any Rust programming rules.

The pinning mechanism helps highlight the risk in user code by requiring unsafe blocks to obtain a &mut reference to a pinned value. This puts the responsibility on the author of such code to ensure that the safety expectations of the type are upheld.

/// Call this after creating a BagOfApples to initialize the instance.
pub fn initialize(mut self: Pin<&mut Self>) {
// SAFETY: BagOfApples requires pinning and we do not allow
// the obtained pointer to be exposed outside this type, so
// we know it always points to a valid value and therefore it
// is safe to store the pointer. We have also reviewed the code of
// this function to ensure that we do not move the BagOfApples
// instance via the reference we obtain from here nor via the pointer.
let self_mut = unsafe { self.as_mut().get_unchecked_mut() };
self_mut.self_reference = self_mut;
}
view raw 05.rs hosted with ❤ by GitHub

You should now understand how and why we implemented the initialize() method in our first example the way we did – it uses an unsafe block to obtain an exclusive reference (&mut) to update the self_reference field. Whenever an exclusive reference is obtained, we rely on the author of the code to uphold any safety expectations of the type, transferring responsibility from the compiler to the programmer.

What exactly these expectations are depend on the specific type being used (e.g. which fields are safe to modify, which are not, and during which part of the value’s lifecycle). The author of the code is expected to document how they uphold the type’s safety requirements in a safety comment, as shown above.

Pinned fields

Everything above dealt with variables containing pinned values. However, another important place where values exist is in the fields of a struct. Consider a scenario where we have a FruitStand struct that must contain multiple different fruit bags. Without any pinning, it might look like this:

struct FruitStand {
apples: BagOfApples,
oranges: BagOfOranges,
bananas: BagOfBananas,
total_sold: usize,
}
view raw 17.rs hosted with ❤ by GitHub

We know that the fruit bags have methods on them similar to fn sell_one(self: Pin<&mut Self>), which take Pin<&mut Self> as a parameter, indicating that values of these types must be pinned before we can call these methods. At the same time, we also have the field total_sold, which is just an integer that does not require pinning.

For each fruit bag, we need to somehow obtain the Pin<&mut Self> that calling such a sell_one() method requires. As one option, we could simply allocate each fruit bag separately as a pinned box. This allows the patterns already familiar from earlier in the article to be used in the FruitStand methods:

struct FruitStand {
apples: Pin<Box<BagOfApples>>,
oranges: Pin<Box<BagOfOranges>>,
bananas: Pin<Box<BagOfBananas>>,
total_sold: usize,
}
impl FruitStand {
fn new() -> Self {
FruitStand {
apples: Box::pin(BagOfApples::default()),
oranges: Box::pin(BagOfOranges::default()),
bananas: Box::pin(BagOfBananas::default()),
total_sold: 0,
}
}
fn sell_one_of_each(&mut self) {
self.apples.as_mut().sell_one();
self.oranges.as_mut().sell_one();
self.bananas.as_mut().sell_one();
self.total_sold += 3;
}
}
view raw 18.rs hosted with ❤ by GitHub

This essentially works but is not good code because:

  • It splits a single memory allocation (the FruitStand) into four separate memory allocations: the FruitStand, plus one allocation for each fruit bag. This is quite inefficient.
  • The boxes force the fruit bags to be allocated on the heap, which prevents the compiler from placing the data on the stack when possible. This is inefficient.
  • Allocating boxes this way may be incompatible with the intended architecture of the app/service/library being written.

Therefore, the above solution should be avoided.

A better option is to instead require the entire FruitStand to be pinned. This leaves the caller and the compiler a great deal of flexibility in how the object is allocated while still enabling it to be allocated as a single object, avoiding the pitfalls of the previous solution.

Let’s try define a FruitStand that requires itself to be pinned. The most obvious change is that sell_one_of_each() must now take self as a Pin<&mut Self>. Before we even get to calling this method, however, we already see a compile error:

#[derive(Default)]
struct FruitStand {
apples: BagOfApples,
oranges: BagOfOranges,
bananas: BagOfBananas,
total_sold: usize,
}
impl FruitStand {
fn sell_one_of_each(self: Pin<&mut Self>) {
self.apples.sell_one();
// error[E0599]: no method named `sell_one` found for struct `BagOfApples` in the current scope
// –> examples\05_project.rs:85:21
// |
// 4 | struct BagOfApples {
// | —————— method `sell_one` not found for this struct
// …
// 85 | self.apples.sell_one();
// | ^^^^^^^^ method not found in `BagOfApples`
// |
// help: consider pinning the expression
// |
// 85 ~ let mut pinned = std::pin::pin!(self.apples);
// 86 ~ pinned.as_mut().sell_one();
// |
self.oranges.sell_one();
self.bananas.sell_one();
self.total_sold += 3;
}
}
view raw 19.rs hosted with ❤ by GitHub

What’s going on here? We have a combination of two factors:

  1. sell_one() requires the bag of apples to be pinned: fn sell_one(self: Pin<&mut Self>).
  2. Meanwhile, the expression self.apples returns a regular &BagOfApples shared reference.

That is a mismatch! The compiler cannot go from &BagOfApples to Pin<&mut BagOfApples>. Not only because of the lack of Pin but also because the former is a shared reference (&T), whereas you need an exclusive reference (&mut T) for the latter.

This highlights a potentially surprising behavior: the compiler treats fields of pinned types as not pinned by default.

Even if we wanted to empower the compiler to “just do the right thing” here, whether a field of a pinned type needs to be treated as pinned or not depends on whether the author of the field’s type designed it to be used via the Pin wrapper or not (i.e. it depends on the type of the self parameter in the type’s methods). The compiler does not know what is the right choice.

We need to help the compiler and tell it that when our FruitStand type is pinned, certain fields of it need to also be treated as pinned. We do not want all fields to be treated as pinned because not all types are designed to be pinned (i.e. some fields like total_sold may need to be mutated via regular &mut T exclusive references instead of Pin<&mut T>).

This functionality is enabled by the pin_project crate, which we can add to the project via cargo add pin_project. Our updated FruitStand is as follows:

#[derive(Default)]
#[pin_project]
struct FruitStand {
#[pin]
apples: BagOfApples,
#[pin]
oranges: BagOfOranges,
#[pin]
bananas: BagOfBananas,
total_sold: usize,
}
impl FruitStand {
fn sell_one_of_each(mut self: Pin<&mut Self>) {
let self_projected = self.as_mut().project();
self_projected.apples.sell_one();
self_projected.oranges.sell_one();
self_projected.bananas.sell_one();
*self_projected.total_sold += 3;
}
}
view raw 20.rs hosted with ❤ by GitHub

The #[pin] marker signals which fields need to be treated as pinned if the parent type is pinned. To obtain a version of self that matches this marking in sell_one_of_each(), we must call .as_mut().project(). And now it works!

This is the general purpose mechanism to work with pinned fields. Adding all the various .as_mut() and .project() can sometimes get annoyingly verbose if you are dealing with multiple layers of nested types but that is just how it is – accept it as a necessary evil.

Avoid relying on the Unpin auto trait

As a final caution, note that IDEs and AI assistants very often try generating pinning-related Rust code that only works with types that do not actually require pinning (i.e. that implement the Unpin auto trait). Whenever you see the term unpin in generated code, be suspicious. Whenever you see a Pin wrapper magically disappear as a &mut T exclusive reference pops out, be suspicious.

It is very easy to accidentally learn incorrect pinning-related patterns that only work with types that do not actually require pinning. This can not only result in writing code that by accident only works with a subset of pinned types but can delay achieving a proper understanding of the topic and make it seem more confusing than it really is.

More reading

std::pin – Rust

Pin in std::pin – Rust

Unpin in std::marker – Rust

pin_project – Rust

https://without.boats/blog/pin/

Source code

pins-in-rust/examples at main · sandersaares/pins-in-rust

4 thoughts on “Why is std::pin::Pin so weird?

  1. the author is not aware of situations where one would be required to use Pin<&T> over &T

    There is an important interaction between `Pin`, and “the `Drop` guarantee”, which makes it so even `Pin<&T>` is useful. See https://docs.rs/own-ref/0.1.0-alpha/own_ref/pin/index.html#the-drop-guarantee-of-pin-in-a-nutshell-illustrated-by-a-silly-api for a “silly” example, or https://github.com/conradludgate/pin-scoped/tree/958649f6312e203a29794591503a21b20c0c9d6f for a non-silly version thereof, wherein this can be important/useful.

    The gist of the idea is that whenever an arbitrary API ever so much as glimpses a `Pin<&’whatever T>`, when `T : !Unpin` and `needs_drop::<T>()`, then such API is allowed to, using `unsafe` code or whatnot, _lengthen_ the lifetime of the reference to `&’drop T`, in pseudo-code. This is not something lifetimes or static/type-level analysis can detect, so this will often involve some runtime mechanism, such as some form of synchronization or “signal-passing”.

    Like

  2. Given that a Pin can always be dereferenced into a shared reference, the author is not aware of situations where one would be required to use Pin<&T> over &T. If you know of such cases, please leave a comment!

    Another example is:

    If your type contains interior mutability (Mutex, RefCell, …) then you are going to need the Pin<&YourType> guarantee to get a Pin<&mut Inner>

    Like

    1. do note that this is an interior-mutable _pinning projection_. Meaning such a wrapper must very much not offer, simultaneously, a `&Self -> &mut Inner` operation. Hence, for instance, why none of the stdlib shared mutability wrappers offer such a pinning projection

      Like

  3. Given that a Pin can always be dereferenced into a shared reference, the author is not aware of situations where one would be required to use Pin<&T> over &T. If you know of such cases, please leave a comment!

    If your type contains interior mutability (Mutex, RefCell, …) then you are going to need the Pin<&YourType> guarantee to get a Pin<&mut Inner>

    Like

Leave a comment