ExamplesBy LevelBy TopicLearning Paths
706 Advanced

unsafe cell

Functional Programming

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

  • • The specific unsafe feature demonstrated: unsafe cell
  • • When this feature is necessary vs when safe alternatives exist
  • • How to use it correctly with appropriate SAFETY documentation
  • • The invariants that must be maintained for the operation to be sound
  • • Real-world contexts: embedded systems, OS kernels, C FFI, performance-critical code
  • 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

  • Safety model: Rust requires explicit unsafe for these operations; OCaml achieves safety through the GC and type system without explicit unsafe regions.
  • FFI approach: Rust uses raw C types directly with extern "C"; OCaml uses ctypes which wraps C types in OCaml values.
  • Memory control: Rust allows complete control over memory layout (#[repr(C)], custom allocators); OCaml's GC manages memory layout automatically.
  • Auditability: Rust unsafe regions are syntactically visible and toolable; OCaml unsafe operations (Obj.magic, direct C calls) are also explicit but less common.
  • OCaml Approach

    OCaml's GC and type system eliminate most of the need for these unsafe operations. The equivalent functionality typically uses:

  • • C FFI via the ctypes library for external function calls
  • Bigarray for controlled raw memory access
  • • The GC for memory management (no manual allocators needed)
  • Bytes.t for mutable byte sequences
  • OCaml 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"));
        }
    }
    ✓ Tests Rust test suite
    #[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

    ConceptOCamlRust
    Mutable cell type'a refUnsafeCell<T> / Cell<T>
    Allocateref 0UnsafeCell::new(0)
    Writec := vc.set(v)
    Read!cc.get()
    Mutate in placec := f !cc.update(f)
    Thread safetynot guaranteed!Sync enforced by compiler

    Key Insights

  • No special primitive in OCaml. OCaml's 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 stdCell, 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.

  • **The 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.

  • Two cells, two contracts. 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

  • Minimize unsafe: Find the smallest possible unsafe region in the source and verify that all safe code is outside the unsafe block.
  • Safe alternative: Identify if a safe alternative exists for the demonstrated technique (e.g., bytemuck for transmute, CString for FFI strings) and implement it.
  • SAFETY documentation: Write a complete SAFETY comment for each unsafe block listing preconditions, invariants, and what would break if violated.
  • Open Source Repos