Smart Pointers from Scratch
A shared apartment lease is a piece of paper with names on it. While at least one name is on the lease, the landlord keeps the apartment lit and the door unlocked. Add a roommate, write their name on. Roommate moves out, scratch their name off. When the last name comes off, the landlord turns the lights out and rents the place to someone else. A reference-counted pointer in Rust works the same way. The value sits on the heap, a little counter sits next to it, and every owner who clones the pointer adds one to the count. Every owner who drops the pointer subtracts one. When the count hits zero, the value gets destroyed. The pointer is "smart" because it knows when to clean up after itself.

The idea is older than C. In 1960 a programmer at MIT named George Collins wrote a paper on how to manage LISP memory without a garbage collector by attaching a small integer to every cell that tracked how many other cells pointed at it. Anything that hit zero was freed on the spot. The trick spread. Adobe used it in PostScript. NeXT used it in Objective-C with the methods retain and release — every iPhone app written before 2011 was a programmer remembering to call those two methods by hand and shipping a crash to the App Store every time they forgot. Apple finally automated it in 2011 with ARC, the Automatic Reference Counter, which made the compiler insert the retains and releases for you. Rust borrowed the same idea for its Rc<T> type, except in Rust you opt into it explicitly. The compiler does not sprinkle counters under your code. You write Rc::new(value) when you want the counted box, and you live with the rules.
Today you are going to build it. Not use it, build it. The whole point of Rust is that you can write something this fundamental in plain stdlib code, see every line, and end up with a thing that behaves like the real Rc in the standard library. The struct that holds the value and the counts goes on the heap inside a Box. The pointer struct that you hand around just wraps a raw address.
struct Inner<T> {
strong: Cell<usize>,
weak: Cell<usize>,
value: Option<T>,
}Inner<T> is the box on the heap. It holds the actual value, the strong count, and a second count called weak that we will get to. The Cell wrapper lets you change the integer through a shared reference, which is the only way the math works when many owners can each touch the same counter. The value is wrapped in Option<T> because there is a moment — after the last strong owner leaves but before the last weak watcher leaves — when the box still exists but the value inside it has been destroyed. The Option lets you say "the box is here, the value is gone."
pub struct MyRc<T> {
ptr: NonNull<Inner<T>>,
}
pub struct MyWeak<T> {
ptr: NonNull<Inner<T>>,
}The two pointer types are tiny. MyRc is a non-null pointer to the inner box, and that is it. MyWeak is the same pointer with different drop and clone behavior. The trick of reference counting is that the pointer struct itself is cheap — clone is a copy of an address plus one addition — and all the bookkeeping lives at the address it points to.
Creation, sharing, and reading happen in one block.
impl<T> MyRc<T> {
pub fn new(value: T) -> Self {
let boxed = Box::new(Inner {
strong: Cell::new(1),
weak: Cell::new(0),
value: Some(value),
});
let ptr = NonNull::new(Box::into_raw(boxed)).expect("box is non-null");
MyRc { ptr }
}
fn inner(&self) -> &Inner<T> {
unsafe { self.ptr.as_ref() }
}
pub fn strong_count(&self) -> usize {
self.inner().strong.get()
}
pub fn weak_count(&self) -> usize {
self.inner().weak.get()
}
pub fn downgrade(&self) -> MyWeak<T> {
let inner = self.inner();
inner.weak.set(inner.weak.get() + 1);
MyWeak { ptr: self.ptr }
}
pub fn get(&self) -> &T {
self.inner().value.as_ref().expect("value is alive")
}
}MyRc::new(value) puts the inner on the heap with Box::new, then calls Box::into_raw to peel the box back to a raw pointer. The raw pointer is what every clone of the MyRc will hold. The starting strong count is one — the owner who just called new. The function downgrade is how you ask for a weak handle. It bumps the weak count by one and hands you a MyWeak pointing at the same inner. A weak owner is on the call list but not on the lease. They can ask the landlord later "is my friend still living there?" but they do not keep the apartment open.
The two methods that do all the magic are clone and drop. Clone bumps strong. Drop subtracts strong, then checks the count, and runs the cleanup if it hit zero.
impl<T> Clone for MyRc<T> {
fn clone(&self) -> Self {
let inner = self.inner();
inner.strong.set(inner.strong.get() + 1);
MyRc { ptr: self.ptr }
}
}
impl<T> Drop for MyRc<T> {
fn drop(&mut self) {
let inner = unsafe { self.ptr.as_ref() };
let new_strong = inner.strong.get() - 1;
inner.strong.set(new_strong);
if new_strong == 0 {
unsafe { self.ptr.as_mut().value = None };
if inner.weak.get() == 0 {
drop(unsafe { Box::from_raw(self.ptr.as_ptr()) });
}
}
}
}Read the drop logic slowly. When the strong count hits zero, the value gets destroyed by setting the Option to None, which calls T's destructor — if T was a String, the string buffer gets freed right here. But the box itself stays alive if any weak owners are still holding the pointer, because they need the count fields to remain readable so their upgrade call can answer "nope, the value is gone." Only when both counts hit zero does the function call Box::from_raw to reconstruct ownership of the heap allocation and immediately drop it, returning the memory to the allocator.
The weak side mirrors the strong side, except upgrade is where the lesson is.
impl<T> MyWeak<T> {
pub fn upgrade(&self) -> Option<MyRc<T>> {
let inner = unsafe { self.ptr.as_ref() };
let strong = inner.strong.get();
if strong == 0 {
return None;
}
inner.strong.set(strong + 1);
Some(MyRc { ptr: self.ptr })
}
}
impl<T> Clone for MyWeak<T> {
fn clone(&self) -> Self {
let inner = unsafe { self.ptr.as_ref() };
inner.weak.set(inner.weak.get() + 1);
MyWeak { ptr: self.ptr }
}
}
impl<T> Drop for MyWeak<T> {
fn drop(&mut self) {
let inner = unsafe { self.ptr.as_ref() };
let new_weak = inner.weak.get() - 1;
inner.weak.set(new_weak);
if new_weak == 0 && inner.strong.get() == 0 {
drop(unsafe { Box::from_raw(self.ptr.as_ptr()) });
}
}
}upgrade reads the strong count. If it is zero, the value is gone and you get None. If it is greater than zero, the value is alive, and you bump the strong count by one and hand back a fresh MyRc. That promotion is how a weak handle becomes a strong handle for a moment so you can read the value without the value vanishing under you. The weak drop frees the box only when both the strong and weak counts have reached zero — the very last person to leave shuts off the lights.
Now run the demo. One lease, two roommates, one neighbor with the phone number, and then everyone leaves one by one.
fn main() {
let lease = MyRc::new(String::from("apartment 4B"));
println!("signed lease: strong={}, weak={}", lease.strong_count(), lease.weak_count());
let roommate = lease.clone();
let cousin = lease.clone();
println!("two more on lease: strong={}, weak={}", lease.strong_count(), lease.weak_count());
println!("everyone reads: '{}'", roommate.get());
let neighbor = lease.downgrade();
println!("neighbor has the number: strong={}, weak={}", lease.strong_count(), lease.weak_count());
drop(cousin);
drop(roommate);
println!("cousin and roommate left: strong={}, weak={}", lease.strong_count(), lease.weak_count());
match neighbor.upgrade() {
Some(guest) => println!("neighbor calls, lease still live: '{}'", guest.get()),
None => println!("neighbor calls, nobody home."),
}
drop(lease);
match neighbor.upgrade() {
Some(_) => println!("neighbor calls again, lease still live."),
None => println!("neighbor calls again: nobody home."),
}
}The output prints the counts after every move. Watch the strong count climb to 3 as roommates clone the lease, watch the weak count climb to 1 when the neighbor downgrades, and watch the strong count fall back to 1 as roommates drop out. The last upgrade after the final lease holder leaves returns None, because the value is gone — the apartment is dark.
signed lease: strong=1, weak=0
two more on lease: strong=3, weak=0
everyone reads: 'apartment 4B'
neighbor has the number: strong=3, weak=1
cousin and roommate left: strong=1, weak=1
neighbor calls, lease still live: 'apartment 4B'
neighbor calls again: nobody home.The real std::rc::Rc<T> in the Rust standard library does the same dance plus thread safety and panic handling. It also forbids you from mutating the value through the shared pointer, which you would have noticed if you tried to write to the string inside the lease — the get method here returns &T, not &mut T. To get mutation you wrap your value in a RefCell or a Mutex first, and now you have two smart wrappers stacked: one for sharing, one for mutating. That is the standard Rust idiom for shared mutable state and it falls right out of the rules you just wrote.

One question worth chewing on — why does MyRc exist at all if Rust gives you references and lifetimes for free? Because some shapes of data cannot be expressed as a tree of owners. A graph where two nodes both point to a third node has no single owner of that third node, and Rust's normal borrow checker has no way to express joint ownership. Rc is the escape hatch. You pay a small runtime cost in counter math, and in exchange you get to build graphs, doubly linked lists, and shared caches without the compiler refusing to compile.
Next lesson — what happens when reference counting traps itself in a cycle and the counters never reach zero, and why Weak is the tool that breaks the loop.