Interior Mutability Mechanics: UnsafeCell, Cell & Synchronization
Master UnsafeCell as the sole primitive for interior mutability, LLVM readonly stripping, Cell vs RefCell, and thread synchronization boundaries.
Under normal Rust rules, an immutable reference &T guarantees that the underlying memory cannot be modified. The compiler communicates this guarantee directly to LLVM, marking pointers with the readonly attribute to permit aggressive loop hoisting and register caching.
How then can types like RefCell<T>, Mutex<T>, and AtomicUsize mutate data through shared references? The answer lies in a single compiler primitive: UnsafeCell<T>.
The only approved primitive in the entire Rust language that signals to LLVM that memory behind &T may mutate.
1. Why Raw Pointer Casts are Not Enough#
Consider an engineer attempting to bypass the borrow checker by casting an immutable reference to a raw mutable pointer:
123456// FATAL UNDEFINED BEHAVIOR: fn illegal_mutate(val: &i32) { let raw = val as *const i32 as *mut i32; // WRITING THROUGH THIS POINTER IS IMMEDIATE UNDEFINED BEHAVIOR! // unsafe { *raw = 99; } }
Why is this fatal? Because LLVM compiled the calling code under the assumption that val is immutable. If LLVM cached *val in a hardware CPU register, it will never re-read from RAM, and your write may be ignored or cause optimizer miscompilations.
2. The UnsafeCell<T> Compiler Intrinsic#
UnsafeCell<T> is a compiler intrinsic defined as:
1234#[repr(transparent)] pub struct UnsafeCell<T: ?Sized> { value: T, }
When LLVM encounters &UnsafeCell<T>, it strips the readonly attribute from the generated LLVM IR, warning the optimizer that memory at this address may mutate at any time.
Every single interior mutability type in the Rust standard library (Cell, RefCell, Mutex, RwLock, AtomicBool) wraps an UnsafeCell<T> under the hood.
3. Cell<T>: Zero-Cost Copy Mutation#
Cell<T> provides interior mutability for Copy types without any runtime borrow tracking counter:
1234567891011121314151617use std::cell::Cell; struct WindowMetrics { refresh_count: Cell<u32>, } fn render(metrics: &WindowMetrics) { // We can mutate refresh_count even though metrics is borrowed as IMMUTABLE! let current = metrics.refresh_count.get(); metrics.refresh_count.set(current + 1); } fn main() { let metrics = WindowMetrics { refresh_count: Cell::new(0) }; render(&metrics); println!("Refreshes: {}", metrics.refresh_count.get()); // 1 }
Why Cell<T> is 100% Safe at Compile Time#
Cell<T> never hands out references (&T) to its internal value. You can only get a copy via .get() or overwrite it entirely via .set(). Because no references to the interior ever exist, you can never violate Rust's XOR aliasing rule!
4. Concurrency Safety: Why Cell is !Sync#
Cell<T> and RefCell<T> are single-threaded primitives:
- They implement
Send(can be moved between threads). - They do not implement
Sync(!Sync), meaning multiple threads cannot share them concurrently via&Cell<T>.
For thread-safe interior mutability, use primitives that synchronize memory via CPU hardware bus locks:
| Primitives | Target Data | Single-Threaded | Multi-Threaded (Sync) |
|---|---|---|---|
| Direct Copy | Small Copy scalars (u32, bool) | Cell<T> | AtomicU32, AtomicBool |
| Complex Objects | Non-Copy structures, vectors | RefCell<T> | Mutex<T>, RwLock<T> |
5. Concept Verification#
Test your understanding of UnsafeCell and interior mutability:
Why is casting &T to *mut T and writing to it Undefined Behavior, whereas mutating inside UnsafeCell<T> is completely valid?
Why does Cell<T> not need a runtime borrow counter like RefCell<T> to prevent aliasing bugs?