In C, resource cleanup is maintained by people.
Open a file and you need fclose. Allocate memory and you need free. Take a lock and you need to unlock it. Initialize an object and every error path must destroy it. Once a function has several resources, goto cleanup becomes the common structure.
Rust does not remove cleanup. It binds cleanup responsibility to whoever owns the resource. When a value leaves scope, its cleanup logic runs automatically. This mechanism is Drop.
This article only covers the engineering problem C developers care about first: how resources are not missed across multiple return paths.
cleanup Is Necessary in C
Consider a common C shape:
#include <stdio.h>
#include <stdlib.h>
int copy_file(const char *src, const char *dst)
{
FILE *in = NULL;
FILE *out = NULL;
char *buf = NULL;
int ret = -1;
in = fopen(src, "rb");
if (in == NULL) {
goto cleanup;
}
out = fopen(dst, "wb");
if (out == NULL) {
goto cleanup;
}
buf = malloc(4096);
if (buf == NULL) {
goto cleanup;
}
/* read and write ... */
ret = 0;
cleanup:
free(buf);
if (out != NULL) {
fclose(out);
}
if (in != NULL) {
fclose(in);
}
return ret;
}
This is not bad C. It is a mature pattern: centralize cleanup so every failure branch does not repeat free and fclose.
The cost is that cleanup is still manual:
- adding a resource means updating cleanup
- cleanup order must be checked by hand
- an early
returnmay bypass cleanup - after ownership transfer, the old path must not free
- across functions, ownership rules live in documentation and convention
Rust Drop does not solve cleanup by making it disappear. It puts cleanup responsibility into types and scopes.
Resources Follow Values
The Rust version can keep resources in values:
use std::fs::File;
use std::io::{self, copy};
fn copy_file(src: &str, dst: &str) -> io::Result<()> {
let mut input = File::open(src)?;
let mut output = File::create(dst)?;
copy(&mut input, &mut output)?;
Ok(())
}
There is no explicit close.
File is a value that owns a file descriptor. When input and output leave scope, Rust runs their cleanup logic and closes the underlying files.
Even if a ? returns early, values already created successfully are released as the scope exits.
File::open succeeds -> input owns file
File::create fails -> input leaves scope -> closes
copy fails -> input/output leave scope -> close
success return -> input/output leave scope -> close
This is the same goal as C cleanup, but the rule lives in the type and scope.
Drop Is Like Destruction, but Do Not Call It Manually
Rust types can implement Drop:
struct Session {
id: u32,
}
impl Drop for Session {
fn drop(&mut self) {
println!("close session {}", self.id);
}
}
fn main() {
let session = Session { id: 1 };
println!("work");
}
When main ends, session leaves scope and drop runs automatically.
For a C developer, the useful model is:
Drop = cleanup action when this value leaves scope
Do not treat drop as an ordinary cleanup method to call directly. If you need to release early, use the standard function drop(value), which consumes the value:
let file = File::open("data.bin")?;
drop(file); // file cannot be used after this
After early release, the value has moved into drop, so it cannot be accessed again. That prevents the use-after-free pattern C code can fall into.
When Ownership Moves, Cleanup Responsibility Moves
In C, resource passing is error-prone because ownership is a convention:
void take_buffer(uint8_t *buf); /* will this function free buf? */
In Rust, if a function receives an owned value, cleanup responsibility moves with it:
fn consume_buffer(buf: Vec<u8>) {
println!("len = {}", buf.len());
}
fn main() {
let data = vec![1, 2, 3];
consume_buffer(data);
// data cannot be used here
}
After data is passed to consume_buffer, ownership moves into the function. When the function ends, buf leaves scope and the memory is released.
If the function only borrows the data, receive a slice:
fn inspect_buffer(buf: &[u8]) {
println!("len = {}", buf.len());
}
fn main() {
let data = vec![1, 2, 3];
inspect_buffer(&data);
println!("{}", data.len()); // still usable
}
This is the same distinction from earlier articles: Vec<u8> owns data, &[u8] borrows data.
Scope Controls Release Order
Rust local variables usually drop in reverse creation order:
struct Resource(&'static str);
impl Drop for Resource {
fn drop(&mut self) {
println!("drop {}", self.0);
}
}
fn main() {
let a = Resource("a");
let b = Resource("b");
}
At the end of the scope, b drops before a.
This is similar to manually choosing cleanup order in C, but Rust applies the scope rule automatically. If you need a more precise release point, create a smaller scope:
{
let temp = Resource("temp");
println!("use temp");
} // temp drops here
This is common for temporary files, locks, and temporary buffers. Scope is part of the resource lifetime.
Drop Does Not Replace Explicit Error Handling
One mistake to avoid: Drop is good for releasing resources, but it is not the right place for business errors that must be reported.
For example, closing a file may fail. Rust File closes during drop, but if your program must ensure data is on stable storage, do not rely only on scope exit. Call an explicit function:
use std::io::Write;
fn write_config(mut file: std::fs::File) -> std::io::Result<()> {
file.write_all(b"config")?;
file.sync_all()?;
Ok(())
}
Drop means “try to clean up the resource.” If cleanup result affects program semantics, use a normal function that returns Result.
This is the same distinction C developers already know between releasing a handle and confirming data persistence with fsync or equivalent operations.
What to Keep from This Article
The first mapping is:
malloc/free -> Vec / Box and other owned values + Drop
fopen/fclose -> File + Drop
goto cleanup -> automatic drop on scope exit
early release -> drop(value)
who frees -> whoever owns the value
function only reads -> borrow, such as &[u8] / &str
function takes resource -> receive an owned value, such as Vec<u8>
Drop does not mean resources do not need releasing. It means release timing is tied to the value lifetime.
In C, cleanup rules are maintained in cleanup blocks. In Rust, they enter the type earlier: whoever owns the resource is responsible for release; temporary users borrow.
The next article can focus on ownership itself: who owns, who releases, and why a moved value cannot be used again.