tezvyn:

FFI Error Handling: Translation and Unwinding

AI-drafted, machine-checkedSource: nrc.github.iointermediate

FFI error handling is a translation layer: foreign errors must become Rust Results before safe code sees them, or you risk UB. You do this in -sys wrappers around C libraries. The footgun: foreign exceptions unwinding across boundary without -unwind ABI is UB.

WHY IT EXISTS: Rust's ownership and Result-based error model cannot see across language boundaries. When you call C or OS syscalls, errors arrive as integers, null pointers, sentinel values, or thread-unwinding exceptions. If you do not normalize these foreign mechanisms into Rust's type system at the boundary, safe code ends up carrying raw error numbers or, worse, letting foreign stack unwinding crash or corrupt the process.

THE MENTAL MODEL: Think of the FFI boundary as a customs checkpoint. Every error must declare its nature in Rust terms before entering. C errors are just data, so translating them is like converting any other struct or integer. Exceptions are radioactive cargo: they must be caught on the side that throws them, because an unwinding stack cannot cross the language border without risking undefined behavior or an immediate abort.

HOW IT WORKS: The standard pattern is a pair of crates. The -sys crate contains thin, auto-generated bindings that mirror the foreign API exactly, often returning raw integers or pointers. The idiomatic wrapper crate converts those into Result types. For C error codes, you match the raw value: negative numbers might become Err variants, non-negative numbers become Ok. For OS errors, std::io::Error::from_raw_os_error builds a Rust error from an errno, while last_os_error reads the thread's last error value. For exceptions, Rust extern functions use an ABI with or without the -unwind suffix. Without it, a foreign unwind entering Rust is undefined behavior. With it, the unwind may propagate, but if you try to catch it with catch_unwind you get an opaque error or an abort. Rust panics reaching foreign code always abort the process.

WHEN TO USE IT: Use this pattern whenever you maintain bindings to a C library, wrap platform-specific syscalls, or expose a Rust library to another language. If the foreign API returns error codes, translate them immediately in the wrapper. If the foreign API uses exceptions or setjmp/longjmp, install a catch barrier on the foreign side before calling back into Rust.

WHEN NOT TO USE IT: Do not try to unify every C error into a single Rust error type if the wrapper is meant to be a thin, zero-cost shim. Also, do not use last_os_error after any other library call has had a chance to overwrite the thread-local error state. Do not let Rust panics escape into C, and do not assume catch_unwind gives you a useful payload after a foreign unwind.

ONE CANONICAL EXAMPLE: A C library returns an isize where negative values signal failure. In the -sys crate you get that isize directly. In the safe wrapper you write a match that maps -1 to Err(MyError::Foo), -2 to Err(MyError::Bar), and other negatives to Err(MyError::UnknownForeign(c)), while zero or positive values return Ok(c as usize). If you need to pass the error back to C later, you reverse the mapping in a From impl.

Read the original → nrc.github.io

Get five bites like this every day.

Tezvyn delivers a daily feed of 60-second tech bites with quizzes to lock in what you learn.