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.

04. Unsafe Rust, Layout Control & Systems Internals7 min read

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).

The Universal ABIInteroperability
extern \

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:

Architecture Diagram
Rendering diagram...

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:

1
2
3
4
5
6
7
8
9
10
11
12
13
use 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 TypeEncodingLength TrackingMemory Termination
Rust &strUTF-8Explicit 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!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
use 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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
use 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! } }
The #[no_mangle] Attribute
By default, the compiler mangles function names to support generics and modules. When exporting functions for C linkers, add #[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:

Concept check

Why can you not pass a raw Rust &str pointer directly to a C library function expecting a const char*?

Concept check

What happens if an exported Rust function panics without catching it when called from foreign C code?