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 the following criteria:
- If it implements
Unpin
, it does so using the auto-trait mechanism, not a manually written impl. - If it implements
Drop
, either it implementsUnpin
or its destructor uses thefn drop(&pin mut self)
signature.
As long as a type meet those requirements, there’s no way in safe code to violate the pinning
guarantees. (An earlier version of this post stated these criteria differently and incorrectly,
because I forget that Unpin
must be implemented using the auto-trait mechansim for this to be
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.