Skip to content

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

cargo new pi_shared
cd pi_shared

Keep edition = "2024" in Cargo.toml. Replace src/main.rs with:

use std::cell::{Cell, RefCell};
use std::rc::{Rc, Weak};

struct Device {
    label: String,
    visits: Cell<u32>,
    readings: RefCell<Vec<i32>>,
    parent: RefCell<Weak<Device>>,
    children: RefCell<Vec<Rc<Device>>>,
}

impl Device {
    fn new(label: &str) -> Rc<Self> {
        Rc::new(Self {
            label: String::from(label),
            visits: Cell::new(0),
            readings: RefCell::new(Vec::new()),
            parent: RefCell::new(Weak::new()),
            children: RefCell::new(Vec::new()),
        })
    }

    fn record(&self, value: i32) {
        self.readings.borrow_mut().push(value);
    }

    fn visit(&self) {
        // This tiny fixture does not reach the u32 limit.
        self.visits.set(self.visits.get() + 1);
    }
}

fn main() {
    let device = Device::new("Pi 4B");
    let observer = Rc::clone(&device);
    let weak = Rc::downgrade(&device);
    println!("same allocation={}", Rc::ptr_eq(&device, &observer));
    println!("strong owners={}", Rc::strong_count(&device));
    observer.visit();
    observer.record(46_700);
    device.record(0);
    println!("visits={}", device.visits.get());

    let readings = device.readings.borrow();
    println!("readings={:?}", &*readings);
    println!(
        "write blocked={}",
        device.readings.try_borrow_mut().is_err()
    );
    drop(readings);
    device.record(-500);
    println!("after release={:?}", &*device.readings.borrow());

    drop(observer);
    println!("remaining owners={}", Rc::strong_count(&device));
    drop(device);
    println!("value gone={}", weak.upgrade().is_none());

    let root = Device::new("root");
    let child = Device::new("child");
    *child.parent.borrow_mut() = Rc::downgrade(&root);
    root.children.borrow_mut().push(Rc::clone(&child));
    {
        // Upgrade temporarily owns the parent. Release it before dropping root.
        let parent = child.parent.borrow().upgrade().expect("live fixture root");
        println!("parent={}", parent.label);
    }
    drop(root);
    println!("parent gone={}", child.parent.borrow().upgrade().is_none());
    println!("child survives={}", child.label);
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn clone_shares_mutations_without_cloning_the_payload() {
        let device = Device::new("Pi 4B");
        let alias = Rc::clone(&device);
        assert!(Rc::ptr_eq(&device, &alias));
        alias.record(0);
        alias.record(-500);
        alias.visit();
        assert_eq!(&*device.readings.borrow(), &[0, -500]);
        assert_eq!(device.visits.get(), 1);
    }

    #[test]
    fn empty_and_shared_read_borrows_are_valid() {
        let readings = RefCell::new(Vec::<i32>::new());
        let first = readings.borrow();
        let second = readings.borrow();
        assert!(first.is_empty() && second.is_empty());
        assert!(readings.try_borrow_mut().is_err());
    }

    #[test]
    fn releasing_guards_restores_exclusive_access() {
        let readings = RefCell::new(vec![0]);
        let first = readings.borrow();
        let second = readings.borrow();
        drop(first);
        assert!(readings.try_borrow_mut().is_err());
        drop(second);
        let mut writer = readings.borrow_mut();
        writer.push(46_700);
        assert!(readings.try_borrow().is_err());
        assert!(readings.try_borrow_mut().is_err());
        drop(writer);
        assert_eq!(&*readings.borrow(), &[0, 46_700]);
    }

    #[test]
    fn weak_does_not_keep_the_value_alive() {
        let value = Rc::new(String::from("Pi 4B"));
        let weak = Rc::downgrade(&value);
        let upgraded = weak.upgrade().unwrap();
        drop(value);
        assert!(weak.upgrade().is_some());
        drop(upgraded);
        assert!(weak.upgrade().is_none());
    }

    #[test]
    fn weak_parent_allows_root_destruction_while_child_survives() {
        let root = Device::new("root");
        let child = Device::new("child");
        let root_watch = Rc::downgrade(&root);
        let child_watch = Rc::downgrade(&child);
        *child.parent.borrow_mut() = Rc::downgrade(&root);
        root.children.borrow_mut().push(Rc::clone(&child));
        drop(root);
        assert!(root_watch.upgrade().is_none());
        assert!(child.parent.borrow().upgrade().is_none());
        assert_eq!(child.label, "child");
        drop(child);
        assert!(child_watch.upgrade().is_none());
    }

    #[test]
    fn unique_rc_can_be_mutated_without_a_cell() {
        let mut value = Rc::new(String::from("Pi"));
        Rc::get_mut(&mut value).unwrap().push_str(" 4B");
        let alias = Rc::clone(&value);
        assert!(Rc::get_mut(&mut value).is_none());
        drop(alias);
        let weak = Rc::downgrade(&value);
        assert!(Rc::get_mut(&mut value).is_none());
        drop(weak);
        assert_eq!(Rc::get_mut(&mut value).unwrap(), "Pi 4B");
    }

    #[test]
    fn cell_can_replace_non_copy_owned_values() {
        let label = Cell::new(String::from("Pi"));
        assert_eq!(label.replace(String::from("Pi 4B")), "Pi");
        assert_eq!(label.take(), "Pi 4B");
        assert_eq!(label.into_inner(), "");
    }

    #[test]
    #[should_panic(expected = "already borrowed")]
    fn infallible_borrow_panics_on_conflict() {
        let readings = RefCell::new(vec![0]);
        let _reader = readings.borrow();
        let _writer = readings.borrow_mut();
    }
}
1
2
3
4
cargo check
cargo test
cargo fmt --check
cargo run --quiet

Expected binary output:

same allocation=true
strong owners=2
visits=1
readings=[46700, 0]
write blocked=true
after release=[46700, 0, -500]
remaining owners=1
value gone=true
parent=root
parent gone=true
child survives=child

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>: Some adds a strong owner, None means the value is no longer available. Retaining the upgraded handle can therefore postpone destruction. Weak::new starts without a live referent.

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.

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:

1
2
3
4
5
6
7
use std::rc::Rc;

fn main() {
    let mut label = Rc::new(String::from("Pi"));
    let _alias = Rc::clone(&label);
    label.push_str(" 4B");
}

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

  1. 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.
  2. 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.
  3. 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.
  4. 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.
  5. 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.
  6. 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::get, returning a reference through a temporary guard and dropping the owner while its guard remained live produced E0596, E0599, E0515 and E0505 respectively. Moving the guard release after the write compiled but failed the deliberate runtime-conflict test. No thread synchronization, sensor access or performance was measured.

Next in the roadmap are threads, Send/Sync, Arc and locks. That lesson is planned, not yet available.

Previous: Box, Deref and Drop · Course overview

Donate