We like to believe UnsafeCell<T>
is opaque to its contents and returns an arbitrary *mut T
to its contents. This is probably not aligned with the existing Stacked Borrows model, but it's just a model and those can be changed. Additionally, for convenience, UnsafeCell<T>
contains a helper fn get_mut(&mut self) -> &mut T
which ties the output reference's lifetime to the input reference's lifetime, while preventing simultaneous access to the UnsafeCell<T>
as that would be unsound. (Our Cursed Cells won't have that helper, as it would be unsound.) Indeed, this all aligns well with how Miri doesn't complain about it: Rust Playground
So we have our CursedCell:
#[repr(transparent)]
pub struct CursedCell<T: ?Sized> {
inner: Cell<T>,
}
impl<T> CursedCell<T> {
pub fn new(t: T) -> Self {
Self { inner: Cell::new(t) }
}
}
impl<T: ?Sized> CursedCell<T> {
pub fn as_ptr(&self) -> *mut T {
self.inner.as_ptr()
}
}
Now, the space held by the cell is not actually forever. We'd love to represent that in the type system, but we don't know how to. Tho we can do this: Rust Playground And there is one place where Rust infers lifetimes but you can otherwise override it: dyn Trait
. Specifically:
type Foo<'a> = &'a dyn Trait;
// same as:
type Foo<'a> = &'a (dyn Trait + 'a);
// but you can also do:
type Foo<'a> = &'a (dyn Trait + 'static);
// while at it:
type Foo = Box<dyn Trait>; // implies 'static
Unfortunately we can't make use of this. But we might not need it anyway.
The struct above is immovable after throwing &foo
into it, as it is borrowed. (Tho it cannot be moved again even after setting x
to None
!) But it can be moved before then. Let's get rid of the Cell
first: Rust Playground
struct Foo<'a> {
x: Option<&'a Foo<'a>>,
}
fn main() {
let mut foo = Foo {
x: None,
};
let x = &mut foo.x;
*x = Some(&foo);
}
Alright, this clearly doesn't work. But all is not lost. Now, we can't safely do cell projections, but they're still sound. So let's use a Cell
, and change that x
to have a &Cell<Foo>
instead:
use std::cell::Cell;
struct Foo<'a> {
x: Option<&'a Cell<Foo<'a>>>,
}
fn main() {
let foo = Cell::new(Foo {
x: None,
});
let x: &Cell<Option<&Cell<Foo<'_>>>> = todo!();
x.set(Some(&foo));
}
We can also Box::pin
it. Tho on that note std makes it really hard to work with pinned cells, so let's use our CursedCell
instead, and add things to make working with Pin
easier. Maybe even fork pin-project
so it works with the CursedCell
. Now, it's important to note that with pinned types, standard Cell
is unsound (e.g. the example above is unsound), because you could set
the whole Foo
, while Cell
moves it: Rust Playground While at it, standard Drop
is also unsound: Rust Playground (run it in miri) Ideally we'd deal with that in CursedCell
somehow, maybe by emitting our own drop glue (and having a CursedDrop
which gets Pin<&'a CursedCell<Self<'a>>>
) in the #[cursed_pin]
macro, and using a ManuallyDrop
(which is, like Cell
, #[repr(transparent)]
, so shouldn't affect projections) inside the CursedCell
. This would also have to be guarded against by any safe, non-pinned "cell projection" crates.
Anyway sorry, we burned out at this point in writing, so we're not gonna finish this post. We just wanted an excuse to talk about stacked borrows and Pin
and Cell
. Stacked borrows as currently implemented in miri does seem to make Cell
's contents effectively live for as long as the Cell
isn't moved, regardless of how the borrows leading up to the *mut T
are tracked, so that's nice. It'd be nice if there were a crate that made use of it.