Skip to content

← Smart Pointers and Interior Mutability step 14 of 26

Medium Primitives

RAII: build your own guard

Write a scope guard and drive it from a script.

pub fn trace(events: Vec<String>) -> Vec<String>

A Span is a guard over a shared log. Constructing one appends enter <name>; dropping one appends exit <name>. You write the Drop impl — that is the exercise.

Then interpret the script, keeping open spans on a Vec<Span> used as a stack:

event effect
open <name> push a new Span (which logs enter <name>)
close drop the innermost open span; no-op if none are open
note <text> append note <text>
anything else append ? <the whole line>

At the end of the script, every still-open span must close, innermost first. Return the finished log.

What ownership was for

Up to now ownership has mostly appeared as a list of things you may not do. This is what it buys.

A Span‘s cleanup is not something a caller can forget, skip, or get wrong, because it is not something a caller does — it is a consequence of the value going out of scope. Rust guarantees the destructor runs on every exit path from that scope: falling off the end, an early return, a ? that propagates an error, a break, and an unwinding panic. There is no finally, because there is nothing to put in one.

That is RAII — Resource Acquisition Is Initialisation, a name from C++ that describes the wrong half of the idea. The interesting half is the destructor. You have been using it all along without writing one: File closes, MutexGuard unlocks, Ref decrements the borrow flag, Box frees, Rc decrements the count. Every resource API in Rust is this pattern.

The guard’s lifetime is the critical section. That single sentence explains why { let _g = lock(); … } blocks are idiomatic, why holding a Ref too long panics, and why the next item cares so much about the difference between let _ and let _x.

Design rules for a guard

  • It must own or borrow the resource, so that its destructor can reach it.
  • It must not be Copy. A Copy guard would run its destructor once per copy, which is nonsense — and the compiler enforces this: a type cannot implement both Copy and Drop.
  • It should usually be #[must_use], so that lock(); — creating a guard and dropping it immediately — is a warning rather than a silent bug.
  • Its scope is its meaning. If you want a shorter critical section, introduce a block; if you want a longer one, bind it further out.

Two things the tests will catch

Vec drops its elements front to back. So drop(stack) at the end of the script closes the outermost span first, which is backwards. Verify it yourself if you doubt it — dropping vec![S(1), S(2), S(3)] prints 1, 2, 3. To close innermost-first you must pop:

while let Some(span) = stack.pop() {
    drop(span);
}

Vec::pop returns Option<Span>, and it is the binding and dropping of that Option that runs the destructor. This is worth noticing: pop is where the “exit” line comes from, not clear, not truncate, not the end of the function.

The guard must not steal the log. The starter’s Span::new takes log: Log by value, so the first open moves the log into the span and the second one has nothing to work with:

error[E0382]: use of moved value: `log`
  |             "open" => stack.push(Span::new(rest, log)),
  |                                                  ^^^ value moved here,
  |                                                      in previous iteration of loop

Read that as a design question rather than a syntax problem. Should constructing a guard consume the caller’s handle to the resource? Obviously not — several spans are open at once and they all need it. The guard should take a shared reference and keep its own Rc handle, which is precisely what Rc was invented for.

About the scaffolding

The log is an Rc<RefCell<Vec<String>>>, which is the pattern you met in the last few items: shared ownership of one mutable cell. Several spans hold handles to the same log, and each one appends through borrow_mut() in its destructor. Keep the borrows short — a Drop impl that holds a borrow while doing something else is exactly the re-entrancy hazard from the previous problem, and here it would be much harder to see.

Loading visualization…