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 and Drop

🔧 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:
    1. Rust calls drop() on it
    2. Your drop() implementation calls libc::close()
    3. The OS marks the fd as closed, resource is released ✅

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() or fcntl()

Follow for more system-level Rust deep dives!