409: Drop Trait and RAII
Tutorial Video
Text description (accessibility)
This video demonstrates the "409: Drop Trait and RAII" functional Rust example. Difficulty level: Fundamental. Key concepts covered: Functional Programming. Resource management is one of the oldest problems in systems programming. Key difference from OCaml: 1. **Determinism**: Rust's `Drop` runs at a known point (end of scope); OCaml's finalizers run at GC
Tutorial
The Problem
Resource management is one of the oldest problems in systems programming. C requires manual fclose(f), free(ptr), release_lock() — calls that are easily forgotten, especially in error paths. C++ introduced RAII (Resource Acquisition Is Initialization): resources are tied to stack lifetimes, and destructors run automatically when objects go out of scope. Rust adopts this with the Drop trait: implement fn drop(&mut self) and it runs deterministically when the value is destroyed — at end of scope, when moved into a function that consumes it, or when explicitly dropped with drop(val).
Drop powers MutexGuard (unlocks on drop), File (closes on drop), Vec/String (frees heap on drop), database transactions, and any pattern needing guaranteed cleanup.
🎯 Learning Outcomes
Drop enables deterministic resource cleanup in RustFileHandle and LockGuard clean up automatically when they go out of scopeDrop and Copy are mutually exclusive (copying would duplicate resources)std::mem::drop(val) enables early cleanup before scope endCode Example
struct FileHandle {
name: String,
is_open: bool,
}
impl FileHandle {
fn open(name: &str) -> Self {
FileHandle { name: name.to_string(), is_open: true }
}
}
impl Drop for FileHandle {
fn drop(&mut self) {
if self.is_open {
// Close file
self.is_open = false;
}
}
}
fn main() {
{
let f = FileHandle::open("data.txt");
println!("Using: {}", f.name);
} // Drop called automatically here
}Key Differences
Drop runs at a known point (end of scope); OCaml's finalizers run at GC-determined times, which may be delayed.Drop runs even on panic (unless double-panic); OCaml's Fun.protect ~finally must be explicitly structured around every operation.Drop and Copy are mutually exclusive — you can't copy a value with a destructor; OCaml has no such restriction.std::mem::drop(val) for early cleanup; OCaml has no early finalization (only Gc.compact which is unpredictable).OCaml Approach
OCaml's GC manages memory automatically but does not provide deterministic finalization. Gc.finalise attaches a finalizer that runs at some point after the value becomes unreachable, but timing is not guaranteed. The Fun.protect ~finally function provides RAII-like cleanup: Fun.protect ~finally:(fun () -> close f) (fun () -> use f). OCaml's standard idiom for resource management is explicit with_* functions rather than RAII.
Full Source
#![allow(clippy::all)]
//! Drop Trait and RAII
//!
//! Automatic resource cleanup when values go out of scope.
use std::cell::Cell;
/// A simulated file handle demonstrating Drop.
#[derive(Debug)]
pub struct FileHandle {
name: String,
is_open: Cell<bool>,
}
impl FileHandle {
/// Opens a file handle.
pub fn open(name: &str) -> Self {
FileHandle {
name: name.to_string(),
is_open: Cell::new(true),
}
}
/// Returns the file name.
pub fn name(&self) -> &str {
&self.name
}
/// Returns whether the file is open.
pub fn is_open(&self) -> bool {
self.is_open.get()
}
/// Simulates reading from the file.
pub fn read(&self) -> Option<String> {
if self.is_open.get() {
Some(format!("Contents of {}", self.name))
} else {
None
}
}
}
impl Drop for FileHandle {
fn drop(&mut self) {
if self.is_open.get() {
self.is_open.set(false);
// In real code: close file descriptor, flush buffers, etc.
}
}
}
/// A lock guard demonstrating RAII pattern.
pub struct LockGuard<'a> {
resource_name: &'a str,
lock_id: u32,
released: Cell<bool>,
}
impl<'a> LockGuard<'a> {
/// Acquires a lock on a resource.
pub fn acquire(resource: &'a str) -> Self {
LockGuard {
resource_name: resource,
lock_id: 42, // Simulated
released: Cell::new(false),
}
}
/// Returns the resource name.
pub fn resource(&self) -> &str {
self.resource_name
}
/// Returns whether the lock is still held.
pub fn is_held(&self) -> bool {
!self.released.get()
}
}
impl Drop for LockGuard<'_> {
fn drop(&mut self) {
if !self.released.get() {
self.released.set(true);
// In real code: release mutex, semaphore, etc.
}
}
}
/// A transaction guard that commits on success or rolls back on drop.
pub struct Transaction {
name: String,
committed: Cell<bool>,
}
impl Transaction {
/// Begins a new transaction.
pub fn begin(name: &str) -> Self {
Transaction {
name: name.to_string(),
committed: Cell::new(false),
}
}
/// Commits the transaction.
pub fn commit(self) {
self.committed.set(true);
// Don't call drop's rollback
}
/// Returns the transaction name.
pub fn name(&self) -> &str {
&self.name
}
/// Checks if committed.
pub fn is_committed(&self) -> bool {
self.committed.get()
}
}
impl Drop for Transaction {
fn drop(&mut self) {
if !self.committed.get() {
// Rollback
}
}
}
/// Counter that tracks active instances.
pub struct TrackedResource {
id: u32,
counter: *mut u32,
}
impl TrackedResource {
/// Creates a new tracked resource with external counter.
///
/// # Safety
/// The counter pointer must remain valid for the lifetime of the resource.
pub fn new(id: u32, counter: &mut u32) -> Self {
*counter += 1;
TrackedResource {
id,
counter: counter as *mut u32,
}
}
pub fn id(&self) -> u32 {
self.id
}
}
impl Drop for TrackedResource {
fn drop(&mut self) {
unsafe {
*self.counter -= 1;
}
}
}
/// Demonstrates drop order (reverse of creation).
pub fn demonstrate_drop_order() -> Vec<String> {
struct OrderTracker {
name: String,
log: *mut Vec<String>,
}
impl Drop for OrderTracker {
fn drop(&mut self) {
unsafe {
(*self.log).push(format!("Dropped: {}", self.name));
}
}
}
let mut log = Vec::new();
{
let _a = OrderTracker {
name: "A".to_string(),
log: &mut log,
};
let _b = OrderTracker {
name: "B".to_string(),
log: &mut log,
};
let _c = OrderTracker {
name: "C".to_string(),
log: &mut log,
};
// Drops in reverse order: C, B, A
}
log
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_file_handle_open_close() {
let handle = FileHandle::open("test.txt");
assert!(handle.is_open());
assert_eq!(handle.name(), "test.txt");
assert!(handle.read().is_some());
}
#[test]
fn test_file_handle_drop() {
let handle = FileHandle::open("test.txt");
assert!(handle.is_open());
drop(handle);
// handle is no longer accessible
}
#[test]
fn test_lock_guard_raii() {
let guard = LockGuard::acquire("database");
assert!(guard.is_held());
assert_eq!(guard.resource(), "database");
drop(guard);
// Lock automatically released
}
#[test]
fn test_transaction_commit() {
let tx = Transaction::begin("update_user");
assert!(!tx.is_committed());
tx.commit();
// Committed, no rollback in drop
}
#[test]
fn test_transaction_rollback_on_drop() {
let tx = Transaction::begin("update_user");
assert!(!tx.is_committed());
drop(tx);
// Rollback happened in drop
}
#[test]
fn test_tracked_resource_counter() {
let mut counter = 0u32;
{
let _r1 = TrackedResource::new(1, &mut counter);
assert_eq!(counter, 1);
{
let _r2 = TrackedResource::new(2, &mut counter);
assert_eq!(counter, 2);
}
assert_eq!(counter, 1); // r2 dropped
}
assert_eq!(counter, 0); // r1 dropped
}
#[test]
fn test_drop_order() {
let log = demonstrate_drop_order();
assert_eq!(log, vec!["Dropped: C", "Dropped: B", "Dropped: A"]);
}
#[test]
fn test_explicit_drop() {
let handle = FileHandle::open("explicit.txt");
let name = handle.name().to_string();
std::mem::drop(handle);
assert_eq!(name, "explicit.txt");
// Cannot use handle after drop
}
}#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_file_handle_open_close() {
let handle = FileHandle::open("test.txt");
assert!(handle.is_open());
assert_eq!(handle.name(), "test.txt");
assert!(handle.read().is_some());
}
#[test]
fn test_file_handle_drop() {
let handle = FileHandle::open("test.txt");
assert!(handle.is_open());
drop(handle);
// handle is no longer accessible
}
#[test]
fn test_lock_guard_raii() {
let guard = LockGuard::acquire("database");
assert!(guard.is_held());
assert_eq!(guard.resource(), "database");
drop(guard);
// Lock automatically released
}
#[test]
fn test_transaction_commit() {
let tx = Transaction::begin("update_user");
assert!(!tx.is_committed());
tx.commit();
// Committed, no rollback in drop
}
#[test]
fn test_transaction_rollback_on_drop() {
let tx = Transaction::begin("update_user");
assert!(!tx.is_committed());
drop(tx);
// Rollback happened in drop
}
#[test]
fn test_tracked_resource_counter() {
let mut counter = 0u32;
{
let _r1 = TrackedResource::new(1, &mut counter);
assert_eq!(counter, 1);
{
let _r2 = TrackedResource::new(2, &mut counter);
assert_eq!(counter, 2);
}
assert_eq!(counter, 1); // r2 dropped
}
assert_eq!(counter, 0); // r1 dropped
}
#[test]
fn test_drop_order() {
let log = demonstrate_drop_order();
assert_eq!(log, vec!["Dropped: C", "Dropped: B", "Dropped: A"]);
}
#[test]
fn test_explicit_drop() {
let handle = FileHandle::open("explicit.txt");
let name = handle.name().to_string();
std::mem::drop(handle);
assert_eq!(name, "explicit.txt");
// Cannot use handle after drop
}
}
Deep Comparison
OCaml vs Rust: Drop Trait and RAII
Side-by-Side Code
OCaml — Explicit cleanup or with_* pattern
type file_handle = { name: string; mutable closed: bool }
let open_file name =
{ name; closed = false }
let close_file fh =
if not fh.closed then fh.closed <- true
(* RAII via with_file *)
let with_file name f =
let fh = open_file name in
Fun.protect ~finally:(fun () -> close_file fh) (fun () -> f fh)
let () =
with_file "data.txt" (fun f ->
Printf.printf "Using: %s\n" f.name
)
(* Automatically closed, even on exception *)
Rust — Drop trait (automatic RAII)
struct FileHandle {
name: String,
is_open: bool,
}
impl FileHandle {
fn open(name: &str) -> Self {
FileHandle { name: name.to_string(), is_open: true }
}
}
impl Drop for FileHandle {
fn drop(&mut self) {
if self.is_open {
// Close file
self.is_open = false;
}
}
}
fn main() {
{
let f = FileHandle::open("data.txt");
println!("Using: {}", f.name);
} // Drop called automatically here
}
Comparison Table
| Aspect | OCaml | Rust |
|---|---|---|
| Cleanup mechanism | GC finalizers, with_* patterns | Drop trait (deterministic) |
| When cleanup runs | GC-dependent (non-deterministic) | End of scope (deterministic) |
| Explicit cleanup | close_file fh, Fun.protect | std::mem::drop(x) |
| RAII idiom | with_file name (fun f -> ...) | Just use scope: { let f = ... } |
| Order | Non-deterministic | Reverse of creation |
| Exception safety | Fun.protect ~finally:... | Automatic via Drop |
Drop Order
Rust drops in reverse order of creation:
{
let a = Resource::new("A"); // Created first
let b = Resource::new("B");
let c = Resource::new("C"); // Created last
}
// Drop order: C, B, A (reverse)
RAII Patterns
Lock Guard
struct MutexGuard<'a, T> { /* ... */ }
impl<T> Drop for MutexGuard<'_, T> {
fn drop(&mut self) {
// Release lock
}
}
fn use_data(mutex: &Mutex<Data>) {
let guard = mutex.lock(); // Lock acquired
// Use guard...
} // Lock released here via Drop
Transaction (Commit or Rollback)
struct Transaction { committed: bool }
impl Drop for Transaction {
fn drop(&mut self) {
if !self.committed {
// Rollback
}
}
}
fn transfer(tx: Transaction) -> Result<(), Error> {
// Do work...
tx.commit(); // Marks as committed
Ok(())
} // If commit not called, rollback in drop
Explicit Drop
let f = FileHandle::open("data.txt");
// ... use f ...
std::mem::drop(f); // Drop now, before scope ends
// f is no longer usable
5 Takeaways
Drop runs at end of scope, not when GC decides.
No need for with_* wrappers — just use scope.
Important for resources with dependencies.
Copy types don't have custom destructors.
std::mem::drop(x) forces early cleanup.**Useful when you need cleanup before scope ends.
Exercises
Connection (simulating a DB connection) and ConnectionGuard that wraps a &'a mut Pool and a Connection. On drop, return the connection to the pool. Show that the connection is always returned even when the code between acquisition and drop panics.TimedScope { name: String, start: Instant } implementing Drop that prints elapsed time when the scope ends. Use it to measure how long a code block takes without explicit timing calls.SafeHandle that tracks whether it has been dropped (via Arc<AtomicBool>) and panics if the Drop implementation is somehow called twice. Write tests verifying single-drop behavior.