Advantage of Pin over Box

Hello all,

I stiil do not understand the advantage of a Pin<Box<T>> over a Box<T> (in keeping the allocation of the allocated T unchanged). I think there is no way to reallocated a T when it is allocated by boxing and Pin’s document says that:

Pin<Box<T>> functions much like a regular Box<T>

So except unsafe methods, in which cases Pin<Box<T>> behaves differently from Box<T>, where T: Sized?

Pin is only supposed to be used in order to coordinate unsafe Rust with safe Rust. It’s guarantees are rather subtle so I would avoid it if you can.

1 Like

As @KrishnaSannasi said, Pin is a bit of an advanced topic.

What this pointer/reference wrapper does is to provides partial support for so-called immovable types (objects whose data cannot be moved around in memory safely, typically because it contains pointers to itself), by making operations which would allow one to move a value of that type around unsafe.

APIs that consume these pinned pointers are then responsible for making sure that the safe operations which they expose on the pinned type do not allow their user to move the value around.

Pin manages this by wrapping something that has mutable reference semantics (like &mut T, but also Box<T>, Rc<T>…) and making mutable access to the inner value unsafe if the Unpin marker trait is not implemented. Most types will automagically implement Unpin, but self-referential types won’t.

4 Likes

It’s about the possibility of moving the pointee.

An example, when T : Sized + !Unpin, is that you can unbox a Box<T> with the dereference operator, but you cannot unbox a Pin<Box<T>>:

mod lib {
    use ::core::{
        pin::Pin,
        marker::PhantomPinned,
    };

    pub
    struct PinSensitiveInt {
        int: i32,
       _pin_sensitive: PhantomPinned,
    }

    impl PinSensitiveInt {
        pub
        fn new (int: i32) -> Self
        {
            Self { int, _pin_sensitive: PhantomPinned }
        }
    }
}

use ::std::pin::Pin;
use lib::PinSensitiveInt;

fn main ()
{
    // 42 is in the heap
    let boxed_pin_sensitive_int: Box<PinSensitiveInt> =
        Box::new(PinSensitiveInt::new(42))
    ;
    // 42 is now in the stack
    let unboxed_pin_sensitive_int: PinSensitiveInt =
        *boxed_pin_sensitive_int
    ;

    // whereas:
    // 42 is in the heap
    let pinned_boxed_pin_sensitive_int: Pin<Box<PinSensitiveInt>> =
        Box::pin(PinSensitiveInt::new(42))
    ;
    // it is now impossible from within safe code to move that 42 into the stack (or anywhere else)
    let unboxed_pin_sensitive_int: PinSensitiveInt =
         *pinned_boxed_pin_sensitive_int // Error
    ;
}

The cases that rely on the guarantee that the pointee is not moved are very specific and involve raw pointers and thus unsafe code (for instance, when wanting to soundly implement a self-referential struct).

Most often than not, it will be used as an internal tool for async / await, given that there are, imho, easier (and thus safer) to use patterns for self-referential structs, such as @KrishnaSannasi relative pointers.

2 Likes

As @HadrienG said, most types will implement Unpin and self-referential types won’t implement Unpin, but only if the writer of the self-referential type is aware of Unpin and puts a PhantomPinned inside their self-referential type. (or implements !Unpin for Type)

(thanks for the shout-out @Yandros)

2 Likes

If you are using Pin to create a self-referential type and aren’t aware of Unpin then you likely have many more issues than just not putting a PhantomPinned into your type (the amount of unsafe code you need to use to actually create a self-referential type with Pin should really make you read the docs and spend time internalizing how it works before you actually use it).

2 Likes

That is true, but if you used my relative pointer crate that @Yandros showed, then you could create a self-referential type that was Unpin (even better, they can be safely moved after setting the relative pointer). So it’s not necessary to make a self referential type !Unpin. Otherwise I completely agree with you, if you are doing anything with self-referential types read the docs thoroughly and really understand it.

1 Like

Sure, but if you’re not using Pin for the safety of your self-referential type, then Unpin doesn’t matter, it’s only when you’re using the guarantees that Pin gives you that you need to obey its requirements (which includes having a correct implementation for Unpin).

Being a 100% library solution means that someone could literally copy-paste the Pin code into another crate and have an identical parallel implementation that doesn’t interact in any way. You could have a type Foo: std::pin::Unpin + !mypin::Unpin and safely use that as a self-referential type wrapped in a mypin::Pin<Box<Foo>>; but if you were to construct a std::pin::Pin<Box<Foo>> you wouldn’t be able to call any method that relies on the self-referentialness because the type doesn’t use that set of guarantees.

2 Likes

To better understand the usefulness of Pin<Box<T>> over Box<T>, I find it useful to refer to the example in the pin module documentation.

I took the liberty to modify it a bit to add a new Unmovable::dangerous_new() method that returns a Box<T> instead of a Pin<Box<T>>: playground.

As you can see, the crux of the matter is that returning Pin<Box<T>> prevents the users of Unmovable::new() from (safely) using std::mem::swap with two pinned instances: they have to write unsafe if they want to do so.

let unmoved : Pin<Box<_>> = Unmovable::new("hello".to_string());
// [...]

// Since our type doesn't implement Unpin, this will fail to compile:
// let mut new_unmoved : Pin<Box<_>> = Unmovable::new("world".to_string());
// std::mem::swap(&mut *still_unmoved, &mut *new_unmoved);

On the other hand, with the Box<Unmovable> returned by Unmovable::dangerous_new(), users are free to use std::mem::swap between two instances without ever writing unsafe.

let mut unmoved_yet : Box<_> = Unmovable::dangerous_new("boom".to_string());
// [...]
let mut shorter_scope : Box<_>  = Unmovable::dangerous_new("kaboom".to_string());
// [...]
// the following operation copies both the data and its pointer, but it doesn't
// update the pointer: as a result, the pointer will now point to the data
// of the other struct
std::mem::swap(&mut *unmoved_yet, &mut shorter_scope);

However, this is an unsafe operation that can compromise memory safety. In particular in this example, shorter_scope is deallocated before unmoved_yet, but unmoved_yet.slice points to shorter_scope.data, resulting in a use-after-free if one attempts to dereference it.

As a result, Box<Unmovable> is not a safe abstraction, while Pin<Box<Unmovable>> is.

1 Like

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.