FFI Architecture, C ABI Interop & Panic Boundary Guarding
Master Foreign Function Interface (FFI) bindings, C calling conventions, CString vs &str conversions, opaque pointers, and guarding against FFI panics.
No systems language operates in total isolation. Whether interacting with Linux kernel system calls, Windows Win32 APIs, graphics drivers (Vulkan/Metal), or legacy C libraries, Rust provides first-class support for Foreign Function Interfaces (FFI).
Bridging languages safely requires aligning CPU hardware registers and respecting platform Application Binary Interfaces (ABI).
Ensures argument registers, stack frames, and return conventions conform to platform C ABI standards.
1. Calling Conventions & The C ABI#
By default, Rust uses its internal unstable ABI (extern "Rust"). To interface with external libraries, you must specify the platform C calling convention:
When an external C function is declared in an extern "C" block, calling it is inherently unsafe because the compiler cannot verify foreign memory safety:
12345678910111213use std::os::raw::c_int; // Link external C library function: abs(int) extern "C" { fn abs(input: c_int) -> c_int; } fn main() { let num: c_int = -42; // Calling foreign functions requires an unsafe block: let result = unsafe { abs(num) }; println!("Absolute value: {}", result); // 42 }
2. String Representations: Rust &str vs C *const c_char#
A frequent source of memory corruption in FFI is string mismatch:
| String Type | Encoding | Length Tracking | Memory Termination |
|---|---|---|---|
Rust &str | UTF-8 | Explicit usize length in fat pointer. | No null terminator. |
C String (*const c_char) | Arbitrary (ASCII/Latin-1/UTF-8) | Not stored; requires searching for \0. | Null-terminated (\0). |
If you pass a Rust &str directly as a C pointer without a null terminator, C string functions (strlen, printf) will read past the buffer until hitting random memory, causing segmentation faults or data leakage!
12345678910111213141516use std::ffi::{CString, CStr}; use std::os::raw::c_char; fn pass_string_to_c() { // 1. Rust String to CString (allocates buffer with appended '\0') let rust_text = "Database Connected"; let c_text = CString::new(rust_text).expect("No interior null bytes"); // 2. Obtain raw pointer for C API let raw_c_ptr: *const c_char = c_text.as_ptr(); // 3. Convert C string pointer back into Rust &str: let borrowed_c_str: &CStr = unsafe { CStr::from_ptr(raw_c_ptr) }; let back_to_rust_str: &str = borrowed_c_str.to_str().expect("Valid UTF-8"); println!("Restored: {}", back_to_rust_str); }
3. The Fatal FFI Panic Boundary#
In modern Rust, panicking across an FFI boundary is undefined behavior.
If foreign C code calls an exported Rust function, and that function triggers a panic with stack unwinding, the unwinder cannot parse C stack frames. The runtime intercepts this condition and immediately aborts the process (SIGABRT).
To write resilient FFI libraries, wrap all exported entry points in std::panic::catch_unwind:
123456789101112131415161718use std::panic::catch_unwind; // Exported to C: must never unwind! #[no_mangle] pub extern "C" fn calculate_metrics(input: i32) -> i32 { let result = catch_unwind(|| { // Any panic here is safely caught inside the Rust boundary: if input < 0 { panic!("Negative input rejected"); } input * 10 }); match result { Ok(val) => val, Err(_) => -1, // Translate caught panic into C-compatible error code! } }
#[no_mangle] so the exact symbol name appears in the compiled shared library (.so, .dll).4. Concept Verification#
Test your understanding of FFI boundaries and cross-language safety:
Why can you not pass a raw Rust &str pointer directly to a C library function expecting a const char*?
What happens if an exported Rust function panics without catching it when called from foreign C code?