🦀 Functional Rust
🎬 Error Handling in Rust Option, Result, the ? operator, and combinators.
📝 Text version (for readers / accessibility)

• Option represents a value that may or may not exist — Some(value) or None

• Result represents success (Ok) or failure (Err) — no exceptions needed

• The ? operator propagates errors up the call stack concisely

• Combinators like .map(), .and_then(), .unwrap_or() chain fallible operations

• The compiler forces you to handle every error case — no silent failures

715: FFI Error Codes — Converting C Errors to Rust Result

Difficulty: 4 Level: Expert Wrap C integer error codes in a typed `Result<T, E>` at the FFI boundary — never let raw error integers bleed into Rust application code.

The Problem This Solves

C libraries signal errors through integer return codes: `0` for success, negative values for specific errors, or positive `errno` values. This convention is pervasive — POSIX, OpenSSL, SQLite, libusb, and virtually every C library you'll ever bind. The codes are meaningful only if you know the library; raw integers in Rust code are opaque and unverifiable. The idiomatic Rust pattern is to convert at the boundary. You write a thin `unsafe` wrapper that calls the C function, checks the return code, and maps it to `Result<T, MyError>`. From that point on, the rest of your Rust code uses `?` propagation, pattern matching, and typed error handling — no integer comparisons, no magic number `if rc == -22`. The `#[repr(i32)]` enum technique lets you define error codes as named variants with the exact numeric values the C library uses. This makes the mapping zero-cost (it's a cast, not a table lookup) and makes the code self-documenting — `PosixError::InvalidArg` is vastly clearer than `-22`.

The Intuition

Think of the FFI boundary as a translation booth. C speaks "integers with magic meanings." Rust speaks "typed Results with pattern matching." Your wrapper is the translator. It sits at the boundary, takes the integer C hands it, and returns either `Ok(value)` or `Err(TypedError::SpecificProblem)`. Every caller on the Rust side gets clean types; the translation happens exactly once. The rule: one `unsafe` block per C call, immediately wrapped in the error conversion. The moment you're in safe Rust, you have a `Result`.

How It Works in Rust

use std::os::raw::c_int;

#[repr(i32)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PosixError {
 NotFound     = -2,   // ENOENT
 AccessDenied = -13,  // EACCES
 AlreadyExists = -17, // EEXIST
 InvalidArg   = -22,  // EINVAL
 Unknown      = i32::MIN,
}

impl PosixError {
 pub fn from_raw(code: c_int) -> Self {
     match code {
         -2  => Self::NotFound,
         -13 => Self::AccessDenied,
         -17 => Self::AlreadyExists,
         -22 => Self::InvalidArg,
         _   => Self::Unknown,
     }
 }
}

/// Wraps a C call that returns 0 on success, negative on error.
fn check(rc: c_int) -> Result<(), PosixError> {
 if rc == 0 { Ok(()) } else { Err(PosixError::from_raw(rc)) }
}

/// Safe Rust API hiding the C error-code convention.
pub fn safe_open(path: &str) -> Result<i32, PosixError> {
 let rc = unsafe { c_open(path.as_ptr(), path.len()) };
 check(rc).map(|_| rc)
}
For `errno`-style errors (the C function sets the global `errno` on failure), use `std::io::Error::last_os_error()` and map it to your error type.

What This Unlocks

Key Differences

ConceptOCamlRust
C error handling`Unix.Unix_error` exception`Result<T, E>` returned
errno capture`Unix.errno``std::io::Error::last_os_error()`
Error codes`Unix.error` variantsCustom `#[repr(i32)]` enum
Safe wrapper boundaryModule + exception`fn safe_foo() -> Result<T, MyError>`
Zero = successChecked by Unix moduleChecked and converted in wrapper
Propagation`try`/`with``?` operator
// 715. C-style error code patterns in Rust FFI
//
// Demonstrates wrapping C integer-return-code conventions into
// idiomatic Result<T, E> at the FFI boundary.

use std::fmt;
use std::os::raw::{c_char, c_int};

// ── Error type ───────────────────────────────────────────────────────────────

/// Maps POSIX errno values to a typed Rust enum.
/// `#[repr(i32)]` ensures the discriminant fits a C int.
#[repr(i32)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PosixError {
    NotFound    = -2,   // ENOENT
    AccessDenied = -13, // EACCES
    AlreadyExists = -17, // EEXIST
    InvalidArg  = -22,  // EINVAL
    Unknown     = i32::MIN,
}

impl PosixError {
    /// Convert a raw negative C return value to a typed error.
    pub fn from_raw(code: c_int) -> Self {
        match code {
            -2  => Self::NotFound,
            -13 => Self::AccessDenied,
            -17 => Self::AlreadyExists,
            -22 => Self::InvalidArg,
            _   => Self::Unknown,
        }
    }
}

impl fmt::Display for PosixError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        let msg = match self {
            Self::NotFound     => "No such file or directory (ENOENT)",
            Self::AccessDenied => "Permission denied (EACCES)",
            Self::AlreadyExists => "File exists (EEXIST)",
            Self::InvalidArg   => "Invalid argument (EINVAL)",
            Self::Unknown      => "Unknown error",
        };
        f.write_str(msg)
    }
}

impl std::error::Error for PosixError {}

// ── Mock C library ────────────────────────────────────────────────────────────
// In real code this block would be:
//   extern "C" { fn open(path: *const c_char, flags: c_int) -> c_int; }
//
// We stub it here so the example compiles without a C toolchain.

mod mock_libc {
    use std::os::raw::c_int;

