How to understand mut Pin

In the docs about Pin especially Drop guarantee section it is said that data pointed by Pin should not be invalidated, repurposed etc, including calling Option::take.

Though it is completely possible to do so from mut Pin<Option<T>> as well as do mem::swap for mutable Pin pointers:

    let mut opt: Option<Vec<u32>> = Some((1..=10).collect());
    let mut pin_opt = Pin::new(&mut opt);
    let _ = pin_opt.take();
    dbg!(pin_opt);

So mutable Pin pointers are used widely in async world, though as I got they do not provide Pin guarantees so that objects cannot move. Is the only point of mutable Pin is to use them as a kind of a factory for immutable Pins while when no immutable Pin issued object can move in memory?

More examples of when memory can move with mut Pin: https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=9b42eecd3094c383393ef8bda17729d8

Option<Vec<u32>> is Unpin, so none of the pinning constraints apply. The DerefMut implementation for Pin<T> you're using is only for T: Unpin.

If you change your example to use an !Unpin type like PhantomPinned, you can see it doesn't compile anymore: https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=30fba6091d518fcd7b15040668df3535

2 Likes

What helped me understand Unpin is to compare it to Send and Sync (which, for those who are unfamiliar, are the traits that determine if a type is thread-safe or not).

Every new type in Rust automatically implements Send and Sync, because they are defined as 'auto traits'. The only ways to get a type to not be Send or Sync are:

  • Have a field contained within the type that is not Send or Sync
  • Explicitly opt out of the trait using a negative impl (e.g. unsafe impl !Send for MyType)

This means that every type in Rust is considered thread-safe unless it (or the data contained within) has explicitly been flagged as not being thread-safe. This is why you cannot put an Rc in a struct and then use it across multiple threads - it has negative impls for these traits.

Unpin is exactly the same idea applied to a different scenario - by default, Rust considers all types to not require a stable memory address unless a type explicitly flags that it does. And much like how the compiler ignores the restrictions around moving stuff between threads for Send/Sync types, Pin ignores the restrictions around moving data when it points at an Unpin type.

2 Likes

I find that part of the documentation to be a bit confusing indeed. The example of using Option::take() has to do with someone implementing a Pin projection to the payload of the Some variant.

For instance:

pub struct Pinned<T> (T, ::core::marker::PhantomPinned);
// someone can have: for some T:
impl Drop for Pinned<T> { fn drop (&mut self) { ... }} // fix dangling pointers to it etc.

pub struct Foo<T> {
    x: Option<Pinned<T>>,
}
impl Foo<T> {
    pub fn pinned_x (self: Pin<&'_ mut Self>) -> Pin<&'_ mut Pinned<T>>
    {
        unsafe {
            self.map_unchecked_mut(|it| it.x.as_mut().unwrap())
        }
    }
    pub fn take_x (self: Pin<&'_ mut Self>) -> Option<Pinned<T>>
    {
        unsafe {
            self.get_unchecked_mut().x.take()
        }
    }
}

violates the Pin contract.

Indeed, someone can first grab Pin<&'_ mut Pinned<T>> and then use the address with raw pointers and no lifetimes, knowing that when Pinned<T> is dropped it will do some runtime changes (e.g., clearing back those pointers) to ensure there is no use-after-free.

But someone now can go and do mem::forget(thing.take_x()) (or simply keep holding the Pinned<T>) and now those pointers are dangling and have not been cleared.

This is because Option::take() is just another case of the "dreaded" mem::swap for Pin<impl DerefMut<Target = impl !Unpin>> types.

So if your Option offers a pinning projection, then the take() operation must be kept private and used with care (e.g., it is legal to use an Option and offer pinning projection if the only usage of the Option wrapper is for a delayed initialization of that field).

2 Likes

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