Home/Unsafe & FFI/FFI Boundaries

FFI Boundaries

C interop best practices

advanced
ffic-interopextern
🎮 Interactive Playground

What is FFI?

Foreign Function Interface (FFI) allows Rust code to interact with code written in other languages, primarily C. FFI boundaries are the interfaces where Rust's safety guarantees meet the unsafe world of foreign code. Properly designed FFI boundaries ensure that unsafe foreign interactions don't compromise Rust's memory safety.

FFI is essential for:

  • Interfacing with system libraries (libc, Windows API, POSIX)
  • Wrapping existing C/C++ libraries
  • Allowing other languages to call Rust code
  • Performance-critical code leveraging optimized C libraries
  • Hardware interaction and device drivers

The Problem

When crossing FFI boundaries, you face several challenges:

  1. Type Safety: C has different type representations and no ownership semantics
  2. Memory Management: Who owns allocated memory? Who frees it?
  3. Error Handling: C uses error codes, Rust uses Result types
  4. Null Pointers: C allows null pointers everywhere, Rust doesn't
  5. String Encoding: C uses null-terminated strings, Rust uses UTF-8 slices
  6. ABI Compatibility: Function calling conventions must match
  7. Undefined Behavior: C's undefined behavior can violate Rust's guarantees

Without careful design, FFI code can introduce memory leaks, data races, use-after-free bugs, and other memory safety violations.

Example Code

Example 1: Basic FFI Declaration

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

// Declare external C functions
extern "C" {
    // Standard C library functions
    fn strlen(s: *const c_char) -> usize;
    fn strcmp(s1: *const c_char, s2: *const c_char) -> c_int;
    fn malloc(size: usize) -> *mut c_void;
    fn free(ptr: *mut c_void);

    // Custom C library
    fn compute_hash(data: *const u8, len: usize) -> u64;
    fn process_array(arr: *mut i32, len: usize) -> c_int;
}

// Safe wrapper around strlen
fn safe_strlen(s: &str) -> usize {
    // Convert Rust string to C string
    let c_str = std::ffi::CString::new(s).expect("CString::new failed");

    unsafe {
        // SAFETY: c_str.as_ptr() returns a valid null-terminated string
        strlen(c_str.as_ptr())
    }
}

// Safe wrapper around strcmp
fn safe_strcmp(s1: &str, s2: &str) -> std::cmp::Ordering {
    let c_str1 = std::ffi::CString::new(s1).expect("CString::new failed");
    let c_str2 = std::ffi::CString::new(s2).expect("CString::new failed");

    unsafe {
        // SAFETY: Both pointers are valid null-terminated strings
        let result = strcmp(c_str1.as_ptr(), c_str2.as_ptr());
        use std::cmp::Ordering;
        match result {
            0 => Ordering::Equal,
            x if x < 0 => Ordering::Less,
            _ => Ordering::Greater,
        }
    }
}

fn example_basic_ffi() {
    let s = "Hello, FFI!";
    let len = safe_strlen(s);
    println!("Length of '{}': {}", s, len);

    let s1 = "apple";
    let s2 = "banana";
    println!("Comparing '{}' and '{}': {:?}", s1, s2, safe_strcmp(s1, s2));
}

Example 2: String Handling

use std::ffi::{CStr, CString};
use std::os::raw::c_char;

extern "C" {
    fn get_version() -> *const c_char;
    fn set_name(name: *const c_char) -> bool;
    fn get_error_message() -> *mut c_char;
    fn free_string(s: *mut c_char);
}

/// Safe wrapper that converts C string to Rust String
fn safe_get_version() -> Option<String> {
    unsafe {
        // SAFETY: Assuming get_version returns a valid static string or null
        let ptr = get_version();
        if ptr.is_null() {
            return None;
        }

        // Convert C string to Rust string
        let c_str = CStr::from_ptr(ptr);
        c_str.to_str().ok().map(|s| s.to_owned())
    }
}

/// Safe wrapper that converts Rust string to C string
fn safe_set_name(name: &str) -> Result<(), Box<dyn std::error::Error>> {
    // CString automatically adds null terminator
    let c_name = CString::new(name)?;

    unsafe {
        // SAFETY: c_name.as_ptr() is valid for the duration of this call
        let success = set_name(c_name.as_ptr());
        if success {
            Ok(())
        } else {
            Err("Failed to set name".into())
        }
    }
}