    /// Simulates a C function: returns >=0 on success, negative on error.
    /// Real signature: `unsafe extern "C" fn open(*const c_char, c_int) -> c_int`
    pub unsafe fn open(path_ptr: *const u8, _flags: c_int) -> c_int {
        // SAFETY: caller guarantees path_ptr is a valid, null-terminated C string.
        // We read only until we find a null byte, so no OOB access.
        let mut len = 0usize;
        while *path_ptr.add(len) != 0 {
            len += 1;
        }
        let path = std::str::from_utf8(std::slice::from_raw_parts(path_ptr, len))
            .unwrap_or("");

        if path.contains("missing") {
            -2  // ENOENT
        } else if path.contains("secret") {
            -13 // EACCES
        } else {
            3   // fake fd
        }
    }

    pub unsafe fn close(_fd: c_int) -> c_int {
        // SAFETY: caller guarantees fd is a valid open file descriptor.
        0 // success
    }
}

// ── Safe wrapper ──────────────────────────────────────────────────────────────

/// A safe file descriptor handle that closes on drop.
pub struct OwnedFd(c_int);

impl Drop for OwnedFd {
    fn drop(&mut self) {
        // SAFETY: self.0 was obtained from a successful open() call and
        // has not been closed yet (we own it exclusively).
        unsafe { mock_libc::close(self.0); }
    }
}

impl OwnedFd {
    pub fn raw(&self) -> c_int { self.0 }
}

/// Open a file, converting C error codes to `Result`.
pub fn open(path: &str) -> Result<OwnedFd, PosixError> {
    // Build a null-terminated byte string on the stack.
    let mut buf = [0u8; 256];
    let bytes = path.as_bytes();
    if bytes.len() >= buf.len() {
        return Err(PosixError::InvalidArg);
    }
    buf[..bytes.len()].copy_from_slice(bytes);
    // buf[bytes.len()] is already 0 (null terminator).

    // SAFETY: `buf` is a valid null-terminated byte array living on the stack.
    // We pass a pointer to it; the C function does not retain the pointer after
    // it returns, so there are no lifetime issues.
    let fd = unsafe { mock_libc::open(buf.as_ptr(), 0) };

    if fd >= 0 {
        Ok(OwnedFd(fd))
    } else {
        Err(PosixError::from_raw(fd))
    }
}

// ── Convenience: check-and-return pattern ─────────────────────────────────────

/// Helper used throughout the codebase: turn any negative C int into an error.
#[inline]
pub fn check(rc: c_int) -> Result<c_int, PosixError> {
    if rc >= 0 { Ok(rc) } else { Err(PosixError::from_raw(rc)) }
}

// ── main ──────────────────────────────────────────────────────────────────────

fn main() {
    let paths = &["normal_file.txt", "missing_file.txt", "secret_data.bin"];

    for path in paths {
        match open(path) {
            Ok(fd)  => println!("Opened '{}' → fd={}", path, fd.raw()),
            Err(e)  => println!("Failed '{}' → {}", path, e),
        }
    }

    // Demonstrate the `check()` helper with a raw C return value.
    let raw: c_int = -22; // pretend a C call returned EINVAL
    match check(raw) {
        Ok(v)  => println!("C call succeeded: {}", v),
        Err(e) => println!("C call failed: {}", e),
    }
}

// ── Tests ─────────────────────────────────────────────────────────────────────

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn success_path() {
        let fd = open("normal_file.txt").expect("should open");
        assert!(fd.raw() >= 0);
    }

    #[test]
    fn enoent() {
        let err = open("missing_file.txt").unwrap_err();
        assert_eq!(err, PosixError::NotFound);
    }

    #[test]
    fn eacces() {
        let err = open("secret_data.bin").unwrap_err();
        assert_eq!(err, PosixError::AccessDenied);
    }

    #[test]
    fn check_helper_positive() {
        assert_eq!(check(0), Ok(0));
        assert_eq!(check(42), Ok(42));
    }

    #[test]
    fn check_helper_negative() {
        assert_eq!(check(-2), Err(PosixError::NotFound));
    }

    #[test]
    fn error_display() {
        assert!(PosixError::AccessDenied.to_string().contains("EACCES"));
    }
}
(* OCaml FFI: C-style error codes via the Unix module *)

(* Declare an external C function that returns an int error code.
   By convention: 0 = success, negative = error. *)
external c_open : string -> int -> int = "caml_unix_open"

(* A typed error variant — mirrors what we'd see in errno.h *)
type posix_error =
  | ENOENT   (* No such file or directory *)
  | EACCES   (* Permission denied *)
  | EEXIST   (* File exists *)
  | Unknown of int

let posix_error_of_int = function
  | -2  -> ENOENT
  | -13 -> EACCES
  | -17 -> EEXIST
  | n   -> Unknown n

(* Wrap the C call: return Result-style using a variant *)
type ('a, 'e) result = Ok of 'a | Error of 'e

let safe_open path flags : (int, posix_error) result =
  let fd = c_open path flags in
  if fd >= 0 then Ok fd
  else Error (posix_error_of_int fd)

(* Higher-level: compose over result *)
let with_file path flags f =
  match safe_open path flags with
  | Error e -> Error e
  | Ok fd ->
      let r = f fd in
      (* close(fd) call elided for brevity *)
      Ok r

(* Simulate the pattern without actual C call *)
let simulate_c_call succeed =
  if succeed then 3 (* fake fd *) else -2 (* ENOENT *)

let () =
  let r =
    let raw = simulate_c_call false in
    if raw >= 0 then Ok raw
    else Error (posix_error_of_int raw)
  in
  match r with
  | Ok fd   -> Printf.printf "Opened fd=%d\n" fd
  | Error ENOENT -> Printf.printf "File not found\n"
  | Error EACCES -> Printf.printf "Permission denied\n"
  | Error _      -> Printf.printf "Unknown error\n"