Drop and Resource Management

Resource Safety Patterns

Resource safety patterns combine ownership, Drop, ManuallyDrop, and closure-based scope guards to guarantee that system invariants, external FFI handles, and pooled objects are restored on every execution path without leaking or double-freeing resources.

Pattern 1: Closure-Based Scope Guard (defer Equivalent)

When a function needs ad-hoc cleanup or state restoration on exit without defining a dedicated single-purpose struct, a generic ScopeGuard<F: FnOnce()> wraps a closure inside Option<F> (or ManuallyDrop<F>) and invokes it inside Drop::drop:

struct ScopeGuard<F: FnOnce()> {
    on_drop: Option<F>,
}

impl<F: FnOnce()> ScopeGuard<F> {
    fn new(on_drop: F) -> Self {
        Self {
            on_drop: Some(on_drop),
        }
    }

    /// Cancels the deferred cleanup closure and consumes the guard.
    fn disarm(mut self) {
        self.on_drop.take();
    }
}

impl<F: FnOnce()> Drop for ScopeGuard<F> {
    fn drop(&mut self) {
        if let Some(callback) = self.on_drop.take() {
            callback();
        }
    }
}

fn process_batch(succeed: bool) -> Result<&'static str, &'static str> {
    println!("  [start] marking batch as IN_PROGRESS");
    let guard = ScopeGuard::new(|| {
        println!("  [rollback] resetting batch status to FAILED");
    });

    if !succeed {
        return Err("validation error"); // `guard` runs rollback automatically!
    }

    // Disarm rollback guard once work succeeds:
    guard.disarm();
    println!("  [commit] marking batch as COMPLETED");
    Ok("ok")
}

fn main() {
    println!("Run 1 (failure):");
    let _ = process_batch(false);

    println!("Run 2 (success):");
    let _ = process_batch(true);
}
Run 1 (failure):
  [start] marking batch as IN_PROGRESS
  [rollback] resetting batch status to FAILED
Run 2 (success):
  [start] marking batch as IN_PROGRESS
  [commit] marking batch as COMPLETED

Pattern 2: Object Pool Checkout Guard (ManuallyDrop Return-to-Pool)

In connection pools and buffer pools, checking out an item returns a smart pointer guard that dereferences to the item (Deref / DerefMut). Instead of destroying the item when dropped, the guard's Drop implementation moves the item back into the pool's free list:

use std::cell::RefCell;
use std::mem::ManuallyDrop;
use std::ops::{Deref, DerefMut};

struct BufferPool {
    free_list: RefCell<Vec<Vec<u8>>>,
}

struct PooledBuffer<'a> {
    pool: &'a BufferPool,
    buffer: ManuallyDrop<Vec<u8>>,
}

impl BufferPool {
    fn new() -> Self {
        Self {
            free_list: RefCell::new(Vec::new()),
        }
    }

    fn checkout(&self) -> PooledBuffer<'_> {
        let buf = self
            .free_list
            .borrow_mut()
            .pop()
            .unwrap_or_else(|| Vec::with_capacity(1024));
        PooledBuffer {
            pool: self,
            buffer: ManuallyDrop::new(buf),
        }
    }

    fn available(&self) -> usize {
        self.free_list.borrow().len()
    }
}

impl Deref for PooledBuffer<'_> {
    type Target = Vec<u8>;
    fn deref(&self) -> &Self::Target {
        &self.buffer
    }
}

impl DerefMut for PooledBuffer<'_> {
    fn deref_mut(&mut self) -> &mut Self::Target {
        &mut self.buffer
    }
}

impl Drop for PooledBuffer<'_> {
    fn drop(&mut self) {
        // SAFETY: `self.buffer` is taken once here in `Drop` and never accessed again.
        let mut returned = unsafe { ManuallyDrop::take(&mut self.buffer) };
        returned.clear(); // Clear contents while preserving allocated capacity
        self.pool.free_list.borrow_mut().push(returned);
    }
}

fn main() {
    let pool = BufferPool::new();
    println!("available initially: {}", pool.available());

    {
        let mut buf = pool.checkout();
        buf.extend_from_slice(b"reusable scratch buffer");
        println!("checked out buffer len: {}, cap: {}", buf.len(), buf.capacity());
    } // `buf` is automatically cleared and returned to `pool` here.

    println!("available after scope: {}", pool.available());
    let reused = pool.checkout();
    println!("reused buffer len: {}, cap: {}", reused.len(), reused.capacity());
}
available initially: 0
checked out buffer len: 23, cap: 1024
available after scope: 1
reused buffer len: 0, cap: 1024

Pattern 3: Wrapping Raw C / FFI Handles Immediately

When calling a C API that allocates a raw pointer or integer handle, wrap the raw handle in a single-field struct implementing Drop at the exact point of acquisition so no Rust code can early-return or panic between acquisition and release.

Anti-Pattern Safe Resource Pattern
Holding a raw *mut c_void across multiple fallible ? calls before calling c_free(ptr) Wrapping *mut c_void in struct RawHandle(NonNull<c_void>) whose Drop calls c_free
Calling .unlock() or .cleanup() manually at the end of a function Returning an RAII guard whose Drop releases the lock or restores state
Using std::mem::forget to skip Drop after moving a field Using ManuallyDrop<T> or Option<T>::take() inside the wrapper struct