/// Wrapper for C function that allocates a string (caller must free)
fn safe_get_error_message() -> Option<String> {
    unsafe {
        // SAFETY: Assuming function returns null or a valid malloc'd string
        let ptr = get_error_message();
        if ptr.is_null() {
            return None;
        }

        // Convert to CStr first
        let c_str = CStr::from_ptr(ptr);
        let result = c_str.to_str().ok().map(|s| s.to_owned());

        // Free the C-allocated string
        free_string(ptr);

        result
    }
}

fn example_string_handling() {
    if let Some(version) = safe_get_version() {
        println!("Version: {}", version);
    }

    if let Err(e) = safe_set_name("RustApp") {
        eprintln!("Error setting name: {}", e);
    }

    if let Some(error) = safe_get_error_message() {
        eprintln!("C library error: {}", error);
    }
}

Example 3: Struct Passing

use std::os::raw::{c_int, c_double};

// C-compatible struct with #[repr(C)]
#[repr(C)]
#[derive(Debug, Copy, Clone)]
pub struct Point {
    pub x: c_double,
    pub y: c_double,
}

#[repr(C)]
#[derive(Debug, Copy, Clone)]
pub struct Rect {
    pub top_left: Point,
    pub bottom_right: Point,
}

#[repr(C)]
pub struct Color {
    pub r: u8,
    pub g: u8,
    pub b: u8,
    pub a: u8,
}

extern "C" {
    fn calculate_distance(p1: Point, p2: Point) -> c_double;
    fn rect_contains_point(rect: *const Rect, point: Point) -> bool;
    fn create_gradient(start: Color, end: Color, steps: c_int) -> *mut Color;
    fn free_colors(colors: *mut Color);
}

impl Point {
    pub fn new(x: f64, y: f64) -> Self {
        Point { x, y }
    }

    pub fn distance_to(&self, other: &Point) -> f64 {
        unsafe {
            // SAFETY: Point is repr(C) and both are valid
            calculate_distance(*self, *other)
        }
    }
}

impl Rect {
    pub fn new(x1: f64, y1: f64, x2: f64, y2: f64) -> Self {
        Rect {
            top_left: Point::new(x1, y1),
            bottom_right: Point::new(x2, y2),
        }
    }

    pub fn contains(&self, point: &Point) -> bool {
        unsafe {
            // SAFETY: self is repr(C) and we pass a valid reference
            rect_contains_point(self as *const Rect, *point)
        }
    }
}

impl Color {
    pub fn new(r: u8, g: u8, b: u8, a: u8) -> Self {
        Color { r, g, b, a }
    }

    pub fn gradient(start: Color, end: Color, steps: usize) -> Vec<Color> {
        unsafe {
            // SAFETY: Assuming function returns valid array of 'steps' colors
            let ptr = create_gradient(start, end, steps as c_int);
            if ptr.is_null() {
                return Vec::new();
            }

            // Copy colors to Rust Vec
            let colors = std::slice::from_raw_parts(ptr, steps).to_vec();

            // Free C-allocated memory
            free_colors(ptr);

            colors
        }
    }
}

fn example_struct_passing() {
    let p1 = Point::new(0.0, 0.0);
    let p2 = Point::new(3.0, 4.0);
    let distance = p1.distance_to(&p2);
    println!("Distance: {}", distance);

    let rect = Rect::new(0.0, 0.0, 10.0, 10.0);
    let point = Point::new(5.0, 5.0);
    println!("Contains: {}", rect.contains(&point));

    let start = Color::new(255, 0, 0, 255);
    let end = Color::new(0, 0, 255, 255);
    let gradient = Color::gradient(start, end, 10);
    println!("Gradient steps: {}", gradient.len());
}

Example 4: Callback Functions

use std::os::raw::{c_int, c_void};
use std::ffi::c_void as ffi_void;

// Type alias for C callback
type Callback = extern "C" fn(value: c_int, user_data: *mut c_void);

