Rust has a reputation for being safe, efficient, and modern — but when you drop down into system-level programming, you’ll still be working with raw system resources. One of the most common and risky is the file descriptor (fd).
So if Rust automatically drops values when they go out of scope, you might ask:
“Why do I need to write something like
FdWrapper
myself?”
Let’s dive deep and explain:
- What a file descriptor is and how it behaves
- Why Rust doesn’t close it automatically
- What really happens in memory when you drop something
- How
FdWrapper
solves this using Rust’s ownership model andDrop
🔧 What Is a File Descriptor (fd)?
In Unix-like operating systems, a file descriptor is just a small integer — usually an i32
. The kernel gives it to you when you open a file, socket, pipe, or other resource.
let fd: i32 = unsafe { libc::open(path, libc::O_RDONLY) }; // fd might be 3
But here's the danger: Rust sees this as just a number.
- It has no idea that
fd = 3
refers to a file that must be closed. - If you forget to call
libc::close(fd)
, the resource stays open — and you’ve created a leak.
🧠 What Does Rust Drop Automatically?
Rust will automatically call drop()
when a value goes out of scope. That’s part of the RAII (Resource Acquisition Is Initialization) model.
Example:
struct MyValue;
impl Drop for MyValue {
fn drop(&mut self) {
println!("MyValue cleaned up");
}
}
fn main() {
let x = MyValue;
} // ✅ `drop()` is called here automatically
But for plain i32
(like a file descriptor), Rust has no Drop
behavior — it just frees the memory of the variable, not the underlying system resource.
❌ This Won’t Work:
let fd = unsafe { libc::open(path, libc::O_RDONLY) };
// No close(fd)! Rust drops the integer, but the file stays open 😱
🧰 Enter: FdWrapper
We can fix this by creating a RAII-safe wrapper that knows:
- How to track whether an
fd
is still open - How to automatically close it when dropped
🔨 Example Implementation
use std::fmt;
use libc::close;
pub struct FdWrapper {
fd: i32,
closed: bool,
}
impl FdWrapper {
pub fn new(fd: i32) -> Self {
Self { fd, closed: false }
}
pub fn close(&mut self) {
if !self.closed {
println!("Closing fd {}", self.fd);
unsafe { close(self.fd); }
self.closed = true;
}
}
}
impl Drop for FdWrapper {
fn drop(&mut self) {
self.close();
}
}
impl fmt::Debug for FdWrapper {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "FdWrapper {{ fd: {}, closed: {} }}", self.fd, self.closed)
}
}
🔍 What Happens Behind the Scenes (Memory-Level View)
Without FdWrapper
:
- You store the
fd
in a local variable: it lives on the stack - When it goes out of scope, Rust deallocates the stack slot, but nothing happens to the file descriptor
- Result: The kernel still considers the file open
With FdWrapper
:
-
fd
is wrapped in a struct on the stack - When the wrapper goes out of scope:
- Rust calls
drop()
on it - Your
drop()
implementation callslibc::close()
- The OS marks the fd as closed, resource is released ✅
- Rust calls
This is safe, automatic, and expressive — the essence of RAII.
📦 System-Level Use Case Examples
🧪 CLI Tool (e.g. cat
or grep
)
let path = CString::new("/etc/passwd").unwrap();
let fd = unsafe { libc::open(path.as_ptr(), O_RDONLY) };
let file = FdWrapper::new(fd);
println!("Using fd: {:?}", file); // Auto-close at the end of scope
🌐 TCP Server Socket (Raw Access)
let stream = TcpStream::connect("127.0.0.1:8000").unwrap();
let fd = stream.as_raw_fd();
let _wrapped_fd = FdWrapper::new(fd); // drop closes the socket automatically
🕹️ Embedded System (Sensor or GPIO)
fn open_device(port: u8) -> FdWrapper {
let fd = (port as i32) + 100; // simulate open()
FdWrapper::new(fd)
}
let sensor = open_device(5);
✅ Summary: Why Rust Needs FdWrapper
🔍 Situation | ❌ Without FdWrapper
|
✅ With FdWrapper
|
---|---|---|
i32 fd value |
No auto-close | Automatically closed with Drop
|
Manual cleanup | Easy to forget close(fd)
|
Always cleaned up safely |
Debugging | Hard to trace leaked fds | Track status with closed flag |
🧠 Final Thoughts 🦀
By wrapping these values in RAII-safe structs like FdWrapper
, you’re building:
- Safer APIs
- More robust system tools
- Cleaner, more idiomatic Rust code
💡 Want to explore more?
- Write a
SocketWrapper
for TCP fds - Add
AsRawFd
trait to make it usable with other APIs - Simulate
dup()
orfcntl()
Follow for more system-level Rust deep dives!