Loading page…
Rust walkthroughs
Loading page…
UnsafeCell is the foundational primitive for interior mutability in Rust. It's the core building block upon which Cell, RefCell, Mutex, and other types are built. Unlike those safe wrappers, UnsafeCell provides raw access to interior mutability without any runtime checks or safety guarantees.
Key characteristics:
unsafe required — All access requires unsafe blocksThe only guarantee UnsafeCell provides: it prevents certain compiler optimizations that would be invalid with interior mutability (like assuming a &T never changes). All safety must be manually ensured.
When to use UnsafeCell:
Cell or RefCell)Warning: Incorrect use of UnsafeCell leads to undefined behavior. Always prefer safe alternatives when possible.
use std::cell::UnsafeCell;
fn main() {
let cell = UnsafeCell::new(5);
// Safe: we have exclusive access, no other references exist
unsafe {
// Get a raw pointer to the inner value
let ptr = cell.get();
// Read the value
let value = *ptr;
println!("Value: {}", value);
// Modify the value
*ptr = 10;
println!("Modified: {}", *ptr);
}
}use std::cell::UnsafeCell;
// A minimal implementation of Cell for Copy types
struct MyCell<T> {
value: UnsafeCell<T>,
}
// Cell is not Sync because interior mutability without synchronization
// is not thread-safe
impl<T> !Sync for MyCell<T> {}
impl<T: Copy> MyCell<T> {
fn new(value: T) -> Self {
Self {
value: UnsafeCell::new(value),
}
}
fn get(&self) -> T {
// Safe because T is Copy (no aliasing issues)
// and we never return a reference, just a copy
unsafe { *self.value.get() }
}
fn set(&self, value: T) {
// Safe because we have a valid UnsafeCell and T is Copy
unsafe {
*self.value.get() = value;
}
}
}
fn main() {
let cell = MyCell::new(5);
println!("Initial: {}", cell.get());
cell.set(10);
println!("After set: {}", cell.get());
// Works through shared reference!
let reference = &cell;
reference.set(20);
println!("After set via ref: {}", cell.get());
}use std::cell::UnsafeCell;
use std::cell::Cell;
struct MyRefCell<T> {
value: UnsafeCell<T>,
borrow_state: Cell<isize>, // 0 = none, >0 = immutable borrows, -1 = mutable
}
impl<T> MyRefCell<T> {
fn new(value: T) -> Self {
Self {
value: UnsafeCell::new(value),
borrow_state: Cell::new(0),
}
}
fn borrow(&self) -> Ref<T> {
let state = self.borrow_state.get();
if state < 0 {
panic!("Already mutably borrowed");
}
self.borrow_state.set(state + 1);
Ref {
value: unsafe { &*self.value.get() },
state: &self.borrow_state,
}
}
fn borrow_mut(&self) -> RefMut<T> {
let state = self.borrow_state.get();
if state != 0 {
panic!("Already borrowed");
}
self.borrow_state.set(-1);
RefMut {
value: unsafe { &mut *self.value.get() },
state: &self.borrow_state,
}
}
}
struct Ref<'a, T> {
value: &'a T,
state: &'a Cell<isize>,
}
impl<T> std::ops::Deref for Ref<'_, T> {
type Target = T;
fn deref(&self) -> &T {
self.value
}
}
impl<T> Drop for Ref<'_, T> {
fn drop(&mut self) {
self.state.set(self.state.get() - 1);
}
}
struct RefMut<'a, T> {
value: &'a mut T,
state: &'a Cell<isize>,
}
impl<T> std::ops::Deref for RefMut<'_, T> {
type Target = T;
fn deref(&self) -> &T {
self.value
}
}
impl<T> std::ops::DerefMut for RefMut<'_, T> {
fn deref_mut(&mut self) -> &mut T {
self.value
}
}
impl<T> Drop for RefMut<'_, T> {
fn drop(&mut self) {
self.state.set(0);
}
}
fn main() {
let cell = MyRefCell::new(vec![1, 2, 3]);
{
let mut borrowed = cell.borrow_mut();
borrowed.push(4);
}
{
let borrowed = cell.borrow();
println!("Value: {:?}", *borrowed);
}
}use std::cell::UnsafeCell;
use std::ffi::c_int;
// A C library might expect to modify data through a pointer
// even though we only have a shared reference
extern "C" {
fn c_modify_data(data: *mut c_int, len: usize);
}
struct CBuffer {
// UnsafeCell tells the compiler this might change through a shared reference
data: UnsafeCell<Vec<c_int>>,
}
impl CBuffer {
fn new(data: Vec<c_int>) -> Self {
Self {
data: UnsafeCell::new(data),
}
}
fn modify_via_c(&self) {
unsafe {
let data = &mut *self.data.get();
c_modify_data(data.as_mut_ptr(), data.len());
}
}
fn get_data(&self) -> &[c_int] {
unsafe { &*self.data.get() }
}
}
// Mock C function for demonstration
#[no_mangle]
unsafe extern "C" fn c_modify_data(data: *mut c_int, len: usize) {
for i in 0..len {
*data.add(i) *= 2;
}
}
fn main() {
let buffer = CBuffer::new(vec![1, 2, 3, 4, 5]);
println!("Before: {:?}", buffer.get_data());
// Called with &self, but C code modifies the data
buffer.modify_via_c();
println!("After: {:?}", buffer.get_data());
}use std::cell::UnsafeCell;
struct OnceFlag {
triggered: UnsafeCell<bool>,
}
impl OnceFlag {
const fn new() -> Self {
Self {
triggered: UnsafeCell::new(false),
}
}
fn trigger(&self) {
unsafe {
*self.triggered.get() = true;
}
}
fn is_triggered(&self) -> bool {
unsafe { *self.triggered.get() }
}
}
fn main() {
let flag = OnceFlag::new();
println!("Triggered: {}", flag.is_triggered());
flag.trigger();
println!("Triggered: {}", flag.is_triggered());
}use std::cell::UnsafeCell;
struct SplitSlice<'a, T> {
data: UnsafeCell<&'a mut [T]>,
}
impl<'a, T> SplitSlice<'a, T> {
fn new(data: &'a mut [T]) -> Self {
Self {
data: UnsafeCell::new(data),
}
}
// Get mutable access to a portion of the slice
// Safety: each index can only be accessed once
fn get_mut_portion(&self, start: usize, end: usize) -> Option<&mut [T]> {
unsafe {
let data = &mut *self.data.get();
if start <= end && end <= data.len() {
Some(&mut data[start..end])
} else {
None
}
}
}
}
fn main() {
let mut data = vec![1, 2, 3, 4, 5, 6];
let split = SplitSlice::new(&mut data);
// This would be UB if called twice with overlapping ranges!
// Only use this pattern when you can guarantee non-overlapping access
if let Some(portion) = split.get_mut_portion(0, 3) {
for item in portion.iter_mut() {
*item *= 2;
}
}
println!("Data: {:?}", data);
}use std::cell::UnsafeCell;
use std::sync::atomic::{AtomicBool, Ordering};
struct SimpleMutex<T> {
locked: AtomicBool,
value: UnsafeCell<T>,
}
// Safe because we use atomic operations for synchronization
unsafe impl<T: Send> Sync for SimpleMutex<T> {}
unsafe impl<T: Send> Send for SimpleMutex<T> {}
impl<T> SimpleMutex<T> {
fn new(value: T) -> Self {
Self {
locked: AtomicBool::new(false),
value: UnsafeCell::new(value),
}
}
fn lock(&self) -> MutexGuard<T> {
// Spin until we acquire the lock
while self.locked.compare_exchange(
false,
true,
Ordering::Acquire,
Ordering::Relaxed,
).is_err() {
// In real code, use a proper wait mechanism
std::hint::spin_loop();
}
MutexGuard {
mutex: self,
}
}
}
struct MutexGuard<'a, T> {
mutex: &'a SimpleMutex<T>,
}
impl<T> std::ops::Deref for MutexGuard<'_, T> {
type Target = T;
fn deref(&self) -> &T {
unsafe { &*self.mutex.value.get() }
}
}
impl<T> std::ops::DerefMut for MutexGuard<'_, T> {
fn deref_mut(&mut self) -> &mut T {
unsafe { &mut *self.mutex.value.get() }
}
}
impl<T> Drop for MutexGuard<'_, T> {
fn drop(&mut self) {
self.mutex.locked.store(false, Ordering::Release);
}
}
fn main() {
let mutex = SimpleMutex::new(vec![1, 2, 3]);
{
let mut data = mutex.lock();
data.push(4);
}
println!("Data: {:?}", *mutex.lock());
}use std::cell::UnsafeCell;
// UnsafeCell is the only way to have interior mutability in const context
const COUNTER: UnsafeCell<u32> = UnsafeCell::new(0);
// Note: This is extremely dangerous in practice!
// Modifying static data without synchronization is undefined behavior
// in multi-threaded contexts.
fn increment_counter() -> u32 {
unsafe {
let ptr = COUNTER.get();
*ptr += 1;
*ptr
}
}
fn main() {
println!("Counter: {}", increment_counter());
println!("Counter: {}", increment_counter());
println!("Counter: {}", increment_counter());
}use std::cell::UnsafeCell;
fn main() {
// WITHOUT UnsafeCell: Compiler assumes &x never changes
let mut x = 5;
let ref_x = &x;
// x = 10; // This would fail to compile
println!("ref_x: {}", ref_x);
// WITH UnsafeCell: Compiler knows &x might change
let cell = UnsafeCell::new(5);
unsafe {
let ptr1 = cell.get();
let ptr2 = cell.get(); // Multiple pointers to same location!
// The compiler won't assume *ptr1 stays the same
*ptr1 = 10;
// This is valid because UnsafeCell disables the no-alias optimization
println!("ptr2: {}", *ptr2); // Will print 10
}
}use std::cell::UnsafeCell;
use std::sync::atomic::{AtomicU8, Ordering};
struct LazyCell<T, F = fn() -> T> {
state: AtomicU8, // 0 = not initialized, 1 = initializing, 2 = initialized
value: UnsafeCell<Option<T>>,
init: F,
}
// Safety: we use atomic operations for synchronization
unsafe impl<T: Send + Sync, F: Send + Sync> Sync for LazyCell<T, F> {}
unsafe impl<T: Send, F: Send> Send for LazyCell<T, F> {}
impl<T, F: FnOnce() -> T> LazyCell<T, F> {
const fn new(f: F) -> Self {
Self {
state: AtomicU8::new(0),
value: UnsafeCell::new(None),
init: f,
}
}
fn get(&self) -> &T {
match self.state.load(Ordering::Acquire) {
2 => unsafe {
// Already initialized
(*self.value.get()).as_ref().unwrap()
},
_ => self.initialize(),
}
}
#[cold]
fn initialize(&self) -> &T {
// Simple spin lock for demonstration
while self.state.compare_exchange(
0,
1,
Ordering::AcqRel,
Ordering::Acquire,
).is_err() {
if self.state.load(Ordering::Acquire) == 2 {
return unsafe {
(*self.value.get()).as_ref().unwrap()
};
}
std::hint::spin_loop();
}
let value = (self.init)();
unsafe {
*self.value.get() = Some(value);
}
self.state.store(2, Ordering::Release);
unsafe { (*self.value.get()).as_ref().unwrap() }
}
}
fn main() {
let lazy = LazyCell::new(|| {
println!("Computing value...");
42
});
println!("First access");
println!("Value: {}", lazy.get());
println!("Second access");
println!("Value: {}", lazy.get());
}use std::cell::UnsafeCell;
fn main() {
let cell = UnsafeCell::new(5);
// PITFALL 1: Creating aliasing mutable references
// This is UNDEFINED BEHAVIOR!
// unsafe {
// let ref1 = &mut *cell.get();
// let ref2 = &mut *cell.get(); // UB! Two mutable refs to same data
// *ref1 = 10;
// *ref2 = 20;
// }
// PITFALL 2: Returning reference with wrong lifetime
// This would compile but is UB if called multiple times:
// fn bad_borrow(cell: &UnsafeCell<i32>) -> &mut i32 {
// unsafe { &mut *cell.get() }
// }
// let r1 = bad_borrow(&cell);
// let r2 = bad_borrow(&cell); // UB!
// CORRECT: Ensure exclusive access
unsafe {
let ptr = cell.get();
*ptr = 10; // Only one access at a time
}
// PITFALL 3: Thread safety
// UnsafeCell is not Sync by default for a reason!
// Use atomics or Mutex for thread-safe interior mutability
println!("Safe usage: {}", unsafe { *cell.get() });
}UnsafeCell Methods:
| Method | Description |
|--------|-------------|
| new(value) | Create new UnsafeCell |
| get() | Get *mut T pointer (unsafe to use) |
| into_inner() | Consume and return inner value (safe) |
| raw_get() | Get *const T pointer (unsafe to use) |
When to Use UnsafeCell:
| Use Case | Appropriate? | |----------|-------------| | Building safe abstractions | ✅ Yes | | FFI with C code | ✅ Yes | | Performance optimization | ⚠️ Rarely needed | | Simple interior mutability | ❌ Use Cell/RefCell | | Thread-safe sharing | ❌ Use Mutex/RwLock |
Safety Invariants to Maintain:
&mut T to the same dataKey Points:
UnsafeCell is the foundation for all interior mutability typesunsafe blocks&T behind UnsafeCell is immutableCell, RefCell, Mutex) whenever possibleUnsafeCell is not Sync by default (and shouldn't be made Sync without synchronization)