extern "C" {
    fn register_callback(callback: Callback, user_data: *mut c_void);
    fn process_items(items: *const c_int, count: usize);
    fn unregister_callback();
}

// Rust callback function with C ABI
extern "C" fn rust_callback(value: c_int, user_data: *mut c_void) {
    unsafe {
        // SAFETY: user_data must be a valid pointer to our context
        if !user_data.is_null() {
            let context = &mut *(user_data as *mut CallbackContext);
            context.sum += value as i64;
            context.count += 1;
            println!("Callback received: {}", value);
        }
    }
}

struct CallbackContext {
    sum: i64,
    count: usize,
}

fn example_callbacks() {
    let mut context = CallbackContext { sum: 0, count: 0 };

    unsafe {
        // SAFETY: context lives longer than the callback registration
        let context_ptr = &mut context as *mut _ as *mut c_void;
        register_callback(rust_callback, context_ptr);

        // Process some items, which will trigger callbacks
        let items = vec![10, 20, 30, 40, 50];
        process_items(items.as_ptr(), items.len());

        unregister_callback();
    }

    println!("Total sum: {}, count: {}", context.sum, context.count);
}

Example 5: Opaque Types

use std::marker::PhantomData;
use std::os::raw::c_void;

// Opaque type - we don't know its layout
#[repr(C)]
pub struct OpaqueHandle {
    _private: [u8; 0],
    _marker: PhantomData<(*mut u8, std::marker::PhantomPinned)>,
}

extern "C" {
    fn create_handle(config: *const u8, len: usize) -> *mut OpaqueHandle;
    fn use_handle(handle: *const OpaqueHandle) -> i32;
    fn destroy_handle(handle: *mut OpaqueHandle);
}

/// Safe wrapper around opaque C handle
pub struct Handle {
    ptr: *mut OpaqueHandle,
}

impl Handle {
    pub fn new(config: &[u8]) -> Result<Self, &'static str> {
        unsafe {
            // SAFETY: config is a valid slice
            let ptr = create_handle(config.as_ptr(), config.len());
            if ptr.is_null() {
                Err("Failed to create handle")
            } else {
                Ok(Handle { ptr })
            }
        }
    }

    pub fn use_it(&self) -> i32 {
        unsafe {
            // SAFETY: ptr is valid for the lifetime of self
            use_handle(self.ptr)
        }
    }
}

impl Drop for Handle {
    fn drop(&mut self) {
        unsafe {
            // SAFETY: ptr is valid and we own it
            destroy_handle(self.ptr);
        }
    }
}

// Ensure Handle is not Send/Sync if the C library isn't thread-safe
// Uncomment if needed:
// impl !Send for Handle {}
// impl !Sync for Handle {}

fn example_opaque_types() {
    let config = b"some configuration";
    match Handle::new(config) {
        Ok(handle) => {
            let result = handle.use_it();
            println!("Handle result: {}", result);
            // handle is automatically destroyed here
        }
        Err(e) => eprintln!("Error: {}", e),
    }
}

Example 6: Error Handling

use std::os::raw::{c_int, c_char};
use std::ffi::CStr;

extern "C" {
    fn perform_operation(input: c_int) -> c_int;
    fn get_last_error() -> c_int;
    fn get_error_string(error_code: c_int) -> *const c_char;
}

#[derive(Debug)]
pub enum FFIError {
    InvalidInput,
    OutOfMemory,
    IoError,
    Unknown(i32),
}

impl std::fmt::Display for FFIError {
    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
        match self {
            FFIError::InvalidInput => write!(f, "Invalid input"),
            FFIError::OutOfMemory => write!(f, "Out of memory"),
            FFIError::IoError => write!(f, "I/O error"),
            FFIError::Unknown(code) => write!(f, "Unknown error: {}", code),
        }
    }
}

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

impl FFIError {
    fn from_error_code(code: i32) -> Self {
        match code {
            1 => FFIError::InvalidInput,
            2 => FFIError::OutOfMemory,
            3 => FFIError::IoError,
            _ => FFIError::Unknown(code),
        }
    }

    fn get_message(&self) -> Option<String> {
        if let FFIError::Unknown(code) = self {
            unsafe {
                let ptr = get_error_string(*code);
                if !ptr.is_null() {
                    let c_str = CStr::from_ptr(ptr);
                    return c_str.to_str().ok().map(|s| s.to_owned());
                }
            }
        }
        None
    }
}

