Interior Mutability

Runtime Borrow Checking

Runtime borrow checking is the state-machine mechanism inside RefCell<T> that enforces Rust's aliasing invariant—either any number of shared readers (&T) or a single exclusive writer (&mut T)—by maintaining a dynamic integer counter (BorrowFlag) at runtime instead of proving disjointness at compile time.

The BorrowFlag State Machine

Inside std::cell::RefCell<T>, the borrow state is stored in a single Cell<isize> (referred to internally as BorrowFlag):

                  borrow()                    borrow()
        +--------------------------> +-------------------------->
        |                            |
   [ UNUSED: 0 ]                [ SHARED: 1 ]               [ SHARED: 2 ... ]
        |   ^                        |                            |
        |   +------------------------+ <--------------------------+
        |         drop(Ref)                    drop(Ref)
        |
        | borrow_mut()               drop(RefMut)
        +--------------------------> [ EXCLUSIVE: -1 ] -----------+
                                                                  |
        ^---------------------------------------------------------+
Current BorrowFlag State .borrow() Action .borrow_mut() Action
0 (Unused) Increments flag to 1; returns Ref<'_, T> Decrements flag to -1; returns RefMut<'_, T>
n > 0 (n active Refs) Increments flag to n + 1; returns Ref<'_, T> Fails (BorrowMutError / panics)
n < 0 (active RefMuts) Fails (BorrowError / panics) Fails (BorrowMutError / panics)

Note that when RefMut::map_split splits a mutable guard into two disjoint mutable sub-guards, the negative counter decrements further (-1 $\rightarrow$ -2), and only returns to 0 when all split RefMut sub-guards have been dropped.

Comparing Compile-Time and Runtime Borrow Checking

Dimension Compile-Time Borrow Checker (&T / &mut T) Runtime Borrow Checker (RefCell<T>)
Verification time Compile time (rustc) Runtime (on every .borrow() / .borrow_mut() / Drop)
Failure mode Compiler error (E0502, E0499) Runtime panic (or Err via try_borrow*)
Memory overhead 0 bytes 8 bytes (isize counter) per RefCell<T> on 64-bit
CPU overhead 0 instructions Integer comparison + increment/decrement per borrow/drop
Granularity Field-level disjointness (&mut s.a and &mut s.b simultaneously) Whole-cell granularity (unless split via RefMut::map_split)

Whole-Cell Granularity vs. Struct Field Disjointness

At compile time, the borrow checker understands that borrowing &mut player.health and &player.name accesses two disjoint fields of player. Because RefCell<Player> tracks a single BorrowFlag for the entire Player struct, calling player.borrow_mut().health += 10 while player.borrow().name is held in the same expression will conflict unless the guards are sequenced or split via RefMut::map_split (or each independently mutated field is wrapped in its own Cell / RefCell):

use std::cell::{Cell, RefCell};

// Granular design: wrap only the fields that need independent interior mutability
struct Player {
    name: String,
    health: Cell<u32>,
    inventory: RefCell<Vec<&'static str>>,
}

fn main() {
    let player = Player {
        name: String::from("Aria"),
        health: Cell::new(100),
        inventory: RefCell::new(vec!["shield"]),
    };

    // Borrowing `inventory` mutably never conflicts with reading `name` or updating `health`:
    let mut inv = player.inventory.borrow_mut();
    player.health.set(player.health.get() - 15);
    inv.push("potion");

    println!(
        "player {} -> health: {}, inventory: {:?}",
        player.name,
        player.health.get(),
        *inv
    );
}
player Aria -> health: 85, inventory: ["shield", "potion"]