Rust Rc, Weak, Cell and RefCell¶
Rc shares ownership; Cell and RefCell allow controlled mutation through shared access. Weak observes an allocation without keeping its value alive. Combining them is useful for single-threaded object graphs, but does not make those graphs thread-safe.
Prerequisites and outcome¶
Complete Box, Deref and Drop. You will distinguish ownership from access, release a runtime borrow explicitly, and explain why a weak back-link does not form a strong ownership cycle. The program uses Raspberry Pi labels and synthetic readings, not sensors or performance measurements.
Build the complete Rust shared-state example¶
Keep edition = "2024" in Cargo.toml. Replace src/main.rs with:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 | |
Expected binary output:
Eight tests cover shared state, empty input, overlapping guards, weak lifetimes, tree destruction, unique ownership, non-Copy Cell contents and a deliberately expected panic. That panic is a passing negative test, not the normal binary's behaviour.
Rc owns; cloning its handle does not copy Device¶
The two handles refer to one Device. Mutations made through observer are visible through device. Rc::clone increments the strong-owner count rather than calling Device::clone. Counts in this fixture explain ownership; they are not an application protocol. Moving an Rc still moves a binding as usual.
The last strong owner's destruction drops Device. A Weak does not keep Device alive, although it retains the backing allocation until weak handles are also released. upgrade returns Option
Rc normally exposes shared access. Rc::get_mut can provide &mut T when there are no other strong or weak handles, as the test demonstrates. Do not add RefCell merely because a value happens to be heap-allocated. For clone-on-write, Rc::make_mut is another distinct API; it may clone a shared payload and does not promise all handles keep observing one mutable object. See the Rc API and Weak API.
Cell and RefCell implement different access strategies¶
Interior mutability is a safe API's ability to change controlled contents through shared access. It does not grant permission to mutate arbitrary data behind &T. These standard-library containers encapsulate the implementation obligations; learning their APIs does not require writing unsafe code.
Cell's get copies out a T and requires T: Copy. set and replace can handle non-Copy values without handing out a shared reference to the stored value; replace returns the old owned value. take replaces it with T::default(), and into_inner consumes the container. Our counter uses get/set; the String test uses replacement instead. Cell is not limited to integers, and it does not use RefCell-style borrow guards. For details see Cell.
RefCell's borrow returns a Ref guard; borrow_mut returns a RefMut guard. Multiple readers or one writer are permitted, not both. Enforcement happens at runtime for that container. The surrounding references, moves and lifetimes still undergo compiler checking. This is not a switch that disables Rust's borrow checker. See RefCell.
try_borrow and try_borrow_mut report conflicts as errors. borrow and borrow_mut panic instead. A failed try does not wait for another operation or repair the conflict. Choose error handling when a conflict is an expected API outcome, and avoid hiding unexpected logic errors by silently discarding them.
A borrow ends when its guard is dropped¶
The readings variable owns a guard, not a copied Vec. Its last printed use does not itself call the guard's destructor; drop(readings) explicitly releases the dynamic borrow before record requests exclusive access. Every live read guard must be released, as the two-reader test shows.
Keep guards short-lived. A temporary borrow in a simple statement is released when that temporary's scope ends, but not every expression has the same temporary scope; consult temporary scopes. Named guards and explicit blocks make boundaries visible. Avoid calling a callback or a recursively invoked method while holding a guard if that code may borrow the same cell incompatibly. Such re-entrancy can fail even on one thread.
Prefer ordinary fields and &mut self when exclusive access naturally describes the operation. Interior mutability moves a potential failure to runtime and can make dependencies less obvious; it is a design choice, not a universal compiler-error repair.
Weak back-links separate navigation from ownership¶
Our root owns its child strongly; the child observes its parent weakly. The external child handle lets the child survive root destruction, but its parent link then upgrades to None. The inner block releases the temporary parent owner before dropping root.
If both directions hold strong Rc handles, the owners can keep one another alive after external handles disappear. Rust permits memory leaks in safe code: a leak is not automatically undefined behaviour. Decide which edges own values and which merely navigate. Weak breaks a cycle only when the remaining strong ownership graph no longer contains that cycle. See single-threaded reference counting.
For a different design, an owning Vec with indices or another explicit owner can avoid distributed ownership. Rc is useful when shared ownership is genuinely needed, not simply because two parts of a program read the same value.
Deliberately failing: shared ownership is not exclusive access¶
In a separate scratch project, replace src/main.rs with:
cargo check reports E0596: Rc does not supply DerefMut to String. A mutable handle binding does not establish unique ownership of the referent. An exclusive owner can use Rc::get_mut and handle None; a genuinely shared mutable design can choose an appropriate cell API.
Exercises and troubleshooting¶
- Move drop(readings) immediately after device.record(-500). cargo check still succeeds, but cargo run panics when borrow_mut encounters the live reader. Restore the original order and compare. Deleting the guard release entirely also leaves it alive across the later drop(device), causing E0505 at compile time instead. The should_panic test illustrates the runtime conflict without making the normal program fail.
- Hold a second read guard and drop only the first: try_borrow_mut must still return Err. After both readers end, one writer is allowed; while that writer lives, both another writer and a reader must fail. These cases are in the guard test.
- Replace the counter type with Cell
and attempt get: expect E0599 because String is not Copy. Use replace/take instead; the non-Copy test demonstrates the owned-value result. - Try to return &str from a function taking &RefCell
by borrowing and returning a reference into that temporary guard. Expect E0515. Return an owned String clone, or design an API that retains a Ref guard, rather than returning a reference whose guard has already ended. - Keep an upgraded parent handle alive across drop(root): the parent's value remains available until that handle is also dropped. Explain why this does not invalidate Weak's non-owning property.
- Apply Rc::get_mut to the failing scratch program. With the alias alive it returns None. Dropping the alias makes it succeed if no weak handles remain; do not unwrap before establishing that invariant.
Rc is neither Send nor Sync. Cell and RefCell are not Sync, although they can be Send when their contents satisfy the relevant bounds. Do not replace Rc with Arc and assume a RefCell inside becomes safe to share between threads. The cell module distinguishes single-threaded interior-mutability tools from synchronization tools. Threads, Send/Sync and locks follow in the next lesson.
Verification and next step¶
On October 10, 2026, this lesson was checked on the authorised Raspberry Pi 4B with 64-bit user space, kernel 6.18.50+rpt-rpi-v8, Rust and Cargo 1.99.0, and edition 2024. Cargo check, eight tests, formatting and debug/release output comparisons passed. The unique-owner repair and retained-parent variant passed. Direct Rc mutation, Cell
Next in the roadmap are threads, Send/Sync, Arc and locks. That lesson is planned, not yet available.