fn safe_perform_operation(input: i32) -> Result<i32, FFIError> {
    unsafe {
        // SAFETY: perform_operation is a valid C function
        let result = perform_operation(input);

        if result < 0 {
            // Operation failed, get error code
            let error_code = get_last_error();
            Err(FFIError::from_error_code(error_code))
        } else {
            Ok(result)
        }
    }
}

fn example_error_handling() {
    match safe_perform_operation(42) {
        Ok(result) => println!("Success: {}", result),
        Err(e) => {
            eprintln!("Error: {}", e);
            if let Some(msg) = e.get_message() {
                eprintln!("Details: {}", msg);
            }
        }
    }
}

Example 7: Array and Buffer Handling

use std::os::raw::{c_int, c_float};
use std::slice;

extern "C" {
    fn sum_array(arr: *const c_float, len: usize) -> c_float;
    fn modify_array(arr: *mut c_int, len: usize);
    fn allocate_buffer(size: usize) -> *mut u8;
    fn free_buffer(ptr: *mut u8);
    fn fill_buffer(buffer: *mut u8, size: usize, pattern: u8);
}

/// Safe wrapper for summing an array
fn safe_sum_array(arr: &[f32]) -> f32 {
    unsafe {
        // SAFETY: arr is a valid slice
        sum_array(arr.as_ptr(), arr.len())
    }
}

/// Safe wrapper for modifying an array
fn safe_modify_array(arr: &mut [i32]) {
    unsafe {
        // SAFETY: arr is a valid mutable slice
        modify_array(arr.as_mut_ptr(), arr.len());
    }
}

/// RAII wrapper for C-allocated buffer
pub struct CBuffer {
    ptr: *mut u8,
    size: usize,
}

impl CBuffer {
    pub fn new(size: usize) -> Option<Self> {
        unsafe {
            let ptr = allocate_buffer(size);
            if ptr.is_null() {
                None
            } else {
                Some(CBuffer { ptr, size })
            }
        }
    }

    pub fn fill(&mut self, pattern: u8) {
        unsafe {
            // SAFETY: ptr is valid and size is correct
            fill_buffer(self.ptr, self.size, pattern);
        }
    }

    pub fn as_slice(&self) -> &[u8] {
        unsafe {
            // SAFETY: ptr is valid for size bytes
            slice::from_raw_parts(self.ptr, self.size)
        }
    }

    pub fn as_mut_slice(&mut self) -> &mut [u8] {
        unsafe {
            // SAFETY: ptr is valid for size bytes and we have exclusive access
            slice::from_raw_parts_mut(self.ptr, self.size)
        }
    }
}

impl Drop for CBuffer {
    fn drop(&mut self) {
        unsafe {
            // SAFETY: ptr was allocated by allocate_buffer
            free_buffer(self.ptr);
        }
    }
}

// Safety: If the C library is thread-safe
unsafe impl Send for CBuffer {}
unsafe impl Sync for CBuffer {}

fn example_array_handling() {
    let arr = vec![1.0, 2.0, 3.0, 4.0, 5.0];
    let sum = safe_sum_array(&arr);
    println!("Sum: {}", sum);

    let mut arr2 = vec![1, 2, 3, 4, 5];
    safe_modify_array(&mut arr2);
    println!("Modified: {:?}", arr2);

    if let Some(mut buffer) = CBuffer::new(1024) {
        buffer.fill(0xFF);
        println!("Buffer first bytes: {:?}", &buffer.as_slice()[..10]);
    }
}

Why It Works

Type Compatibility

C and Rust have different type representations. Use #[repr(C)] to ensure Rust types match C's layout:

#[repr(C)]
struct Point {
    x: f64,
    y: f64,
}

Memory Safety

FFI boundaries are inherently unsafe. Safe wrappers ensure:

  1. Validity: Pointers are valid before dereferencing
  2. Lifetime: Data lives long enough for C code to use it
  3. Ownership: Clear ownership of allocated memory
  4. Aliasing: No mutable aliasing across the boundary

ABI Compatibility

