ExamplesBy LevelBy TopicLearning Paths
409 Fundamental

409: Drop Trait and RAII

Functional Programming

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

  • • Understand how Drop enables deterministic resource cleanup in Rust
  • • Learn the RAII pattern: resource lifetime equals value lifetime
  • • See how FileHandle and LockGuard clean up automatically when they go out of scope
  • • Understand that Drop and Copy are mutually exclusive (copying would duplicate resources)
  • • Learn how std::mem::drop(val) enables early cleanup before scope end
  • Code 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

  • Determinism: Rust's Drop runs at a known point (end of scope); OCaml's finalizers run at GC-determined times, which may be delayed.
  • Error paths: Rust's Drop runs even on panic (unless double-panic); OCaml's Fun.protect ~finally must be explicitly structured around every operation.
  • Copy exclusion: Rust's Drop and Copy are mutually exclusive — you can't copy a value with a destructor; OCaml has no such restriction.
  • Explicit drop: Rust allows 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
        }
    }
    ✓ Tests Rust test suite
    #[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

    AspectOCamlRust
    Cleanup mechanismGC finalizers, with_* patternsDrop trait (deterministic)
    When cleanup runsGC-dependent (non-deterministic)End of scope (deterministic)
    Explicit cleanupclose_file fh, Fun.protectstd::mem::drop(x)
    RAII idiomwith_file name (fun f -> ...)Just use scope: { let f = ... }
    OrderNon-deterministicReverse of creation
    Exception safetyFun.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

  • Rust's Drop is deterministic; OCaml's finalizers are not.
  • Drop runs at end of scope, not when GC decides.

  • RAII is built into Rust's ownership model.
  • No need for with_* wrappers — just use scope.

  • Drop order is reverse of creation.
  • Important for resources with dependencies.

  • Cannot implement both Drop and Copy.
  • Copy types don't have custom destructors.

  • **std::mem::drop(x) forces early cleanup.**
  • Useful when you need cleanup before scope ends.

    Exercises

  • Connection pool: Implement 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.
  • Timed scope: Create 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.
  • Double-drop protection: Implement a 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.
  • Open Source Repos