We can't find the internet
Attempting to reconnect
Something went wrong!
Attempting to reconnect
← Smart Pointers and Interior Mutability step 25 of 26
Build `Cell<T>` and `RefCell<T>` from scratch
Re-implement the two types this whole track has been built on, over
UnsafeCell, and drive each one from a script.
pub fn cell_script(ops: Vec<String>) -> Vec<i64>
pub fn refcell_semantics(script: Vec<String>) -> Vec<String>
Both interpreters are written for you. What is missing is MyCell<T> and
MyRefCell<T> — the parts that need unsafe.
MyCell<T> over two cells named A and B
| op | pushes |
|---|---|
set <c> <n> |
0 |
get <c> |
the value |
replace <c> <n> |
the old value |
take <c> |
the old value, leaving the default |
swap <c> <d> |
0 |
bump <c> <n> |
the new value, via get_mut |
consume <c> |
the old value, via into_inner, leaving a fresh cell |
| anything else, or an unknown cell name |
-1 |
MyRefCell<i64> with a stack of live guards
| op | logs |
|---|---|
share |
share ok <value> / share fail |
excl |
excl ok / excl fail |
write <n> |
write <n> if the innermost guard is exclusive, else write fail |
peek |
peek <value> from the innermost guard, else peek fail |
release |
release <state> after dropping the innermost guard, else release none |
state |
state <n> |
panic_share |
share ok, or panic: <message> |
panic_excl |
excl ok, or panic: <message> |
The state counter is the whole design: 0 free, n > 0 means n shared
borrows, −1 means one exclusive borrow. borrow() panics with
MyRefCell already mutably borrowed; borrow_mut() panics with
MyRefCell already borrowed — the same direction convention real RefCell
uses on 1.95.
The smallest complete sound abstraction in std
UnsafeCell<T> is the only type the compiler treats specially. It is the sole
legal way to get a *mut T — and from it a &mut T — out of a &T, because
it is the sole type for which the compiler suppresses the noalias promise
that &T otherwise carries. Everything with interior mutability in the
language is a safe wrapper over it, and today you write two of them.
Teach yourself by contrast, because the two designs answer the same question in opposite ways.
MyCell needs no bookkeeping at all. Not because it is clever, but
because of a decision about what it refuses to expose: it never hands out a
reference to its interior. get copies out, set writes over, replace and
swap exchange whole values. Since no &T into the cell can ever escape,
there is nothing that a later write could invalidate. Sound by construction,
zero runtime cost, no failure mode.
MyRefCell buys back the references and pays for them with a counter.
Now a &T can escape — inside a guard — so something has to stop a writer
appearing while it is alive. That something is one isize and a branch, and
the proof that it works is that the guard’s Drop restores the count on every
exit path, including a panic.
The rules your implementation has to respect
The guard must borrow the cell. MyRef<'a, T> holds &'a MyRefCell<T>,
and that lifetime is what stops a guard outliving the cell it is counting for.
Leave it off and the code often still compiles, because elision quietly
supplies one — which is exactly why this needs to be stated rather than
discovered.
The guard must not be Copy, and must not be constructible from outside.
A Copy guard would decrement the count once per copy. A publicly
constructible one would let anyone mint a borrow that was never checked. Both
would make the unsafe blocks unsound, and neither is something the compiler
can catch for you.
borrow_mut must not return &mut T directly. Clippy will stop you if
you try:
error: mutable borrow from immutable input(s)
(clippy::mut_from_ref)
mut_from_ref is in the correctness group and deny-by-default, and it
is one of the rare cases where a lint is the safety review: a
fn borrow_mut(&self) -> &mut T lets a caller obtain two &mut T at once
from two &self calls, which is instant undefined behaviour. Returning a
guard is what makes the design work at all.
The type must stay !Sync. It inherits that from UnsafeCell. If you
ever hit a thread-related error here, adding unsafe impl Sync would “fix”
it and would make the whole thing unsound — the counter is a plain Cell with
no synchronisation. That is precisely why RefCell is Send but never
Sync, and why Arc<RefCell<T>> does not compile.
swap A A
The real Cell::swap opens with:
if ptr::eq(self, other) { return; }
Your swap should too, and the swap A A case checks it. The invariant to
protect is that a cell swapped with itself is unchanged, and that you never
hand two aliasing pointers to a routine documented for two distinct
locations. Since Rust 1.81 Cell::swap additionally panics on cells that
partially overlap, which tells you how seriously the standard library takes
the aliasing question here even where a naive implementation happens to
produce the right answer on your machine. “Happens to work today” is not the
bar for unsafe.
The honest limit: mem::forget
This design depends on the destructor running. std::mem::forget(guard) is a
safe function that leaks a value without dropping it, and doing that to a
MyRef leaves the count stuck above zero forever. No exclusive borrow will
ever succeed again.
Is that a soundness hole? No — and understanding why is the sharpest lesson in this track. A stuck count only causes future borrows to fail. It never lets two conflicting borrows coexist, so it can never produce undefined behaviour. You get a permanently unusable cell, which is a bug, not a memory error.
Contrast that with the classic mem::forget unsoundness — leaking a guard
that was holding a borrow of a partially-moved buffer, which is how the
historical Vec::drain bug worked. There, forgetting the guard left the
collection in a state where safe code could read uninitialised memory. The
rule that fell out of that episode is the one this design satisfies: leaking
must always be safe, so an abstraction may never rely on a destructor running
for soundness — only for correctness.
The error waiting in the starter
error[E0133]: dereference of raw pointer is unsafe and requires
unsafe function or block
= note: raw pointers may be null, dangling or unaligned; they can violate
aliasing rules and cause data races
UnsafeCell::get() is safe to call — it just hands you a *mut T.
Dereferencing it is where you take responsibility, and the unsafe block is
where you say so. Write a // SAFETY: comment on every one of them explaining
which invariant makes it sound. Clippy’s undocumented_unsafe_blocks is
allow-by-default so the gate will not ask you for it; every reviewer you will
ever meet will.
Loading visualization…
Stuck?
Rust reference solution
Sign in to attempt this problem and reveal the reference solution.