Use extern "C" to specify C calling convention:

extern "C" fn callback(x: i32) -> i32 {
    x * 2
}

Null Pointer Safety

C can return null pointers. Always check:

unsafe {
    let ptr = c_function();
    if ptr.is_null() {
        return Err("Null pointer");
    }
    // Safe to use ptr now
}

When to Use

Use FFI when:

  • Wrapping existing C/C++ libraries
  • Calling system APIs
  • Integrating with platform-specific code
  • Using hardware-specific libraries
  • Performance-critical operations in C

Create safe wrappers when:

  • Exposing FFI to Rust users
  • Building library interfaces
  • Managing C resources (memory, handles)
  • Converting between C and Rust types

Direct unsafe FFI when:

  • Internal implementation details
  • Performance-critical hot paths
  • Working with well-understood C APIs

⚠️ Anti-patterns

⚠️ Mistake #1: Not Checking Null Pointers

extern "C" {
    fn get_data() -> *const u8;
}

// ❌ DON'T: Dereference without null check
unsafe fn bad() -> u8 {
    *get_data() // May be null!
}

// ✅ DO: Check for null first
unsafe fn good() -> Option<u8> {
    let ptr = get_data();
    if ptr.is_null() {
        None
    } else {
        Some(*ptr)
    }
}

⚠️ Mistake #2: Memory Leaks

extern "C" {
    fn allocate_data() -> *mut u8;
    fn free_data(ptr: *mut u8);
}

// ❌ DON'T: Forget to free
unsafe fn leak() {
    let ptr = allocate_data();
    // Use ptr
    // Oops, never freed!
}

// ✅ DO: Use RAII wrapper
struct Data(*mut u8);

impl Drop for Data {
    fn drop(&mut self) {
        unsafe { free_data(self.0); }
    }
}

⚠️ Mistake #3: String Encoding Issues

use std::ffi::CString;

// ❌ DON'T: Ignore null bytes in strings
fn bad_string(s: &str) {
    let c_str = CString::new(s).unwrap(); // Panics if s contains null!
}

// ✅ DO: Handle null bytes gracefully
fn good_string(s: &str) -> Result<CString, std::ffi::NulError> {
    CString::new(s)
}

⚠️ Mistake #4: Lifetime Issues

use std::ffi::CString;

extern "C" {
    fn store_string(s: *const i8);
}

// ❌ DON'T: Temporary CString
unsafe fn bad() {
    let s = CString::new("hello").unwrap();
    store_string(s.as_ptr());
    // s is dropped here, but C code may still reference it!
}

// ✅ DO: Ensure lifetime
unsafe fn good(s: &CString) {
    store_string(s.as_ptr());
    // Caller ensures s lives long enough
}

⚠️ Mistake #5: Assuming Thread Safety

// ❌ DON'T: Assume C library is thread-safe
struct Handle(*mut std::ffi::c_void);

unsafe impl Send for Handle {} // Dangerous if C library isn't thread-safe!
unsafe impl Sync for Handle {}

// ✅ DO: Document and verify thread safety
struct SafeHandle(*mut std::ffi::c_void);

// Only implement if C library documentation guarantees thread safety
// unsafe impl Send for SafeHandle {}

Advanced Example: Complete FFI Wrapper

use std::ffi::{CStr, CString};
use std::os::raw::{c_char, c_int, c_void};
use std::ptr;

// External C library declarations
extern "C" {
    fn db_open(path: *const c_char) -> *mut c_void;
    fn db_close(handle: *mut c_void);
    fn db_get(handle: *mut c_void, key: *const c_char) -> *mut c_char;
    fn db_set(handle: *mut c_void, key: *const c_char, value: *const c_char) -> c_int;
    fn db_delete(handle: *mut c_void, key: *const c_char) -> c_int;
    fn db_free_value(value: *mut c_char);
}

#[derive(Debug)]
pub enum DbError {
    OpenFailed,
    NotFound,
    WriteFailed,
    InvalidKey,
    InvalidValue,
}

