RustArena

Step 5 of 7

RAII guards and Drop

RAII ("resource acquisition is initialization", a name from C++) means: tie the cleanup of a resource to the lifetime of a value. When the value goes out of scope, its Drop code runs and the cleanup happens, no matter how the scope ends: normally, by return, by ?, by break, or by a panic unwinding the stack. You've relied on it all along: a File closes itself, a MutexGuard unlocks, a Vec frees its memory.

A guard is a small value you create just for its Drop. It's the Rust answer to try/finally and defer, and it can't be forgotten on an early return.

struct Account {
    balance: i64,
    log: Vec<String>,
}

struct Transaction<'a> {
    account: &'a mut Account,
    start_balance: i64,
    committed: bool,
}

impl<'a> Transaction<'a> {
    fn begin(account: &'a mut Account) -> Self {
        let start_balance = account.balance;
        Transaction { account, start_balance, committed: false }
    }

    fn withdraw(&mut self, amount: i64) -> Result<(), String> {
        if amount > self.account.balance {
            return Err(format!("cannot withdraw {amount}, balance is {}", self.account.balance));
        }
        self.account.balance -= amount;
        Ok(())
    }

    fn commit(mut self) {
        self.committed = true;
        self.account.log.push(String::from("committed"));
    }
}

impl Drop for Transaction<'_> {
    fn drop(&mut self) {
        if !self.committed {
            self.account.balance = self.start_balance;
            self.account.log.push(String::from("rolled back"));
        }
    }
}

fn pay_bills(account: &mut Account) -> Result<(), String> {
    let mut tx = Transaction::begin(account);
    tx.withdraw(700)?;
    tx.withdraw(300)?;
    tx.commit();
    Ok(())
}

fn main() {
    for start in [1500, 900] {
        let mut acc = Account { balance: start, log: Vec::new() };
        let result = pay_bills(&mut acc);
        println!("{result:?} -> balance {} {:?}", acc.balance, acc.log);
    }
}
Ok(()) -> balance 500 ["committed"]
Err("cannot withdraw 300, balance is 200") -> balance 900 ["rolled back"]

Piece by piece:

let _guard vs let _

A subtle trap: let _ = guard(); drops the guard immediately, because _ doesn't bind anything. let _guard = guard(); keeps it alive until the end of the scope.

struct Noisy(&'static str);

impl Drop for Noisy {
    fn drop(&mut self) {
        println!("drop {}", self.0);
    }
}

fn main() {
    let _a = Noisy("a");
    let _ = Noisy("b");
    println!("body");
    {
        let _c = Noisy("c");
        println!("inner");
    }
    drop(Noisy("d"));
    println!("end of main");
}
drop b
body
inner
drop c
drop d
end of main
drop a

drop(x) (the function std::mem::drop) ends a value's life early, for example to release a lock before slow work.

Common mistakes

Your task

Count active work with a guard. Tracker::enter(&self) increments active, updates peak (the highest active ever seen) and returns an ActiveGuard; dropping the guard decrements active. The fields are Cell<u32> so they can change through &self (interior mutability, from the smart pointers module).

Challenge 2

An undo guard for a list. Undo::begin(list) remembers the list's length; push adds items through the guard; keep(self) makes the changes permanent. If the guard is dropped without keep, the list is truncated back to its old length.

Challenge 3

A temporary file that cleans up after itself. TempFile::new(path, contents) writes the file and returns the guard (or the I/O error); path() returns the path; dropping the guard deletes the file (ignore errors while deleting: let _ = std::fs::remove_file(..)).

Previous: Conversions and flexible parameters: From, Into, AsRef Next: Returning iterators