Skip to content

← Smart Pointers and Interior Mutability step 14 of 26

Medium Primitives

RAII: build your own guard

Implement a guard that records when a span opens and closes, then use it in trace(events: Vec<String>) -> Vec<String>.

Constructing a Span appends enter <name> to a shared log. Implement Drop so destroying that Span appends exit <name>. Keep open guards in a vector used as a stack.

Script rules

Split each event at its first ASCII space. The part before it is the command; the remainder is the argument. If there is no space, the whole event is the command and the argument is empty. Do not trim either part.

Command Effect
open Create and push a Span whose name is the argument
close Remove and drop the most recently opened Span; ignore the argument
note Append note <argument>
Anything else Append ? <whole original event>

Names and notes may be empty or contain spaces. For example, "open" logs "enter ", while "open a" logs "enter a". Commands are case-sensitive, and a tab does not replace the command's space separator.

Closing an empty stack does nothing. After the last event, close all remaining spans from innermost to outermost, then return the completed log.

For ["open outer", "open inner", "note work", "close"], return:

["enter outer", "enter inner", "note work", "exit inner", "exit outer"]

Empty input produces an empty log.

What a guard does

RAII means Resource Acquisition Is Initialization. A guard ties cleanup to an owned value. Its constructor establishes a state, and its destructor releases or completes that state when the value is dropped.

A local guard is normally dropped when its scope exits, including on an early return, error propagation with ?, or an unwinding panic. It can also be dropped earlier with std::mem::drop(guard), or moved elsewhere so its owner keeps it alive longer.

Destruction is not guaranteed on every possible exit. For example, mem::forget, reference cycles, process termination and aborting panics can prevent it. RAII makes ordinary cleanup automatic; it is not a guarantee against every leak or interrupted process.

File handles, lock guards and RefCell borrow guards use this pattern for different resources. Here the resource is the open span represented by a log entry. A guard type implementing Drop cannot also implement Copy. #[must_use] can help catch an accidentally discarded guard, but it is not a substitute for managing its ownership.

Share the log handle

The log is Rc<RefCell<Vec<String>>>. Rc lets multiple guards share ownership of the same log; RefCell allows checked mutable access when appending entries.

The starter passes log by value to Span::new, moving the caller's handle. Later uses produce E0382. Change the constructor to accept &Log and store Rc::clone(log), updating its caller accordingly. Cloning an Rc clones the ownership handle, not the vector of log entries.

Taking a Log by value is not inherently wrong: a caller could instead supply a cloned handle. The borrowed-constructor design used here keeps that sharing decision inside Span::new.

Keep RefCell borrows short. Holding a shared borrow is not itself an error, but dropping a Span while that borrow remains active would make its borrow_mut() panic. A destructor that panics during an existing unwind can abort the process.

Pop before dropping

Vec::pop removes and returns the last value. It does not itself destroy a returned Span that you keep in a variable.

In stack.pop();, the returned temporary Option is discarded at the end of the statement. If it contains a Span, that Span is dropped then. In the final cleanup loop, explicitly drop each popped value:

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

This fragment belongs after the event loop. Popping determines the required innermost-first order. Vec does not guarantee an element destruction order, so drop(stack) must not be used to establish this ordering.

Do not call Span::drop directly. Implement fn drop(&mut self) through the Drop trait and use ordinary scope exit or std::mem::drop to destroy an owned guard.

The required final ordering applies to trace's normal completion. It does not promise that an unwinding vector will drop its elements in reverse order. Keep the distinction between automatic cleanup and ordered cleanup clear when adapting this pattern.

The submission must compile, match the log output and pass Clippy with warnings denied.