UnpinCell

A variation on my previous design for pinned places has occurred to me that would be more consistent with Rust’s existing feature set.

The most outlandish aspect of the previous design was the notion of “pinned fields,” which support pinned projection. This is quite different from how field projection normally works in Rust: if you have a mutable reference to a struct, you can get a mutable reference to its field, period. (I know Niko Matsakis has recently explored ideas that would change this; this post won’t go into any deep consideration of that proposal). I’ve come up with a design which would have similar properties, instead of introducing a kind of field marker.

First, pinned references should support projection just like other references. The only reason they don’t is the unsoundness around Drop, which I previously discussed in this post. The way around this is to say that pinned references support projections so long as the type being projected through meets one of the following criteria, each of which on its own is enough to solve the Drop issue:

  1. It implements Unpin (and therefore does not have any pinning contract).
  2. It does not implement Drop (and therefore cannot have the drop unsoundness).
  3. Its destructor uses the fn drop(&pin mut self) signature, as discussed in my previous post.

Nearly every type should satisfy 1) or 2), for the rare case where a type that does not implement Unpin needs a destructor (such as some tokio synchronization primitives), if it uses a pinned destructor then pin projection is sound.

However, you still need some way of support unpinned fields, which are exceptions to the pinning contract applied to the object as a whole. To do this, the language would introduce a new “cell” type, UnpinCell, which “unpins” whatever object is in it. The API for UnpinCell could look like this:

pub struct UnpinCell<T>(T);

impl<T> UnpinCell<T> {
    pub fn new(value: T) -> UnpinCell<T> {
        UnpinCell(value)
    }

    pub fn into_inner(self) -> T {
        self.0
    }
}

// Even if T: !Unpin
impl<T> Unpin for UnpinCell<T> { }

impl<T> Deref for UnpinCell<T> {
    type Target = T;

    fn deref(&self) -> &T {
        &self.0
    }
}

impl<T> DerefMut for UnpinCell<T> {
    fn deref_mut(&mut self) -> &mut T {
        &mut self.0
    }
}

This incredibly simple API allows mutable access to the value inside of the UnpinCell, even through a pinned pointer, even if the value does not implement Unpin. It is sound because UnpinCell creates a barrier through which it is not possible to pin project, so the object inside the cell is never witnessed pinned.

Revisiting the MaybeDone example from the previous post, it would now look like this:

enum MaybeDone<F: Future> {
    Polling(F),
    Done(UnpinCell<Option<F::Output>>),
}

impl<F: Future> MaybeDone<F> {
    fn maybe_poll(&pin mut self, cx: &mut Context<'_>) {
        if let MaybeDone::Polling(fut) = self {
            if let Poll::Ready(res) = fut.poll(cx) {
                *self = MaybeDone::Done(UnpinCell::new(Some(res)));
            }
        }
    }

    fn is_done(&self) -> bool {
        matches!(self, &MaybeDone::Done(_))
    }

    fn take_output(&pin mut self) -> Option<F::Output> {
        // res: &pin mut UnpinCell<Option<F::Output>>
        if let MaybeDone::Done(res) = self {
            // two deref mut coercions resolve this to Option::take
            res.take()
        } else {
            None
        }
    }
}

(I’ve updated the syntax to use pin instead of pinned, because that’s the syntax the project’s pin ergonomics experiment is using.)

This combination makes pinned places an even more minor change to the language: pinned references act like normal references and to get a field which is an exception, there is a cell-like API for “interior unpinnability”, just like “interior mutability.” In my opinion, this is superior to field modifiers because it doesn’t introduce any new category of language feature.