Weak<T> is a non-owning companion to Rc<T>/Arc<T>: holding a Weak
reference to a value doesn't keep it alive and doesn't count toward its
strong-reference count, only its separate weak count. To actually use the
value, you .upgrade() a Weak<T> into an Option<Rc<T>> (or
Option<Arc<T>>) — Some if the value is still alive, None if every
strong owner has already dropped it.
This exists specifically to break reference cycles. Two Rc-owned
structures that hold strong references to each other (a parent pointing
at its child, and that child pointing back at its parent) would otherwise
never reach a reference count of zero — each keeps the other alive
forever, a memory leak reference counting can't detect or prevent on its
own. Making one direction of such a cycle a Weak reference instead (very
commonly: parents hold strong references to their children — a parent
owns its children — while each child holds only a Weak reference back
up to its parent) breaks the cycle
while still letting either side reach the other when needed.
Basic usage example
usestd::rc::{Rc,Weak};letstrong=Rc::new(5);letweak:Weak<i32>=Rc::downgrade(&strong);// <- doesn't count toward strong_countmatchweak.upgrade(){// <- must upgrade to access the value; can failSome(v)=>println!("{v}"),None=>println!("value already dropped"),}
Restriction: a Weak<T> cannot be dereferenced directly — it must be
upgraded via .upgrade() first, which returns None if every strong
owner has already dropped the value.
Best practices & deeper information
Scenario
Shared ownership
A tree where each child needs to reference its parent would create a
reference cycle if that back-reference were a strong Rc — the parent
keeps the child alive, the child keeps the parent alive, and neither is
ever freed.
usestd::cell::RefCell;usestd::rc::{Rc,Weak};structNode{name:String,parent:RefCell<Weak<Node>>,// <- back-reference: Weak, doesn't keep the parent alivechildren:RefCell<Vec<Rc<Node>>>,// forward reference: Rc, parent legitimately owns its children}letparent=Rc::new(Node{name:"root".into(),parent:RefCell::new(Weak::new()),children:RefCell::new(Vec::new()),});letchild=Rc::new(Node{name:"leaf".into(),parent:RefCell::new(Rc::downgrade(&parent)),// <- doesn't increment parent's strong countchildren:RefCell::new(Vec::new()),});parent.children.borrow_mut().push(Rc::clone(&child));letparent_ref=child.parent.borrow().upgrade();// <- must upgrade; None if parent were already droppedifletSome(p)=parent_ref{println!("{}'s parent is {}",child.name,p.name);}
Why this way: making only one direction of the cycle Weak
(conventionally: parents hold strong references down to their children,
the pointer back up to the parent is Weak) is what lets parent's strong count reach
zero and free the tree once it's no longer reachable from outside — the
Rust Book
walks through exactly this parent/child shape as the standard fix for
reference cycles; see
Shared ownership (Rc & Arc) for when plain
Rc is enough.
Explanation
Embedded support: Partial
Weak<T> is built directly on top of Rc<T>/Arc<T> — it lives in
alloc, and needs the same extern crate alloc; plus a configured
#[global_allocator] that its strong-reference counterparts need. See
Shared ownership (Rc & Arc) for that
caveat in full, including the honest point that Rc/Arc are already
uncommon in embedded code, since embedded rarely has the OS-thread-based
multi-owner use case they were designed for, and interrupt-boundary
sharing is usually better served by a 'staticcritical_section::Mutex<RefCell<T>> instead. Weak inherits that
unpopularity and then some: it only exists to solve a problem —
reference cycles between Rc/Arc-owned values — that only arises once
you're already using Rc/Arc for shared ownership in the first place.
If a project isn't reaching for Rc much, it has correspondingly little
need for Weak either.
What embedded code needs instead is the same underlying idea Weak
provides — a non-owning reference that might not resolve — without the
heap and the refcounting machinery. The closest no-heap analogue is often
just a plain Option<&'a T>: instead of a runtime check ("has every
strong owner already dropped this?"), validity is tracked by the
borrow checker at compile time through the lifetime 'a, so there's
nothing to .upgrade() at runtime — the reference either compiles, or it
doesn't live long enough and the compiler rejects it up front. For
longer-lived, more dynamic non-owning references (a task scheduler
handing out handles to entries that can later be removed), the usual
embedded pattern is an index into a fixed-size slab/arena — often paired
with a generation counter so a stale index into a reused slot is detected
as invalid — rather than a pointer at all: "might not resolve" becomes "the
generation stored in the handle doesn't match the slot's current
generation," checked with a plain comparison instead of reference-count
bookkeeping.
Basic usage example
structSensor{reading:f32}fnlast_active<'a>(sensors:&'a[Sensor],last_index:Option<usize>)->Option<&'aSensor>{// <- Option<&'a T>: "might not resolve" tracked by the borrow checker, not a runtime upgrade()last_index.and_then(|i|sensors.get(i))}
Best practices & deeper information
Scenario
Shared ownership
A tree of sensor nodes where each child needs to reference its parent
would use Weak<Node> on a hosted target to avoid a reference cycle —
without an allocator, the no-heap analogue is a fixed-size arena plus
plain indices, with a generation counter standing in for Weak's
"already dropped" check.
constMAX_NODES:usize=16;#[derive(Clone, Copy)]structNodeHandle{index:u8,generation:u8}// <- the no-heap analogue of a Weak<Node>structNodeSlot{name:&'staticstr,generation:u8,occupied:bool,}structArena{slots:[NodeSlot;MAX_NODES],}implArena{fnresolve(&self,handle:NodeHandle)->Option<&NodeSlot>{letslot=&self.slots[handle.indexasusize];// <- the "might not resolve" check: stale handle into a reused/freed slot returns None, like upgrade()ifslot.occupied&&slot.generation==handle.generation{Some(slot)}else{None}}}
Why this way: a fixed-size arena gives every node a compile-time-known
home with no heap allocation at all, and a generation counter recreates
Weak::upgrade's "tell me if this has already gone away" check without
reference counting — the same shape the
Rust Book's Weak chapter
solves with Rc/Weak, reached here with plain arithmetic instead,
which is the standard no-heap substitute wherever a project can't or
won't configure a global allocator.
Scenario
Designing a public API
A small task registry that hands out handles to registered tasks, some of
which can later be cancelled and removed, needs an API for "a reference
that might no longer be valid" — an index-based handle communicates that
contract without requiring alloc at all.
pubstructTaskHandle{index:u8,generation:u8}// <- public API surface: looks like a lightweight Weak<Task>pubstructTaskRegistry{generations:[u8;8],active:[bool;8],}implTaskRegistry{pubfnis_valid(&self,handle:&TaskHandle)->bool{// <- callers check validity explicitly, same contract as Weak::upgrade().is_some()self.active[handle.indexasusize]&&self.generations[handle.indexasusize]==handle.generation}}
Why this way: documenting the handle as "might not resolve, check
before use" up front — mirroring Weak<T>'s contract — sets the right
caller expectations without needing alloc, Rc, or a refcount at all;
the API Guidelines
favor an API whose type signals its own fallibility over one that panics
on a stale handle and documents "don't do that" as the only safeguard.