Language Internals
Part 8 of 11 · Rust Language ProficiencySmart Pointers & Interior Mutability
Box, Rc/Arc, RefCell/Mutex interior mutability.
- 1Gist
- 2Maps
- 3Q&A
- 4Sandbox
Voice readout needs Web Speech Synthesis in this browser.
Question ladder
L1
What does Box add?
Answer
A unique owner on the heap. The Box drops the inner value. Size of the Box itself is a pointer.
L2
Why Box a recursive type?
Answer
An enum that contains itself would have infinite size. Box makes the variant a pointer.
L3
Rc or Arc?
Answer
Rc is single-threaded shared ownership. Arc is atomic and Send so threads can share it. Neither one allows mutation by itself.
L4
What is interior mutability?
Answer
Mutation through a shared reference, checked at runtime. RefCell on one thread. Mutex or RwLock across threads.
L5
Why does RefCell panic?
Answer
It enforces the same shared-XOR-exclusive rule dynamically. A second exclusive borrow while one is live panics.
L6
What does Arc not do?
Answer
It does not synchronize the inner data. You still need a Mutex, an RwLock, or an atomic.
L7
When do you use Weak?
Answer
To break an Rc or Arc cycle so the strong count can hit zero and Drop can run.
Failure modes
Rc across threads
Rc is not Send. The compiler rejects the move onto a thread.
Arc without a lock
Shared owners of a plain mut value do not compile, or a data race if you force it in unsafe.
RefCell borrow leak
A guard stays live across a second borrow and the thread panics.
Reference cycle
Parent and child both hold Rc, so neither drops.
Misconceptions
Arc is a mutex.
Arc shares ownership with an atomic count. Mutex is the lock. The usual pair is Arc of Mutex.
RefCell makes the program multi-threaded.
RefCell is single-threaded. Crossing threads wants Mutex or RwLock inside Arc.
Box is slower ownership.
It is still unique ownership. You use it for size, recursion, or a trait object, not because the value is shared.
Interviewer traps
Putting Mutex inside Rc for threads.
Rc is not Send. The thread-safe pair is Arc and Mutex.
Using interior mutability as the default API.
Start with a unique owner and a borrow. Add RefCell or Mutex when the graph really shares.
Design scenario
Same prompt for every reader.
Requirements
Rc and RefCell for the widgets. Arc and Mutex for the worker. No RefCell on the thread.
Traffic / scale
A handful of widgets, one background bump.
Latency
The RefCell path must not take a kernel lock. The thread path must.
Consistency
Two exclusive RefCell borrows cannot overlap.
Availability
A cycle must not pin the cache forever.
Failure assumptions
- The same Rc is moved onto a thread.
- Arc alone is treated as a lock.
Constraints
- Stay on smart pointers. Do not start Tokio yet.
Prompt
A single-threaded UI cache shares a counter with several widgets. A later worker thread must bump a different counter.
Unique heap owner or shared owners
Prefer
Name the sharing
Box is still one owner. Rc and Arc add a count. Mutation is a separate runtime check.
- RefCell is one thread.
- Mutex is the cross-thread write.
- Weak breaks cycles.
Alternative
GC alias
TypeScript and Python share by assignment and mutate unless you freeze the object.
- No refcount in the type.
- Cycles need the collector.
- Threads add locks later, ad hoc.
Overview
Plain references cannot express a heap owner, a shared owner, or mutation through a shared owner. Smart pointers and interior mutability are those patterns. They do not replace the borrow rules. They move the check to runtime where the graph requires it.
Comparative
| Need | TypeScript | Python | Rust |
|---|---|---|---|
| Heap object | object | object | Box unique owner (heap indirection) |
| Shared owners | GC alias | GC alias | Rc or Arc |
| Mutate the shared value | default | default | RefCell or Mutex |
Decisions
- 1
Step 1 Need indirection or shared state
- nextStep 2 Who owns it
- ?
Step 2 Who owns it
- one owner, recursive typeStep 3a Box of T
- many owners, one threadStep 3b Rc of T
- many owners, many threadsStep 3c Arc of T
- 3
Step 3a Box of T
- 4
Step 3b Rc of T
- nextStep 4a Mutate through it - RefCell, borrows checked at runtime
- Rc cycleFailure path - memory leak; break it with Weak
- 5
Step 3c Arc of T
- nextStep 4b Mutate through it - Mutex, RwLock, or atomics
- 6
Step 4a Mutate through it - RefCell, borrows checked at runtime
- two borrow_mut alive at onceFailure path - BorrowMutError panic
- 7
Step 4b Mutate through it - Mutex, RwLock, or atomics
- 8
Failure path - BorrowMutError panic
- 9
Failure path - memory leak; break it with Weak
Lesson map
Smart Pointers & Interior Mutability
Box, Rc/Arc, RefCell/Mutex interior mutability.
Architecture. Step 1 Need indirection or shared state Ready. Step 2 Who owns it Ready. Step 3a Box of T Ready. Step 3b Rc of T Ready. Step 3c Arc of T Ready. Step 4a Mutate through it - RefCell, borrows checked at runtime Ready. Step 4b Mutate through it - Mutex, RwLock, or atomics Ready. Failure path - BorrowMutError panic Ready. Failure path - memory leak; break it with Weak Ready
Select a node to see why it exists, or an edge to see the protocol, direction, effect, and consequence.
Mermaid export
flowchart TB A["Step 1 Need indirection or shared state Ready"] B["Step 2 Who owns it Ready"] C["Step 3a Box of T Ready"] D["Step 3b Rc of T Ready"] E["Step 3c Arc of T Ready"] F["Step 4a Mutate through it - RefCell, borrows checked at runtime Ready"] G["Step 4b Mutate through it - Mutex, RwLock, or atomics Ready"] X["Failure path - BorrowMutError panic Ready"] Y["Failure path - memory leak break it with Weak Ready"] A -->|continues| B B -->|one owner, recursive type| C B -->|many owners, one thread| D B -->|many owners, many threads| E D -->|continues| F E -->|continues| G F -->|two borrow_mut alive at once| X D -->|Rc cycle| Y
Press Run. Snippets must be self-contained — no network, files, or native modules.
Rosetta — shared mutable counter
use std::cell::RefCell;
use std::rc::Rc;
fn main() {
let n = Rc::new(RefCell::new(0));
let a = Rc::clone(&n);
*a.borrow_mut() += 1;
println!("{}", n.borrow());
}const n = { value: 0 };
const a = n;
a.value += 1;
console.log(n.value);n = {"value": 0}
a = n
a["value"] += 1
print(n["value"])TypeScript and Python alias. Rust makes the shared owner (Rc) and the runtime borrow (RefCell) visible. A second borrow_mut while the first guard lives panics.
Rosetta — Box and a recursive type
enum List {
Empty,
More(i32, Box<List>),
}type List = { value: number; next?: List };from dataclasses import dataclass
@dataclass
class List:
value: int
next: "List | None" = NoneWithout Box, the Rust enum would contain itself and have no finite size. The GC languages store a reference already.
Rosetta — Arc for threads
use std::sync::{Arc, Mutex};
use std::thread;
fn main() {
let n = Arc::new(Mutex::new(0));
let n2 = Arc::clone(&n);
let handle = thread::spawn(move || {
*n2.lock().unwrap() += 1;
});
handle.join().unwrap();
println!("{}", n.lock().unwrap());
}const n = { value: 0 };
n.value += 1;
console.log(n.value);from threading import Lock, Thread
n = 0
lock = Lock()
def bump() -> None:
global n
with lock:
n += 1
t = Thread(target=bump)
t.start()
t.join()
print(n)Arc is the shared owner. Mutex is the synchronization. Rc cannot move onto the thread. A JavaScript object is usually confined to one event loop. Python still wants a lock for the compound update even with the GIL.
Interview Q&A
Why does RefCell panic?
Answer
It checks borrows at runtime. An exclusive borrow that overlaps another live borrow panics instead of compiling, because the checker could not see the split.
Arc or Mutex?
Answer
Arc shares ownership across threads with an atomic count. Mutex synchronizes mutation. You usually want Arc of Mutex, not one instead of the other.
Why is Rc the wrong thread pointer?
Answer
Rc is not Send. Its count is not atomic. Use Arc.
When is Box enough?
Answer
One owner, a large value you do not want to move by bits, a recursive enum, or a trait object.
What is Weak for?
Answer
A non-owning pointer that does not keep the strong count alive, so a parent and child cycle can drop.
How is this different from a TypeScript object?
Answer
Assignment shares under the collector. There is no RefCell panic and no Send bound.
How is this different from a Python dict alias?
Answer
Both names mutate the same object. The GIL does not replace the Mutex when the update is a read-modify-write you care about.
Which standard library is this?
Answer
std::cell and std::sync as shipped with rustc 1.98.1. Edition 2024 does not change Rc versus Arc.
Pitfalls
- Rc moved onto a thread.
- Arc of a value you then mutate unsafely.
- Holding a RefCell guard across a second borrow.
- A parent and child that both use strong counts.
- Interior mutability on an API that could take an exclusive borrow.
A widget tree shares a counter on one thread, and a worker thread shares a different counter. Name the two pointer pairs and the one you must not cross.