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"]