impl std::fmt::Display for DbError {
    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
        match self {
            DbError::OpenFailed => write!(f, "Failed to open database"),
            DbError::NotFound => write!(f, "Key not found"),
            DbError::WriteFailed => write!(f, "Write operation failed"),
            DbError::InvalidKey => write!(f, "Invalid key"),
            DbError::InvalidValue => write!(f, "Invalid value"),
        }
    }
}

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

/// Safe wrapper around C database library
pub struct Database {
    handle: *mut c_void,
}

impl Database {
    /// Open a database at the given path
    pub fn open(path: &str) -> Result<Self, DbError> {
        let c_path = CString::new(path).map_err(|_| DbError::InvalidKey)?;

        unsafe {
            // SAFETY: c_path is a valid null-terminated string
            let handle = db_open(c_path.as_ptr());
            if handle.is_null() {
                Err(DbError::OpenFailed)
            } else {
                Ok(Database { handle })
            }
        }
    }

    /// Get a value by key
    pub fn get(&self, key: &str) -> Result<String, DbError> {
        let c_key = CString::new(key).map_err(|_| DbError::InvalidKey)?;

        unsafe {
            // SAFETY: handle is valid, c_key is a valid string
            let value_ptr = db_get(self.handle, c_key.as_ptr());

            if value_ptr.is_null() {
                return Err(DbError::NotFound);
            }

            // Convert C string to Rust String
            let c_str = CStr::from_ptr(value_ptr);
            let result = c_str
                .to_str()
                .map_err(|_| DbError::InvalidValue)?
                .to_owned();

            // Free the C-allocated string
            db_free_value(value_ptr);

            Ok(result)
        }
    }

    /// Set a key-value pair
    pub fn set(&mut self, key: &str, value: &str) -> Result<(), DbError> {
        let c_key = CString::new(key).map_err(|_| DbError::InvalidKey)?;
        let c_value = CString::new(value).map_err(|_| DbError::InvalidValue)?;

        unsafe {
            // SAFETY: handle is valid, both strings are valid
            let result = db_set(self.handle, c_key.as_ptr(), c_value.as_ptr());

            if result == 0 {
                Ok(())
            } else {
                Err(DbError::WriteFailed)
            }
        }
    }

    /// Delete a key
    pub fn delete(&mut self, key: &str) -> Result<(), DbError> {
        let c_key = CString::new(key).map_err(|_| DbError::InvalidKey)?;

        unsafe {
            // SAFETY: handle is valid, c_key is valid
            let result = db_delete(self.handle, c_key.as_ptr());

            if result == 0 {
                Ok(())
            } else {
                Err(DbError::NotFound)
            }
        }
    }
}

impl Drop for Database {
    fn drop(&mut self) {
        unsafe {
            // SAFETY: handle is valid and we own it
            db_close(self.handle);
        }
    }
}

// Only implement if the C library is thread-safe
// unsafe impl Send for Database {}
// unsafe impl Sync for Database {}

fn database_example() {
    match Database::open("/tmp/test.db") {
        Ok(mut db) => {
            // Set some values
            if let Err(e) = db.set("name", "Alice") {
                eprintln!("Error setting value: {}", e);
            }

            // Get a value
            match db.get("name") {
                Ok(value) => println!("Name: {}", value),
                Err(e) => eprintln!("Error getting value: {}", e),
            }

            // Delete a key
            if let Err(e) = db.delete("name") {
                eprintln!("Error deleting key: {}", e);
            }
        }
        Err(e) => eprintln!("Error opening database: {}", e),
    }
}

Advanced Example: Callback with Context

use std::collections::HashMap;
use std::os::raw::{c_int, c_void};
use std::sync::{Arc, Mutex};

type NativeCallback = extern "C" fn(event_type: c_int, data: *const c_void, user_data: *mut c_void);

extern "C" {
    fn register_event_handler(callback: NativeCallback, user_data: *mut c_void) -> c_int;
    fn unregister_event_handler(handler_id: c_int);
    fn trigger_event(event_type: c_int, data: *const c_void);
}

/// Rust-friendly event handler trait
pub trait EventHandler: Send {
    fn handle_event(&mut self, event_type: i32, data: &[u8]);
}

/// Safe wrapper for event system
pub struct EventSystem {
    handlers: Arc<Mutex<HashMap<i32, Box<dyn EventHandler>>>>,
    handler_id: Option<i32>,
}

