We can't find the internet
Attempting to reconnect
Something went wrong!
Attempting to reconnect
← The Expert Edge: Idiom, Review and Capstones step 4 of 14
Build Rc<T> and RefCell<T> from scratch
Implement MyRc<T> and MyRefCell<T> yourself, then drive both through a
scripted workload and return the merged event log.
pub fn scaffold(script: Vec<String>) -> Vec<String>
You have built each of these separately. Here they are combined, over the same
tracked payload, with one log recording reference counts, borrow outcomes and
drop events — because the interesting failures only show up when the two are
composed. MyRc<MyRefCell<Node>> is the single most common shared-mutable
shape in Rust, and it is also the shape that leaks.
The scaffold — the world map, the command dispatcher, Node, emit — is
given and must not be changed. Everything above the the scaffold banner is
yours: every body is todo!().
What you must build
struct RcBox<T> { strong: Cell<usize>, value: T }
pub struct MyRc<T> { ptr: NonNull<RcBox<T>>, _owns: PhantomData<RcBox<T>> }
MyRc::new, inner, strong_count, try_clone (returning None on
overflow), the unsafe fn set_strong test seam, plus Clone, Deref and
Drop.
pub struct MyRefCell<T> { state: Cell<isize>, value: UnsafeCell<T> }
state is 0 when free, n > 0 for that many shared borrows, -1 for one
exclusive borrow. try_borrow, try_borrow_mut, try_swap, plus Deref /
DerefMut / Drop on both guard types.
The four things the script is designed to catch
1. The self-swap trap. swap a a asks a cell to exchange contents with
itself. Your try_swap takes two &Self, and nothing in the type system says
they are different cells. If you take one exclusive borrow and then the other,
the second must fail — which is exactly what std’s RefCell::swap does
(it panics). If your implementation reaches in through UnsafeCell::get
twice, you have created two &mut to the same place, which is undefined
behaviour, and the script will show swap a a ok where it should show busy.
2. Refcount overflow. Rc cannot let its count wrap, because a wrap to
zero frees memory that live handles still point at — a use-after-free
reachable from entirely safe code. Real Rc handles this with a saturating
abort; you handle it with checked_add and a None. The poison command
uses the unsafe fn set_strong seam to slam the count to usize::MAX so the
guard is reachable in a test rather than after 2⁶⁴ clones.
::: question set_strong only writes a number. Why is it unsafe?
Because the number is the safety invariant. MyRc‘s Drop frees the
allocation when the count reaches zero, and it is correct only if the count
equals the number of live handles. A safe function that lets a caller write
any value at all lets safe code cause a use-after-free — and “safe code cannot
cause undefined behaviour” is the entire contract of the language.
This is the general rule for unsafe abstractions: the unsafe keyword belongs
on every operation that can break the private invariant, not only on the
ones that dereference a pointer. Raising the count, as poison does, is
actually fine — it only leaks. Lowering it is a catastrophe. One function can
do both, so it is unsafe, and its # Safety section says which direction is
dangerous.
:::
3. Guard-drop ordering. probe takes a shared borrow, tries an exclusive
one while it is held (must fail), drops both, and tries again (must succeed).
Run it twice in a row and it must give the same answer both times. That
catches the classic bug: a guard whose Drop forgets to restore the count, or
restores it to the wrong value, leaving the cell permanently poisoned. Notice
what is doing the work here — the borrow counter is only correct because
Drop runs, which makes MyRef/MyRefMut a textbook case of a type whose
soundness depends on its destructor.
4. The cycle that must leak. link a b then link b a builds two nodes
that hold MyRc handles to each other. Releasing every external handle leaves
both counts at 1, so nothing is freed and neither Node::drop runs. The
expected log for that case contains no drop lines at all, and if yours
does, your Drop is freeing memory that is still referenced.
This is not a bug in your implementation or in std’s. Rc is a reference
count, and reference counting cannot collect cycles — that is a documented,
permanent property, and the standard answer is to make one direction of the
cycle a Weak. Leaking memory is safe in Rust; mem::forget is a safe
function for exactly this reason. Your MyRc is required to leak here in
precisely the way std’s does.
The honest limit of this grade
There is no Miri in this harness. A sloppy implementation with real
undefined behaviour can pass every case below, because UB is not obliged to
misbehave and -O may compile an aliasing violation into code that happens to
do what you meant. The cases have been chosen so that the specific mistakes
people actually make — the double &mut in swap, the guard that does not
restore its counter, the count that wraps — produce a different observable
log. That is a real net, but it is a net with holes.
So write it as though something is watching: one // SAFETY: comment per
unsafe block, saying which invariant makes that operation sound, and a
# Safety section on every unsafe fn. If you cannot write the sentence, you
do not have the proof, and passing the tests did not give you one.
Two clippy lints in this area are worth knowing even though this design avoids
them. mut_from_ref is deny-by-default and fires on any safe
fn f(&self) -> &mut T — the whole reason borrow_mut returns a guard
rather than a reference. non_canonical_clone_impl catches a Clone that
does something other than what Clone promises.
The scripting language
new <name> <payload>, clone <name>, release <name>, count <name>,
read <name>, write <name> <text>, probe <name>, swap <a> <b>,
link <a> <b>, poison <name>. Anything else logs bad <line>. After the
last command the log gets a teardown line and the world is dropped in key
order, so the final drops are deterministic.
Loading visualization…
Stuck?
Rust reference solution
Sign in to attempt this problem and reveal the reference solution.