Based on my current understanding, UnsafeCell<T> is a primitive wrapper used to encapsulate a type, and the only way to access the inner value is by obtaining a raw pointer via the .get() method. Given that it already forces you to go through raw pointers, why does it specifically require the #[lang = "unsafe_cell"] attribute?
Scenario 1: Without this lang item, would the compiler still incorrectly optimize read behaviors (perhaps due to LLVM alias analysis)? For instance, in the snippet below:
use std::cell::UnsafeCell;
fn main() {
let x = UnsafeCell::new(1);
let y = x.get();
// ... do something to mutate `x` via another pointer ...
unsafe {
println!("y: {}", *y);
}
}
Could anyone provide a concrete example demonstrating what would break, or explain the exact compiler magic/optimizations that prove the absolute necessity of #[lang = "unsafe_cell"]?
without being a lang item, it is impossible to get a raw pointer with the correct (i.e. writable) provenance from a shared reference, thus any writes through such pointers would be UB.
in other words, you cannot soundly implement fn get(&UnsafeCell<T>) -> *mut T purely as a library function (even for the standard library): you have to "cheat" (by being a lang item).
if you look at the implementation of UnsafeCell::get():
note, this is the only correct way to implement it, namely, you must coerce &UnsafeCell<T> into a raw pointer first, and then cast the raw pointer types. there's no other way. particularly, the following is INCORRECT:
/// WARNING: this is WRONG
pub const fn get(&self) -> *mut T {
(&self.value) as *const T as *mut T
}
then there is nothing whatsoever you can put in the body of the MyUnsafeCell::get() function that makes it possible to use like UnsafeCell. Given this struct definition, there is nothing that you can write which bypasses the rule that if a &MyUnsafeCell<T> exists, then the T held within it is immutable (other than T itself containing an UnsafeCell somewhere, of course).
No, it is also incorrect to do that. You cannot look at the implementation of std and assume that you can write the same unsafe code. std is allowed to do things that it knows will work on exactly the version of the compiler it was shipped with; you are not. Or, as the UnsafeCell documentation says in this specific case:
Note that the only valid way to obtain a *mut T pointer to the contents of a sharedUnsafeCell<T> is through .get() or .raw_get().
This does not say that you can also do the same thing get() does. It says that you must actually call get(). (It is possible that in the future, this rule will be relaxed, such that the pointer cast is allowed; but it has not been yet.)
Less possible and more almost certainly. It works this way under both Stacked Borrows and Tree Borrows, and while neither are complete, the risk of them changing is already something you have to deal with when doing any nontrivial unsafe. UnsafeCell's semantics are no different
What unsafe cell does is change the language rules for things behind references. And that's something that needs internal language magic to make it happen.
Somehow everyone ignores that simple step while discussing munitiae details of implementations of get.
If you have a shared reference that points to something that's not an UnsafeCell then the existence of such reference is binding promise to keep said object unmodified for the whole time that said reference exist.
And… that's it. Period. End of story. Any type that gives you ability to change something in the object that exist behind shared reference have to have UnsafeCell somewhere inside[1]. Compiler knows that any other type type wouldn't change and program with such type would rely on that fundamental fact.
Given the fact that we do sometimes want to have shared mutability (think about how dull would URLO be if no one would ever be able to see messages written by others) we need UnsafeCell…
Without UnsafeCell it's impossible to write function getat all[2], thus it's pointless to discuss what would happen in your program after call to such function: it may simply never happen.
In correct and valid program we have this:
let x = NotUnsafeCell::new(1); // Type without UnsafeCell inside
let y: *mut _ = x.get(); // Take shared reference and return mutable pointer.
// This line couldn't be executed. Ever.
type of future genearted by async fn may or may not be exception: formally it doesn't include UnsafeCell, but it's opaque, unnameable, anyway, thus we couldn't even say that for sure. ↩︎
Technically it's possible to write implementation that always panics or loops endlessly… what it couldn't do, ever, is to return any value. ↩︎