unsafe cell
Tutorial
The Problem
This example covers a specific aspect of Rust's unsafe programming model: raw memory manipulation, FFI interop, allocator customization, or soundness principles. These topics are essential for systems programming — writing OS components, device drivers, game engines, and any code that must interact with C libraries or control memory layout precisely. Rust's unsafe system is designed to confine unsafety to small, auditable regions while maintaining safety in the surrounding code.
🎯 Learning Outcomes
Code Example
use std::cell::Cell;
fn main() {
let cell = Cell::new(0_i32);
cell.update(|v| v + 5);
cell.update(|v| v + 3);
println!("Cell value: {}", cell.get());
cell.set(100);
println!("After set: {}", cell.get());
}Key Differences
unsafe for these operations; OCaml achieves safety through the GC and type system without explicit unsafe regions.extern "C"; OCaml uses ctypes which wraps C types in OCaml values.#[repr(C)], custom allocators); OCaml's GC manages memory layout automatically.OCaml Approach
OCaml's GC and type system eliminate most of the need for these unsafe operations. The equivalent functionality typically uses:
ctypes library for external function callsBigarray for controlled raw memory access Bytes.t for mutable byte sequencesOCaml programs rarely need operations equivalent to these Rust unsafe patterns.
Full Source
#![allow(clippy::all)]
//! 706 — UnsafeCell: The Foundation of Interior Mutability
//!
//! `UnsafeCell<T>` is the only primitive that lets you mutate data through
//! a shared reference (`&T`) without invoking undefined behaviour.
//! Every interior-mutability type in std — `Cell`, `RefCell`, `Mutex` — is
//! built on top of it. Here we build a `Cell<T>`-like type from scratch
//! to expose the mechanics.
use std::cell::UnsafeCell;
// ─── MyCell ──────────────────────────────────────────────────────────────────
/// A single-threaded mutable cell built directly on `UnsafeCell<T>`.
///
/// Mirrors `std::cell::Cell<T>` in behaviour but exposes the raw
/// `UnsafeCell` machinery so the seams are visible.
///
/// `UnsafeCell<T>` is `!Sync`, so `MyCell<T>` inherits `!Sync`
/// automatically — it cannot be shared across threads.
pub struct MyCell<T> {
inner: UnsafeCell<T>,
}
impl<T: Copy> MyCell<T> {
/// Wrap a value in the cell.
pub fn new(value: T) -> Self {
Self {
inner: UnsafeCell::new(value),
}
}
/// Replace the stored value.
///
/// # Safety rationale
/// `MyCell` is `!Sync`, so only one thread can hold a reference.
/// `UnsafeCell` disables the compiler's "shared ref ⇒ frozen memory"
/// assumption, making the raw-pointer write defined behaviour.
pub fn set(&self, value: T) {
// SAFETY: single-threaded (MyCell: !Sync), no aliased mutable refs.
unsafe { *self.inner.get() = value }
}
/// Return a copy of the stored value.
pub fn get(&self) -> T {
// SAFETY: same guarantee as `set`; we only read, so no data race.
unsafe { *self.inner.get() }
}
/// Apply a function to the stored value and write the result back.
pub fn update(&self, f: impl FnOnce(T) -> T) {
let v = self.get();
self.set(f(v));
}
}
// ─── MyOnceCell ──────────────────────────────────────────────────────────────
/// A write-once cell: after the first `set`, all subsequent writes are
/// silently ignored. Demonstrates a different usage pattern for `UnsafeCell`.
pub struct MyOnceCell<T> {
inner: UnsafeCell<Option<T>>,
}
impl<T> MyOnceCell<T> {
pub fn new() -> Self {
Self {
inner: UnsafeCell::new(None),
}
}
/// Store `value` if the cell is still empty; return `false` otherwise.
pub fn set(&self, value: T) -> bool {
// SAFETY: single-threaded, no concurrent access.
let slot = unsafe { &mut *self.inner.get() };
if slot.is_none() {
*slot = Some(value);
true
} else {
false
}
}
/// Return a reference to the value if it has been set.
pub fn get(&self) -> Option<&T> {
// SAFETY: we only hand out shared references; the value is never
// mutated after initialisation, so this is sound.
unsafe { (*self.inner.get()).as_ref() }
}
}
impl<T> Default for MyOnceCell<T> {
fn default() -> Self {
Self::new()
}
}
// ─── Tests ───────────────────────────────────────────────────────────────────
#[cfg(test)]
mod tests {
use super::*;
// ── MyCell tests ─────────────────────────────────────────────────────────
#[test]
fn cell_new_and_get() {
let c = MyCell::new(42_i32);
assert_eq!(c.get(), 42);
}
#[test]
fn cell_set_overwrites() {
let c = MyCell::new(0_i32);
c.set(7);
assert_eq!(c.get(), 7);
c.set(99);
assert_eq!(c.get(), 99);
}
#[test]
fn cell_update_accumulates() {
let c = MyCell::new(0_i32);
c.update(|v| v + 5);
c.update(|v| v + 3);
assert_eq!(c.get(), 8);
}
#[test]
fn cell_mutation_through_shared_ref() {
// Verify interior mutability: `cell` is not declared `mut`
// yet we can write to it through a shared reference.
let cell = MyCell::new(100_i32);
let r: &MyCell<i32> = &cell;
r.set(200);
assert_eq!(cell.get(), 200);
}
#[test]
fn cell_copy_types_work() {
let c = MyCell::new(true);
assert!(c.get());
c.set(false);
assert!(!c.get());
}
// ── MyOnceCell tests ─────────────────────────────────────────────────────
#[test]
fn once_cell_empty_on_creation() {
let c = MyOnceCell::<i32>::new();
assert!(c.get().is_none());
}
#[test]
fn once_cell_first_set_succeeds() {
let c = MyOnceCell::new();
assert!(c.set(42_i32));
assert_eq!(c.get(), Some(&42));
}
#[test]
fn once_cell_second_set_is_ignored() {
let c = MyOnceCell::new();
c.set(1_i32);
let accepted = c.set(2);
assert!(!accepted);
assert_eq!(c.get(), Some(&1));
}
#[test]
fn once_cell_write_once_through_shared_ref() {
let cell = MyOnceCell::new();
let r: &MyOnceCell<&str> = &cell;
r.set("hello");
assert_eq!(cell.get(), Some(&"hello"));
}
}#[cfg(test)]
mod tests {
use super::*;
// ── MyCell tests ─────────────────────────────────────────────────────────
#[test]
fn cell_new_and_get() {
let c = MyCell::new(42_i32);
assert_eq!(c.get(), 42);
}
#[test]
fn cell_set_overwrites() {
let c = MyCell::new(0_i32);
c.set(7);
assert_eq!(c.get(), 7);
c.set(99);
assert_eq!(c.get(), 99);
}
#[test]
fn cell_update_accumulates() {
let c = MyCell::new(0_i32);
c.update(|v| v + 5);
c.update(|v| v + 3);
assert_eq!(c.get(), 8);
}
#[test]
fn cell_mutation_through_shared_ref() {
// Verify interior mutability: `cell` is not declared `mut`
// yet we can write to it through a shared reference.
let cell = MyCell::new(100_i32);
let r: &MyCell<i32> = &cell;
r.set(200);
assert_eq!(cell.get(), 200);
}
#[test]
fn cell_copy_types_work() {
let c = MyCell::new(true);
assert!(c.get());
c.set(false);
assert!(!c.get());
}
// ── MyOnceCell tests ─────────────────────────────────────────────────────
#[test]
fn once_cell_empty_on_creation() {
let c = MyOnceCell::<i32>::new();
assert!(c.get().is_none());
}
#[test]
fn once_cell_first_set_succeeds() {
let c = MyOnceCell::new();
assert!(c.set(42_i32));
assert_eq!(c.get(), Some(&42));
}
#[test]
fn once_cell_second_set_is_ignored() {
let c = MyOnceCell::new();
c.set(1_i32);
let accepted = c.set(2);
assert!(!accepted);
assert_eq!(c.get(), Some(&1));
}
#[test]
fn once_cell_write_once_through_shared_ref() {
let cell = MyOnceCell::new();
let r: &MyOnceCell<&str> = &cell;
r.set("hello");
assert_eq!(cell.get(), Some(&"hello"));
}
}
Deep Comparison
OCaml vs Rust: UnsafeCell — The Foundation of Interior Mutability
Side-by-Side Code
OCaml
(* OCaml: mutable cells are first-class via `ref`.
Interior mutability is transparent — the type system does not
distinguish shared vs exclusive references. *)
let cell : int ref = ref 0
let set (c : int ref) (v : int) : unit = c := v
let get (c : int ref) : int = !c
let upd (c : int ref) (f : int -> int) : unit = c := f !c
let () =
upd cell (fun v -> v + 5);
upd cell (fun v -> v + 3);
Printf.printf "Cell value: %d\n" (get cell);
set cell 100;
Printf.printf "After set: %d\n" (get cell)
Rust (idiomatic — using std::cell::Cell)
use std::cell::Cell;
fn main() {
let cell = Cell::new(0_i32);
cell.update(|v| v + 5);
cell.update(|v| v + 3);
println!("Cell value: {}", cell.get());
cell.set(100);
println!("After set: {}", cell.get());
}
Rust (from scratch — raw UnsafeCell)
use std::cell::UnsafeCell;
pub struct MyCell<T> {
inner: UnsafeCell<T>,
}
impl<T: Copy> MyCell<T> {
pub fn new(value: T) -> Self { Self { inner: UnsafeCell::new(value) } }
pub fn set(&self, value: T) {
// SAFETY: MyCell is !Sync; only one thread at a time.
unsafe { *self.inner.get() = value }
}
pub fn get(&self) -> T {
unsafe { *self.inner.get() }
}
pub fn update(&self, f: impl FnOnce(T) -> T) {
self.set(f(self.get()));
}
}
Type Signatures
| Concept | OCaml | Rust |
|---|---|---|
| Mutable cell type | 'a ref | UnsafeCell<T> / Cell<T> |
| Allocate | ref 0 | UnsafeCell::new(0) |
| Write | c := v | c.set(v) |
| Read | !c | c.get() |
| Mutate in place | c := f !c | c.update(f) |
| Thread safety | not guaranteed | !Sync enforced by compiler |
Key Insights
ref is a heap-allocated mutable cell built into the runtime; the type system does not track aliasing, so
mutation through any number of copies of a ref is always safe (and always
possible). In Rust, mutating through a shared reference requires an explicit
opt-in — UnsafeCell.
UnsafeCell is the only blessed escape hatch.** Rust's memory model forbids mutation through &T unless UnsafeCell is somewhere in the
containment chain. Every interior-mutability type in std —
Cell, RefCell, Mutex, RwLock, AtomicUsize — wraps UnsafeCell
internally. Trying to sidestep it (e.g., casting *const T to *mut T
from a &T) is undefined behaviour.
!Sync is automatic.** Because UnsafeCell<T> does not implement Sync, any struct that contains it (like MyCell) also loses Sync for free.
This means the compiler prevents you from accidentally sharing a
MyCell across threads — you must use a Mutex or RwLock instead.
unsafe block documents intent, not just permission.** Writing unsafe { *self.inner.get() = value } forces you to articulate why this
is safe (single-threaded, no aliased mutable references). OCaml simply
allows the mutation without comment.
MyCell<T: Copy> allows repeated overwrites and reads. MyOnceCell<T> demonstrates a stricter contract — write-once
— that is impossible to express cleanly without UnsafeCell but trivial
to build with it.
When to Use Each Style
**Use std::cell::Cell<T> when:** you need single-threaded interior mutability
for Copy types and want zero-cost, zero-unsafe code; the standard library
already built the safe wrapper for you.
**Use UnsafeCell<T> directly when:** you are building a new synchronisation
primitive, a custom allocator-backed container, or an FFI-friendly struct where
neither Cell nor RefCell fit the contract you need to enforce.
Exercises
bytemuck for transmute, CString for FFI strings) and implement it.