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 |