impl EventSystem {
    pub fn new() -> Self {
        EventSystem {
            handlers: Arc::new(Mutex::new(HashMap::new())),
            handler_id: None,
        }
    }

    pub fn register_handler<H: EventHandler + 'static>(&mut self, event_type: i32, handler: H) {
        let mut handlers = self.handlers.lock().unwrap();
        handlers.insert(event_type, Box::new(handler));
    }

    pub fn start(&mut self) -> Result<(), &'static str> {
        // Create a boxed context to pass to C
        let context = Box::new(Arc::clone(&self.handlers));
        let context_ptr = Box::into_raw(context) as *mut c_void;

        unsafe {
            // SAFETY: context_ptr is a valid pointer to our Arc
            let id = register_event_handler(event_callback, context_ptr);
            if id < 0 {
                // Failed to register, reclaim the box
                let _ = Box::from_raw(context_ptr as *mut Arc<Mutex<HashMap<i32, Box<dyn EventHandler>>>>);
                Err("Failed to register handler")
            } else {
                self.handler_id = Some(id);
                Ok(())
            }
        }
    }

    pub fn stop(&mut self) {
        if let Some(id) = self.handler_id.take() {
            unsafe {
                unregister_event_handler(id);
            }
        }
    }
}

impl Drop for EventSystem {
    fn drop(&mut self) {
        self.stop();
    }
}

extern "C" fn event_callback(event_type: c_int, data: *const c_void, user_data: *mut c_void) {
    unsafe {
        // SAFETY: user_data is a valid pointer to our Arc
        if user_data.is_null() {
            return;
        }

        let handlers_ptr = user_data as *const Arc<Mutex<HashMap<i32, Box<dyn EventHandler>>>>;
        let handlers_arc = &*handlers_ptr;

        let mut handlers = handlers_arc.lock().unwrap();

        if let Some(handler) = handlers.get_mut(&event_type) {
            // Convert data to slice (assuming it's a simple byte buffer)
            // In real code, you'd have a more sophisticated protocol
            let data_slice = if !data.is_null() {
                std::slice::from_raw_parts(data as *const u8, 64) // Example size
            } else {
                &[]
            };

            handler.handle_event(event_type, data_slice);
        }
    }
}

// Example event handler implementation
struct LoggingHandler;

impl EventHandler for LoggingHandler {
    fn handle_event(&mut self, event_type: i32, data: &[u8]) {
        println!("Event {}: {} bytes", event_type, data.len());
    }
}

fn event_system_example() {
    let mut system = EventSystem::new();
    system.register_handler(1, LoggingHandler);

    if let Err(e) = system.start() {
        eprintln!("Error starting event system: {}", e);
        return;
    }

    // Event system is now running
    // Events from C will be handled by our Rust handlers

    system.stop();
}

Advanced Example: Zero-Copy FFI

use std::os::raw::c_void;
use std::slice;

extern "C" {
    fn create_buffer(size: usize) -> *mut c_void;
    fn destroy_buffer(buffer: *mut c_void);
    fn get_buffer_data(buffer: *mut c_void) -> *mut u8;
    fn get_buffer_size(buffer: *mut c_void) -> usize;
    fn process_buffer(buffer: *mut c_void) -> i32;
}

/// Zero-copy wrapper around C buffer
pub struct SharedBuffer {
    handle: *mut c_void,
}

impl SharedBuffer {
    pub fn new(size: usize) -> Option<Self> {
        unsafe {
            let handle = create_buffer(size);
            if handle.is_null() {
                None
            } else {
                Some(SharedBuffer { handle })
            }
        }
    }

    /// Get a slice view of the buffer without copying
    pub fn as_slice(&self) -> &[u8] {
        unsafe {
            let ptr = get_buffer_data(self.handle);
            let size = get_buffer_size(self.handle);
            slice::from_raw_parts(ptr, size)
        }
    }

    /// Get a mutable slice view
    pub fn as_mut_slice(&mut self) -> &mut [u8] {
        unsafe {
            let ptr = get_buffer_data(self.handle);
            let size = get_buffer_size(self.handle);
            slice::from_raw_parts_mut(ptr, size)
        }
    }

