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.

04. Unsafe Rust, Layout Control & Systems Internals7 min read

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 Atomic CoreCompiler Intrinsic
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:

1
2
3
4
5
6
// 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:

1
2
3
4
#[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.

Architecture Diagram
Rendering diagram...

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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
use 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:

PrimitivesTarget DataSingle-ThreadedMulti-Threaded (Sync)
Direct CopySmall Copy scalars (u32, bool)Cell<T>AtomicU32, AtomicBool
Complex ObjectsNon-Copy structures, vectorsRefCell<T>Mutex<T>, RwLock<T>

5. Concept Verification#

Test your understanding of UnsafeCell and interior mutability:

Concept check

Why is casting &T to *mut T and writing to it Undefined Behavior, whereas mutating inside UnsafeCell<T> is completely valid?

Concept check

Why does Cell<T> not need a runtime borrow counter like RefCell<T> to prevent aliasing bugs?