Disclaimer: The rules for these kinds of things are not (yet) set in stone. So, there is no definitive answer yet. I'm going to make some guesses based on (a) what kinds of compiler transformations LLVM does/we will eventually want to do, and (b) what kind of models I have in my head that would define the answer to this.
Also, I see two parts to this: The data layout perspective, and the aliasing perspective. The layout issue is that NotReallyImmutable
could, in principle, have a totally different layout than ReallyImmutable
. I don't know much about data layout, but with UnsafeCell
becoming repr(transparent)
and that being the only difference between the two types, I think the intent is for this to work. You are, however, relying on repr(transparent)
being "structural" in the sense that it should allow you to replace things in larger types, which I am not sure has been written down explicitly anywhere. Sounds like a proposal for a follow-up RFC that extends the repr(transparent)
guarantees appropriately?
As far as aliasing is concerned, the issue is breaking the rules around &T
. I'd say that, as long as you never have a live &T
around anywhere when writing through the &UnsafeCell<T>
, you are good -- but I don't think we can guarantee that quite yet. Let's look in more detail.
Compiler perspective
The relevant optimizations here are the ones that exploit &T
being read-only. So if you reordered the last two lines (transmute
and the assignment), that code would likely be UB as we may want the compiler to be able to "pre-fetch" the value behind the shared reference and re-use that value later (i.e. after inlining this).
But in your code, we would only emit "read-only" annotations (noalias
in LLVM) after the transmute
comes back, and the data is indeed read-only starting there. So, this should be good.
Memory models
The "most aggressive" of my memory models essentially asserts that all values are always valid, and I think even that model should be fine with your code. &UnsafeCell
is a special case in that model where validity just stops, and nothing is said about what lives behind this reference. The moment the transmute
returns, we grab the memory it points to and make it all read-only, and even if we did that "recursively" through the Rc
(which my model doesn't, but only because I couldn't figure out a good way to make it do so) you'd be fine as you don't mutate any more after the transmute
. (As you may have noticed, this is the same restriction as in the compiler perspective. The point of these models is to allow compiler optimizations, after all. ;)
(As a side-note, I really wish miri was in better shape right now. Seems I have to try and get validation to work again in there, because then I could tell you to just run your code in miri and it'd tell you if that version of my model is okay with what you are doing :D )
I am thinking about other models currently that only check things "on access", but haven't worked out the UnsafeCell
story for that model yet. What this example shows is that the model may have to contain ways for a "phase transition" of memory first being UnsafeCell
, but later having normal sharing with read-only guarantees. Thanks for bringing this up, that will make for some nice examples to think about!
So, I think I can say that (at least from my side) there is the intent to allow this kind of code, and doing so does not seem to prevent any optimizations. Whether we'll actually manage to find a model that everybody can agree with and that still allows this, I cannot predict.
The opposite direction: T -> UnsafeCell<T>
Now, this is more interesting. The problem is that, as I said above, you must not have a &T
live when writing through an UnsafeCell<T>
. But what does "live" mean here? That's a hard question! In some of my models, this could be as weak as "a reference of that type exists somewhere and the lifetime is still active", i.e., it could have nothing to do with whether the reference is actually used. (That's useful because it lets us do more optimizations, like moving a load out of a loop even if we cannot prove that the loop ever runs -- which would introduce a use of an otherwise unused reference.) And since &T
is Copy
, you cannot even really get rid of such a reference either. So, if you have x: &T
, then after let y: &UnsafeCell<T> = transmute(x)
, the old x
is still around and its lifetime still active, so writing through y
could well be UB.
I think you'd have to somehow restrict the aliasing that &T
allows, very carefully making sure that nobody still holds such a reference. I'm not going to say "this is impossible" because people keep surprising me (especially in this community ;) but TBH I cannot think of a way to make this work. I'd be curious if you have an example though where you think this is reasonable.
UnsafeCell
, it's going to assume no aliasing can happen. – EleryUnsafeCell
, so the knot tying part is perfectly fine. – EveryplaceReallyImmutable
andNotReallyImmutable
were#[repr(C)]
neitherUnsafeCell
norRc
have a defined reliable layout. – EveryplaceUnsafeCell
takes up zero (extra) space. I don't think it is feasible that a future version of Rust would change that. – OntarioT
toUnsafeCell<T>
... even in the absence of aliasing I could see the compiler making assumptions there. – Manipur#[repr(transparent)]
might specify this. PR #1758 and tracking issue – ScotneyDrop
, so you should never usetransmute
! In this particular case, the Rust core developers are constrained - changing howUnsafeCell
affects its data's layout would be a breaking change, and so would implementingDrop
forUnsafeCell
. If these two risks are the only concerns then this feels very safe. – OntarioUnsafeCell
data layout would be a breaking change; they made no promises about the layout afaict. Even if the newtype layout would be defined, they'd still need to promise not to add more fields. But I'd worry more about rust eliminating fields from a type that aren't used or optimizing the field order for different access patterns. – EveryplaceTieWeak
on playground, a weak pointer wrapper to tie the knot. – Everyplace