On replacing UnsafeCell

It looks to me like UnsafeCell is not really a type, but rather property of a struct field.

See: moving it, including passing as a function argument or return value, can replace UnsafeCell with a simpler type:

  • UnsafeCell<T> == T
  • &UnsafeCell<T> == NonNull<T>
  • &mut UnsafeCell<T> == &mut T

And it is about "interior mutability" where said "interior" is often relative to some struct.

Therefore a new language feature could be worked out. Its bikesheddable name is "lit/twilight field dichotomy" (#[feature(twilight_fields)]).

A description of new feature

All ordinary fields of a struct are called lit; structs are now also allowed to have twilight fields which are internally mutable and potentially uninitialized (could be controlled inside an attribute).

struct Foo {
    #[twilight] special: String,
    common: u32,
}

Access to a twilight field (foo.special) produces a nonnull pointer to the contents (NonNull<String>), regardless of whether foo was owned, or shared reference, or exclusive reference. Again regardless of the origin, this pointer has at least read-write provenance for the String.

The twilight fields are stored together with lit ones, and, importantly, moved along the struct (but considered potentially uninitialized). They are accounted in its size and alignment.

&Foo and &mut Foo do not alias twilight fields (presumably they have no provenance to read or write them, but still allowed to do pointer arithmetic since it's inbounds of a Foo allocation).

Required compiler support

Half of this feature can be implemented with unsafe code; unergonomic it (Rust Playground) may be, but it passes Miri except for the leaked memory (destructors for twilight fields should be worked out). However such a struct has to be constructed in-place; compiler support would allow having it first-class and moving it as needed.

What it would solve

The complications of UnsafeCell, UnsafePinned, !Unpin, ManuallyDrop<Box<_>>, and future's ref aliasing[1]; all kinds of accesses could finally be managed in code.


  1. At a certain cost, though; that &mut Foo does no longer provide write access to the whole of its span. â†Šī¸Ž

How would this handle safe cells, like Cell and Mutex?

If you write a structure with a field of type Cell, that puts an UnsafeCell inside the structure, but the wrapper Cell around it makes certain guarantees (that it's always initialised, only accessed from the current thread, and only accessed via swaps and copies). The wrapper type seems useful here, because a *mut T or *mut Cell<T> wouldn't be usable from safe code (whereas a Cell can be).

The only way to make it work would be to say "form a reference to the wrapper struct, then have the unsafe cell operations happen inside that struct". But then you could implement UnsafeCell the same way (as a wrapper struct that has one "twilight field"), and avoid the complication of needing a new language feature – it's almost always better to use a standard library type than a language feature in places where the syntax doesn't become too unergonomic.

It's worth noting that there's no significant difference in practice between &UnsafeCell<T> and *mut T: &UnsafeCell has a lifetime, but I think raw pointers should also have a lifetime (that reflects how long their provenance is valid, rather than saying anything about the reference target), because dereferencing a pointer without provenance is always undefined behaviour and the lifetime helps to prevent you doing that by mistake.

The reason that UnsafeCell exists separately from a pointer is that if it isn't behind a shared reference, e.g. if you have &mut UnsafeCell<T>, it can be dereferenced in safe code. UnsafePinned exists to disable that ability, and is a lot less commonly used than cells are (and thus it doesn't really make sense to combine the features).

In terms of the rest of your motivation, I think ManuallyDrop is unrelated (ManuallyDrop<Box<T>> causes problems because you can't move a dropped Box, but in that scenario, you are accessing by value not by reference), and although there may be a connection to !Unpin, it's somewhat complicated and it's unclear how this proposal would simplify things. (Merely containing a cell doesn't make something !Unpin, after all: types are !Unpin if they're address-sensitive or contain self-references, and although I think it can make sense to view a self-reference as a type of cell, most cells aren't used for that purpose and don't prevent types being Unpin. For example, both Cell and UnsafeCell implement Unpin if their contents do.)

1 Like

I'm generally opposed to any feature which makes fields not uphold their usual invariants. If the field is i32 and you can get an &i32, that should be a legal &i32, but that doesn't hold here.

The wrapper type is really useful because getting a &UnsafeCell<i32> is perfectly fine -- it can be passed around, it can have Debug defined on the unsafecell so that derive(Debug) works, etc.

Attributes that add extra guarantees is fine -- a &i32 to a field with #[align(128)] is still usable, for example -- but removing type things is best done with a wrapper, not an attribute.