137. Box, Rc and RefCell
Rust’s standard library has three tools for data that doesn’t fit one owner and a few borrows. Try to build a VM’s object graph out of them and each one gives way somewhere: Rc leaks cycles, and RefCell turns a borrowing mistake into a crash at run time.
- Use Box to put a value on the heap, and to build a recursive type such as a syntax tree.
- Share a value between several owners with Rc, and mutate it through RefCell.
- Show why a cycle of Rc pointers leaks, and why a RefCell double borrow panics.
- Explain why drip-rs doesn’t build its object graph from Rc<RefCell<..>>.
Who owns the upvalue?
Here is a variation on chapter 57’s counter, in Drip:
fn makeCounter() {
let n = 0;
fn inc() { n = n + 1; }
fn get() { return n; }
inc();
return get;
}
inc and get both capture n. In the TypeScript VM, both closures
hold a reference to one ObjUpvalue, and nobody asks which closure
owns it: JavaScript’s collector frees it once nothing can reach it.
Rust asks that about every value: one owner, and borrows that end (chapter 124). A VM’s heap breaks both rules constantly. Closures share upvalues; a field can hold a bound method whose receiver is the instance itself. The standard library has three tools for data that doesn’t fit the rules. Let’s see how far they get.
Box: one owner, on the heap
A Box is a locker key. The thing itself is in a locker down the hall
(the heap); you carry only the key. Hand the key to someone and the
locker is theirs. Whoever holds the key when they leave empties it.
Try to write chapter 31’s syntax tree as a Rust enum:
enum Expr {
Num(f64),
Add(Expr, Expr),
}
Rust 1.97 (trimmed):
error[E0072]: recursive type `Expr`
has infinite size
3 | Add(Expr, Expr),
| ---- recursive without
| indirection
help: insert some indirection (e.g.,
a `Box`, `Rc`, or `&`)
An Expr holding two Exprs would contain itself, so it has no
finite size. A Box<Expr> is a pointer, 8 bytes on this machine, so
Add(Box<Expr>, Box<Expr>) has a fixed size; the children live on the
heap.
A Box<T> is a pointer to a T stored on the heap. The box is the
T’s only owner: moving the box moves ownership, and dropping the box
drops the T and frees its memory[1]. No counting
happens at run time.
A tree is the perfect case: each node has one parent, which owns it.
Rc: shared, counted ownership
The upvalue has two owners, so a Box won’t do.
Rc is a shared flat with a sign-in sheet on the door. Each new tenant
signs in; each one who moves out signs off. The last to leave turns off
the lights.
Rc::new(v) puts v on the heap next to a counter set to 1.
Rc::clone(&r) doesn’t copy v: it copies the pointer and adds 1.
Dropping an Rc subtracts 1, and when the count reaches 0, v is
dropped[2].
There is a catch. Try to change the upvalue through one owner:
let up = Rc::new(Upvalue { value: 0.0 });
let a = Rc::clone(&up);
a.value += 1.0;
error[E0594]: cannot assign to data in
an `Rc`
Of course: a and up point at the same value, and chapter 125 said
many readers or one writer. An Rc only gives out shared access.
An Rc<T> (“reference counted”) is a pointer to a heap value that
has several owners. It stores a strong count: Rc::clone adds 1,
dropping an Rc subtracts 1, and the value is dropped when the count
reaches 0. Through an Rc you can only read[2].
RefCell: the borrow checker, at run time
A library book with a slip inside the cover. Anyone may read it in the reading room at the same time, but to take it home and write in the margins you need everyone else to hand it back first. The librarian checks the slip each time, not once when the library was built.
A RefCell<T> holds a value and a little borrow counter. borrow()
gives you a reader, borrow_mut() a writer, and each call first checks
the rules from chapter 125. Put one inside an Rc and you have shared,
mutable data: Rc<RefCell<Upvalue>>, the textbook way to share mutable
state[1].
use std::cell::RefCell;
use std::rc::Rc;
// A captured variable, shared by
// two closures (chapter 57).
struct Upvalue {
value: f64,
}
struct Closure {
name: &'static str,
up: Rc<RefCell<Upvalue>>,
}
fn main() {
let up = Rc::new(RefCell::new(
Upvalue { value: 0.0 },
));
let inc = Closure {
name: "inc",
up: Rc::clone(&up),
};
let get = Closure {
name: "get",
up: Rc::clone(&up),
};
println!("owners: {}",
Rc::strong_count(&up));
inc.up.borrow_mut().value += 1.0;
inc.up.borrow_mut().value += 1.0;
let v = get.up.borrow().value;
println!("{} sees {}", get.name, v);
drop(inc);
drop(get);
println!("owners: {}",
Rc::strong_count(&up));
}It prints owners: 3 (the original plus two closures), get sees 2,
and then owners: 1. It works.
But “checked at run time” means failing at run time too. Drip happily
runs a.heal(a): heal a using its own hit points. Here is that
method written with RefCell:
fn heal(me: &RefCell<Instance>,
other: &RefCell<Instance>) {
let mut m = me.borrow_mut();
let o = other.borrow();
m.hp += o.hp / 2.0;
}
heal(&a, &b) is fine. Then heal(&a, &a), built with
cargo and rustc 1.97 (the panic line is wrapped here):
a: 12
thread 'main' (24945) panicked at
src/main.rs:12:19:
RefCell already mutably borrowed
The compiler saw two parameters and no broken rule. The rule was broken by which objects were passed, and in a VM the Drip program makes that choice. Any VM code that holds a borrow while it touches another object is one aliasing program away from a panic.
A RefCell<T> moves borrow checking from compile time to run time.
borrow() returns a shared guard and borrow_mut() an exclusive one;
each guard releases its borrow when dropped. Asking for a borrow that
breaks the rules (a writer while anyone else holds one, or a reader while
a writer does) panics[3].
Cycles never reach zero
Give each object a field that can hold another, and a Drop that
prints. Then build a chain and a cycle:
// Obj { name, field }, with a Drop
// that prints "free" and the name
{
let a = new("a");
let b = new("b");
a.borrow_mut().field =
Some(Rc::clone(&b));
}
{
let c = new("c");
let d = new("d");
c.borrow_mut().field =
Some(Rc::clone(&d));
d.borrow_mut().field =
Some(Rc::clone(&c));
println!("c has {} owners",
Rc::strong_count(&c));
}
println!("end of main");
Run with rustc 1.97, it prints:
free a
free b
c has 2 owners
end of main
The chain is freed. c and d never are. Step through the counts:
{let c = new("c");let d = new("d");c.borrow_mut().field =Some(Rc::clone(&d));d.borrow_mut().field =Some(Rc::clone(&c));} // c and d go out of scope
The standard library is blunt: a cycle between Rc pointers will
never be deallocated[2]. The Rust book adds that
a leak is still memory safe[1]. The usual fix is
Weak, a non-owning pointer for the edge that points “back”.
A VM can’t use that fix. let a = Node(); a.me = a; is a one-line
cycle, and a recursive local function captures itself through an
upvalue. The program decides which edges point back. A VM needs the
question mark-and-sweep asks (chapter 58): what can the roots reach?
drip-rs’s object.rs opens with this argument: safe Rust can’t link
objects with raw pointers as clox does, and Rc<RefCell<..>> “would
hand memory management to Rust instead of to our own garbage
collector”. Chapter 138 shows what it does instead. The tools still
appear where they fit. A string’s characters are a Box<str>, owned by
one string object. Each compiled function’s bytecode is an Rc<Chunk>,
shared by the function object, every call frame running it, and the
VM register that holds the running chunk. A chunk is frozen once compiled and holds no Rc itself: no
RefCell needed, and no cycle possible.
Your turn
A closure captures a variable by sharing it (chapter 57): two closures that capture the same variable see each other’s changes. Write capture, which makes a Closure that shares up, and the methods bump (add 1 to the variable) and read.
use std::cell::RefCell;
use std::rc::Rc;
// One captured variable, shared.
type Up = Rc<RefCell<f64>>;
struct Closure {
up: Up,
}
// Share `up`; don't copy it.
fn capture(up: &Up) -> Closure {
todo!()
}
impl Closure {
// Add 1 to the captured variable.
fn bump(&self) {
todo!()
}
fn read(&self) -> f64 {
todo!()
}
}A syntax tree (chapter 31) is the shape Box was made for: each node has one parent. Write add and mul, which build a node from two subtrees, and eval, which computes the tree’s value.
enum Expr {
Num(f64),
Add(Box<Expr>, Box<Expr>),
Mul(Box<Expr>, Box<Expr>),
}
fn add(a: Expr, b: Expr) -> Expr {
todo!()
}
fn mul(a: Expr, b: Expr) -> Expr {
todo!()
}
fn eval(e: &Expr) -> f64 {
todo!()
}What you learned
- A
Box<T>puts a value on the heap with exactly one owner. It makes recursive types like syntax trees possible. - An
Rc<T>shares a value among owners with a strong count, and drops it when the count reaches 0. It only gives shared access. - A
RefCell<T>checks the borrowing rules at run time, and panics when they are broken:RefCell already mutably borrowed. - Cycles of
Rcnever reach a count of 0, so they leak. A VM’s object graph is full of cycles that the program, not you, decides. - drip-rs uses
BoxandRconly where ownership is simple, and gives its objects to its own collector instead.
Check your understanding
0/6- 1.
Which of these is a good fit for plain
Box<T>? - 2.
After
let a = Rc::new(5); let b = Rc::clone(&a); drop(a);, what isRc::strong_count(&b)? - 3.
You write
a.borrow_mut()and, while that guard is alive, calla.borrow(). When do you find out? - 4.
Why does a cycle of
Rcpointers leak? - 5.
Which Drip program makes a cycle of objects?
- 6.
What does drip-rs use instead of
Rc<RefCell<..>>for its objects?
References
- [1] Steve Klabnik, Carol Nichols, Chris Krycho (2026). The Rust Programming Language (3rd edition). No Starch Press; free online at doc.rust-lang.org/book. doc.rust-lang.org/book/ (checked 2026-10-31)
- [2] The Rust Project Developers. Module alloc::rc (std::rc), Rust standard library documentation. Rust standard library API documentation; source in library/alloc/src/rc.rs of github.com/rust-lang/rust. doc.rust-lang.org/std/rc/index.html (checked 2026-10-31)
- [3] The Rust Project Developers. Module core::cell (std::cell), Rust standard library documentation. Rust standard library API documentation; source in library/core/src/cell.rs of github.com/rust-lang/rust. doc.rust-lang.org/std/cell/index.html (checked 2026-10-31)