Earlier we used Drop as the first replacement for C cleanup paths: when a value leaves scope, its resource is released automatically.
That already replaces many goto cleanup patterns. But when writing library code that wraps file handles, connections, locks, temporary directories, or FFI handles, “scope exit releases the value” is not enough.
C programmers still need answers to practical questions:
- What is the exact cleanup order?
- How do you release early?
- What if cleanup can fail?
- Which struct field is dropped first?
- Can
Dropmove a field out? - How should an FFI handle be wrapped?
These are the parts of Drop that matter in real engineering code.
Drop Runs Automatically
A Rust type can implement Drop:
struct Session {
id: u32,
}
impl Drop for Session {
fn drop(&mut self) {
println!("close session {}", self.id);
}
}
When a Session value leaves scope, drop runs automatically:
fn main() {
let session = Session { id: 1 };
println!("work");
} // session is dropped here
Do not treat the drop method like a C session_close(&session) function:
let mut session = Session { id: 1 };
// session.drop(); // not allowed
Rust does not allow direct calls to Drop::drop, because that would leave behind a value that still exists but has already been cleaned up. In C, that is exactly where use-after-free and double-free bugs come from.
To release early, use the standard drop(value) function:
let session = Session { id: 1 };
drop(session);
// session cannot be used anymore
drop(session) consumes the value. Early release is tied to ownership, so the value cannot be accessed afterward.
Local Variables Drop in Reverse Order
C cleanup blocks often encode the intended order manually:
cleanup:
close_child(child);
close_parent(parent);
In Rust, local variables in the same scope are usually dropped in reverse creation order:
struct Resource(&'static str);
impl Drop for Resource {
fn drop(&mut self) {
println!("drop {}", self.0);
}
}
fn main() {
let parent = Resource("parent");
let child = Resource("child");
}
The output is:
drop child
drop parent
This often matches resource dependency: a temporary resource created later is cleaned up first.
If a resource must be released earlier, use a smaller scope:
fn main() {
let parent = Resource("parent");
{
let child = Resource("child");
println!("use child");
} // child is dropped here
println!("parent still alive");
}
Scope is not decoration. It is part of the resource lifetime.
Struct Fields Drop in Declaration Order
Local variables drop in reverse order, but struct fields are different. They are dropped in declaration order, not initialization order.
struct Resource(&'static str);
impl Drop for Resource {
fn drop(&mut self) {
println!("drop {}", self.0);
}
}
struct Pair {
first: Resource,
second: Resource,
}
fn main() {
let _pair = Pair {
first: Resource("first"),
second: Resource("second"),
};
}
The fields are dropped in declaration order:
drop first
drop second
This differs from local variables. When designing a resource wrapper, do not guess cleanup order from the order used in the initializer.
A more robust design is to encode the dependency in one field, or perform the truly order-sensitive action explicitly in Drop.
Drop Cannot Return Errors
In C, cleanup functions may return a status:
int session_close(struct session *s);
Rust Drop::drop has a fixed signature:
fn drop(&mut self)
It cannot return Result. This is intentional. Drop can happen through many implicit paths: scope exit, early return, panic unwinding, or container element cleanup. There is no natural caller waiting to handle a drop error.
Cleanup that must report failure should be a normal explicit method:
struct Writer {
file: std::fs::File,
}
impl Writer {
fn finish(mut self) -> std::io::Result<()> {
use std::io::Write;
self.file.flush()?;
self.file.sync_all()?;
Ok(())
}
}
Drop can still release the handle as a fallback. But operations such as flush, commit, and sync_all affect business semantics and should return Result explicitly.
A practical rule:
release the resource -> Drop
need to know whether it succeeded -> explicit method returning Result
Drop Cannot Freely Move Fields Out
drop(&mut self) receives &mut self, not self. That means you cannot directly move an owned field out:
struct Holder {
data: String,
}
impl Drop for Holder {
fn drop(&mut self) {
// let data = self.data; // not allowed
}
}
The reason is simple: after drop returns, Rust still has to clean up the value’s fields according to the normal rules. If a field were moved out, the struct would be partially invalid.
If a field really must be taken during Drop, a common pattern is Option<T>:
struct Holder {
data: Option<String>,
}
impl Drop for Holder {
fn drop(&mut self) {
if let Some(data) = self.data.take() {
println!("drop with data: {data}");
}
}
}
take() moves the value out and leaves None, so the struct remains valid.
Do not overuse this pattern. Most Drop implementations only need to borrow fields to clean up.
Use Drop to Wrap C Handles
When writing FFI or system libraries, the most common use of Drop is wrapping a C handle.
A C API may look like this:
struct device *device_open(const char *path);
void device_close(struct device *dev);
The Rust side can wrap the raw pointer in an owning type:
use std::ptr::NonNull;
struct Device {
raw: NonNull<ffi::device>,
}
impl Drop for Device {
fn drop(&mut self) {
unsafe {
ffi::device_close(self.raw.as_ptr());
}
}
}
The meaning of Device is clear: owning a Device value means owning the underlying C handle; when Device leaves scope, device_close runs automatically.
The constructor converts C failures into a Rust Result:
impl Device {
fn open(path: &std::ffi::CStr) -> Result<Self, OpenError> {
let raw = unsafe { ffi::device_open(path.as_ptr()) };
let raw = NonNull::new(raw).ok_or(OpenError)?;
Ok(Self { raw })
}
}
Now the raw pointer and unsafe are contained inside the wrapper type. Outside code works with a Rust value that releases itself.
mem::forget Intentionally Skips Drop
Rust also has a way to intentionally skip Drop:
let session = Session { id: 1 };
std::mem::forget(session);
forget consumes the value but does not call its Drop. This leaks the resource.
Ordinary application code almost never needs it. It mainly appears in FFI or very low-level ownership transfer: the resource has been handed to C, so Rust must not release it anymore.
If you only want to delay release, change release order, or release early, do not use forget. Use scope or drop(value).
What to Keep
Drop is Rust’s resource finalization mechanism, but it is not a renamed C cleanup function.
The engineering rules matter more:
do not call Drop::drop manually; use drop(value) for early release
local variables usually drop in reverse creation order
struct fields drop in declaration order
Drop cannot return errors; use finish/close/commit methods when results matter
wrap C handles in owning types that implement Drop
do not use mem::forget to avoid ordinary cleanup problems
C cleanup paths rely on convention and review. Rust turns ownership, release timing, and “cannot use after release” into rules the type system can check.
The next topic is match and pattern matching. We have already used enum as a replacement for tagged unions; now we need to see how Rust combines state branching and data unpacking.