Operating-System Interfaces
User-space programs cannot access disks, network cards, or physical memory directly. Instead, they request privileged services from the operating system kernel through system calls (syscalls) and OS handles / file descriptors. Rust's standard library wraps these low-level kernel interfaces while exposing type-safe, RAII-managed descriptor types in std::os::fd (Unix/WASI) and std::os::windows::io (Windows).
User Space (Ring 3 / EL0):
Rust Std / App (`File::read`, `TcpStream::connect`)
|
v `std::os::fd::BorrowedFd` / `OwnedFd`
C Runtime / Syscall Wrapper (`libc` / `rustix` / `windows-sys`)
========|==========================================================
Kernel Space (Ring 0 / EL1): `syscall` / `svc #0` trap instruction
VFS / Network Stack / Scheduler / Page Tables
Type-Safe OS Descriptor Ownership (OwnedFd and BorrowedFd)
Historically, Unix code passed raw integer file descriptors (RawFd = i32) and Windows code passed raw pointers (RawHandle = *mut c_void). Passing a raw i32 descriptor has two serious hazards:
- Ambiguous ownership: Does the callee close the
i32descriptor, or does the caller still own it? Closing a descriptor twice (double close) can accidentally close an unrelated socket or file opened by another thread that reused the same integer FD! - Use-after-close: Nothing stops a raw
i32from being used after its underlyingFilewas dropped.
Since Rust 1.63, the standard library provides I/O Safety types that apply Rust's ownership and lifetime rules directly to OS kernel descriptors:
Type (Unix std::os::fd / Windows std::os::windows::io) |
Equivalent Rust Memory Type | Semantics |
|---|---|---|
OwnedFd / OwnedHandle / OwnedSocket |
Box<T> |
Exclusively owns the OS descriptor and automatically closes it (close / CloseHandle) on Drop! |
BorrowedFd<'fd> / BorrowedHandle<'a> |
&'fd T |
Borrows an open OS descriptor for lifetime 'fd; cannot outlive the owner and never closes the descriptor! |
RawFd (i32) / RawHandle (*mut c_void) |
*mut T |
Legacy raw integer/pointer without lifetime or drop guarantees. |
use std::env;
use std::fs::{self, File};
use std::io::{self, Write};
fn main() -> io::Result<()> {
let temp_path = env::temp_dir().join("rust_ch59_io_safety.txt");
let file = File::create(&temp_path)?;
#[cfg(unix)]
{
use std::os::fd::{AsFd, AsRawFd, BorrowedFd, OwnedFd};
// 1. Borrow the descriptor with lifetime tied to `file` (`BorrowedFd<'_>`)
let borrowed: BorrowedFd<'_> = file.as_fd();
println!("Borrowed valid Unix FD >= 0: {}", borrowed.as_raw_fd() >= 0);
// 2. Transfer ownership from `File` into `OwnedFd` and back into `File`
let owned_fd: OwnedFd = file.into();
let mut restored_file = File::from(owned_fd);
restored_file.write_all(b"I/O safety verified\n")?;
}
#[cfg(windows)]
{
use std::os::windows::io::{AsHandle, OwnedHandle};
let _borrowed = file.as_handle();
let owned_handle: OwnedHandle = file.into();
let mut restored_file = File::from(owned_handle);
restored_file.write_all(b"I/O safety verified\n")?;
}
let content = fs::read_to_string(&temp_path)?;
println!("Read back: {}", content.trim());
fs::remove_file(&temp_path)?;
Ok(())
}
Borrowed valid Unix FD >= 0: true
Read back: I/O safety verified
Capturing the Thread-Local OS Error Code (last_os_error)
When invoking a direct OS system call via libc or windows-sys that returns -1 or 0 on failure, immediately call std::io::Error::last_os_error() (which reads thread-local errno on Unix or GetLastError() on Windows) or std::io::Error::from_raw_os_error(code) to construct a rich std::io::Error with its corresponding ErrorKind:
use std::io::{Error, ErrorKind};
fn main() {
// POSIX `ENOENT` (2) maps to `ErrorKind::NotFound`
let err = Error::from_raw_os_error(2);
assert_eq!(err.kind(), ErrorKind::NotFound);
println!("Raw OS error 2 mapped to kind: {:?} ({})", err.kind(), err);
}
Raw OS error 2 mapped to kind: NotFound (No such file or directory (os error 2))