Why isn't `RefCell` unconditionally `Unpin`?

RefCell<T> is Unpin only when T is Unpin:

impl<T> Unpin for RefCell<T>
where
    T: Unpin + ?Sized,

(link)

Given that

  • RefCell<T> doesn't currently provide structural pinning of T, and
  • It can't provide structural pinning of T in the future because it has interior mutability—a Pin<&RefCell<T>> allows you to obtain &RefCell<T>, which allows you to borrow &mut T and swap it. So there can't exist a way to get from Pin<&RefCell<T>> to Pin<&T>.

Why doesn't RefCell implement Unpin unconditionally?

1 Like

Unpin would allow getting &mut RefCell<T> from the pinned version and it allows for get_mut.

2 Likes

It's true, but even without Unpin I can get a mutable reference:

#[derive(Debug)]
struct Bla(u32, PhantomPinned);

fn main() {
    let x: Pin<&mut RefCell<Bla>> = pin!(RefCell::new(Bla(1, PhantomPinned)));
    {
        // Swap inner value
        let rc: &RefCell<Bla> = x.as_ref().get_ref();
        let mut y = rc.borrow_mut();
        let z: &mut Bla = &mut *y;
        let _ = mem::replace(z, Bla(2, PhantomPinned));
    }
    // Pin is still alive but new value was moved into it. So if I had Pin<&Bla>
    // it would have violated pin guarantees.
    dbg!(x);
}

(Playground)

2 Likes

in my opinion, it's simply because no one really cares to change that.

note that this code is located in the in the Auto Trait Implementations section.
no one wrote code saying RefCell<T> should be Unpin only if T is.
if you want to change that, you probably could make an ACP.

the same applies for Cell Mutex, and RwLock of curse.

3 Likes

This is actually a bit confusing, because I wrote some replies to this post, which made it look like RefCell was unconditionally Unpin (I thought this to be true, because that was the simplest explanation). But after seeing this post and looking more deeply into the struct defs, I realized that it was related Pin having a Clone derive.

The above rambling is relevant, because it lead me to this:

// Note: the `Clone` derive below causes unsoundness as it's possible to implement
// `Clone` for mutable references.
// See <https://internals.rust-lang.org/t/unsoundness-in-pin/11311> for more details.
#[stable(feature = "pin", since = "1.33.0")]
#[lang = "pin"]
#[fundamental]
#[repr(transparent)]
#[rustc_pub_transparent]
#[derive(Copy, Clone)]
pub struct Pin<Ptr> {
    pointer: Ptr,
}

The key here is this thread: Unsoundness in `Pin` - language design - Rust Internals

In my understanding, the issue is how Unpin propagates. It's an auto trait, a struct is Unpin if all its fields are.

If RefCell<T>: Unpin unconditionally:

struct Wrapper<T> { 
    inner: RefCell<T> 
}
// Wrapper<T>: Unpin always (auto-derived from fields). Hypothetical:

let mut pinned: Pin<Box<Wrapper<NotUnpin>>> = Box::pin(...);
let wrapper: &mut Wrapper<NotUnpin> = pinned.as_mut().get_mut();  // allowed!
let inner: &mut NotUnpin = wrapper.inner.get_mut();
std::mem::swap(inner, &mut other);  // moved !Unpin out of Pin

RefCell gives &mut T, which lets you move T. That's why RefCell<T>: Unpin only when T: Unpin.

Compare with Rc<T> which is unconditionally Unpin, it oly gives &T, so you can't move the inner value.

This is actually part of a broader issue with Pin. Its guarantees are enforced by API contract, not compiler magic. There are multiple paths to circumvent it:

Path Mechanism Example
Unpin auto-trait Container becomes Unpin -> get_mut() -> &mut T what @Ddystopia and @Morgane55440 suggested, in the scenario where RefCell was unconditionally Unpin
Interior mutability &RefCell<T> -> borrow_mut() -> &mut T what you suggested in the second post
Custom DerefMut impl DerefMut for &'a Foo exploits Pin::as_mut()
Custom Clone impl Clone for &'a mut Foo exploits Pin::clone()

The last two are described in the original post of the internals thread.

both issues mentioned in the internal threads have been fixed, and are completely unrelated to under which condition RefCell should be Unpin.

this is not really a good argument for RefCell<T> not being unconditionally Unpin, as this is about a custom wrapper type which can make its own rules however it wants, through PhantomPinned and manual UnPin impls.
rn, it is a fact that can get a &mut T out of a Pin<Ptr<RefCell<T>>>, which is a good argument for just making RefCell unconditionally Unpin, as it is not structural.

in order to make the Wrapper be structural over T, if that is what you want to do, you would simply need to add a PhantomPinned field, which is very common and has the advantage of being very explicit.

but that conversation does bring up an important point that has been ignred so far ; backwards compatibility.
indeed, it is a major breaking change to make RefCell unconditionally Unpin, as existing code may rely on the current bounds. And indeed for example, the pincell crate does rely on this behavior.
simply because of that, no change can be done.

ultimately, any level of Unpin for RefCell<T> would have been a sound choice when Unpin was created, but now it is to late, as any change could brek existing code.

so i have to take back my advice on making an ACP, sadly

1 Like

My point was that this whole Pin/Unpin thing is pretty delicate and can lead to some not so obvious ways of unsound things if not handled properly, and there are a lot of things at play here that we may not be fully aware about. Among the four examples, only the 2nd one should compile. But that is related more to RefCell and interior mutability issues.

it certainly is true that Pinning is complex, but it is not very hard to et to a point whare you understand everything you need to. i would advise you look at the documentatin of the pin module, as i found it very helpful in the past.

it is a pretty basic fact that any Unpin behavior for RefCell<T> would have been correct, as it exposes no projection.
and it is also a pretty simple fact that RefCell<T> cannot expose any projection to its inner T, because &RefCell<T> -> &mut T is possible.

this is why the Unpin behavior of RefCell<T> is really an ergonomics issue, and making it unconditionally Unpin would certainly make for simpler ergonomics, at least imo.

again, the last 2 "examples" are old bugs that have been fixed for a while, and there is nothing wrong with the 1st compiling as well.

4 Likes

I see, thanks. IIUC I can generalize what you said to: It always a breaking change to add blanket Unpin to any type because maybe some user relied that type being !Unpin in order to make itself !Unpin.

2 Likes

exactly !

I agree that, if #2 already gives you &mut T, then #1 doing the same via a more ergonomic path isn't adding new unsoundness. The projection argument makes sense.

I was just being cautious that Pin's design has had non-obvious issues discovered after stabilization of Pin, but you're right that RefCell's Unpin bound may be more about backwards compatibility than soundness.

EDIT: I already assumed it to be Unpin in the last post we discussed it in :'D so there's that