    /// Process buffer in-place using C code
    pub fn process(&mut self) -> Result<(), &'static str> {
        unsafe {
            let result = process_buffer(self.handle);
            if result == 0 {
                Ok(())
            } else {
                Err("Processing failed")
            }
        }
    }
}

impl Drop for SharedBuffer {
    fn drop(&mut self) {
        unsafe {
            destroy_buffer(self.handle);
        }
    }
}

fn zero_copy_example() {
    if let Some(mut buffer) = SharedBuffer::new(1024) {
        // Write to buffer
        let slice = buffer.as_mut_slice();
        for (i, byte) in slice.iter_mut().enumerate() {
            *byte = (i % 256) as u8;
        }

        // Process in-place
        if let Err(e) = buffer.process() {
            eprintln!("Error: {}", e);
        }

        // Read results
        let result = buffer.as_slice();
        println!("First 10 bytes: {:?}", &result[..10]);
    }
}

Performance Characteristics

FFI Call Overhead

  • Direct FFI: ~1-2ns overhead per call (similar to native function call)
  • With conversions: CString creation, validation add overhead
  • Callbacks: Function pointer indirection, context passing

Memory Considerations

  • Zero-copy: Share memory directly between Rust and C
  • Copying: CString/String conversions copy data
  • Allocation: C allocations bypass Rust's allocator

Optimization Tips

  1. Batch calls: Reduce FFI boundary crossings
  2. Buffer reuse: Reuse buffers instead of allocating
  3. Inline: Use LTO to inline across language boundaries
  4. Avoid conversions: Use raw types when safe

Exercises

Beginner

  1. Wrap a simple C function that takes an integer and returns an integer
  2. Create a safe wrapper for strlen from libc
  3. Write a function that converts a Rust string to a C string and back

Intermediate

  1. Wrap a C function that allocates memory and returns a pointer
  2. Implement a safe wrapper around a C struct with proper Drop
  3. Create a callback system where C calls back into Rust
  4. Build a wrapper for a C library that uses error codes

Advanced

  1. Implement a thread-safe wrapper around a non-thread-safe C library
  2. Create a zero-copy buffer sharing system between Rust and C
  3. Build an event system that bridges C events to Rust async
  4. Design a plugin system where C code loads Rust plugins
  5. Implement a safe API for a complex C library with resources and callbacks

Real-World Usage

System Libraries

// libc bindings
use libc::{c_int, c_void, size_t};

extern "C" {
    fn socket(domain: c_int, ty: c_int, protocol: c_int) -> c_int;
    fn bind(sockfd: c_int, addr: *const c_void, addrlen: size_t) -> c_int;
}

OpenSSL

// openssl-sys crate provides FFI bindings
use openssl_sys::*;

// High-level wrapper in openssl crate
use openssl::ssl::{SslConnector, SslMethod};

libsqlite3

// rusqlite wraps sqlite3 C library
use rusqlite::{Connection, Result};

let conn = Connection::open("my_db.db")?;
conn.execute("CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY, name TEXT)", [])?;

libgit2

// git2 crate wraps libgit2
use git2::Repository;

let repo = Repository::open(".")?;
let head = repo.head()?;

SDL2

// sdl2 crate wraps SDL2 C library
use sdl2::pixels::Color;
use sdl2::event::Event;

let sdl_context = sdl2::init()?;
let video_subsystem = sdl_context.video()?;

Further Reading

Main Function

fn main() {
    println!("=== Basic FFI ===");
    example_basic_ffi();

    println!("\n=== String Handling ===");
    example_string_handling();

    println!("\n=== Struct Passing ===");
    example_struct_passing();

    println!("\n=== Callbacks ===");
    example_callbacks();

    println!("\n=== Opaque Types ===");
    example_opaque_types();

    println!("\n=== Error Handling ===");
    example_error_handling();

    println!("\n=== Array Handling ===");
    example_array_handling();

    println!("\n=== Database Example ===");
    database_example();

    println!("\n=== Event System ===");
    event_system_example();

    println!("\n=== Zero-Copy ===");
    zero_copy_example();
}

🎮 Try it Yourself

🎮

FFI Boundaries - Playground

Run this code in the official Rust Playground