1. rust
  2. /systems
  3. /ffi-c-interop

FFI & C Interop

Foreign Function Interface (FFI) allows Rust to interoperate with code written in other languages, primarily C. This capability is essential for integrating with existing C libraries, operating system APIs, and building libraries that can be used from other languages.

Basic FFI Concepts

FFI in Rust involves crossing the boundary between safe Rust code and unsafe foreign functions. This requires careful handling of data types, memory management, and error handling.

Calling C Functions from Rust

// Basic extern declaration
extern "C" {
    fn puts(s: *const std::os::raw::c_char) -> std::os::raw::c_int;
    fn strlen(s: *const std::os::raw::c_char) -> usize;
    fn malloc(size: usize) -> *mut std::os::raw::c_void;
    fn free(ptr: *mut std::os::raw::c_void);
}

use std::ffi::CString;

fn main() {
    let c_string = CString::new("Hello from Rust!").expect("CString::new failed");
    
    unsafe {
        // Call C's puts function
        puts(c_string.as_ptr());
        
        // Call C's strlen function
        let length = strlen(c_string.as_ptr());
        println!("String length: {}", length);
    }
}

Exposing Rust Functions to C

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

// Function callable from C
#[no_mangle]
pub extern "C" fn add_numbers(a: c_int, b: c_int) -> c_int {
    a + b
}

#[no_mangle]
pub extern "C" fn process_string(input: *const c_char) -> c_int {
    if input.is_null() {
        return -1;
    }
    
    let c_str = unsafe { CStr::from_ptr(input) };
    match c_str.to_str() {
        Ok(string) => {
            println!("Received from C: {}", string);
            string.len() as c_int
        }
        Err(_) => -1,
    }
}

// Free function for memory allocated by Rust
#[no_mangle]
pub extern "C" fn free_rust_string(ptr: *mut c_char) {
    if !ptr.is_null() {
        unsafe {
            let _ = CString::from_raw(ptr);
        }
    }
}

// Return a string to C (caller must free)
#[no_mangle]
pub extern "C" fn create_greeting(name: *const c_char) -> *mut c_char {
    if name.is_null() {
        return std::ptr::null_mut();
    }
    
    let c_str = unsafe { CStr::from_ptr(name) };
    if let Ok(name_str) = c_str.to_str() {
        let greeting = format!("Hello, {}!", name_str);
        match CString::new(greeting) {
            Ok(c_string) => c_string.into_raw(),
            Err(_) => std::ptr::null_mut(),
        }
    } else {
        std::ptr::null_mut()
    }
}

Working with C Data Types

Primitive Type Mappings

use std::os::raw::*;

// Common C type mappings
fn type_examples() {
    let c_int_val: c_int = 42;              // int
    let c_long_val: c_long = 1000;          // long
    let c_float_val: c_float = 3.14;        // float
    let c_double_val: c_double = 2.718;     // double
    let c_char_val: c_char = b'A' as c_char; // char
    let c_uchar_val: c_uchar = 255;         // unsigned char
    
    println!("C types: int={}, long={}, float={}, double={}, char={}, uchar={}", 
             c_int_val, c_long_val, c_float_val, c_double_val, c_char_val, c_uchar_val);
}

// Size-specific types
fn sized_types() {
    let int8: i8 = -128;
    let uint8: u8 = 255;
    let int16: i16 = -32768;
    let uint16: u16 = 65535;
    let int32: i32 = -2147483648;
    let uint32: u32 = 4294967295;
    let int64: i64 = -9223372036854775808;
    let uint64: u64 = 18446744073709551615;
    
    println!("Sized types defined");
}

Structs and Unions

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

#[repr(C)]
struct Person {
    name: [c_char; 32],
    age: c_int,
    height: c_float,
}

#[repr(C)]
union Data {
    int_val: c_int,
    float_val: c_float,
    bytes: [u8; 4],
}

// Functions that work with C structs
extern "C" {
    fn process_point(point: *const Point) -> c_double;
    fn init_person(person: *mut Person, name: *const c_char, age: c_int, height: c_float);
}

#[no_mangle]
pub extern "C" fn distance_from_origin(point: *const Point) -> c_double {
    if point.is_null() {
        return -1.0;
    }
    
    unsafe {
        let p = &*point;
        (p.x * p.x + p.y * p.y).sqrt()
    }
}

#[no_mangle]
pub extern "C" fn create_point(x: c_double, y: c_double) -> Point {
    Point { x, y }
}

fn main() {
    let point = Point { x: 3.0, y: 4.0 };
    let distance = distance_from_origin(&point);
    println!("Distance from origin: {}", distance);
}

String Handling

C Strings and Rust Strings

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

fn string_conversions() -> Result<(), Box<dyn std::error::Error>> {
    // Rust String to C string
    let rust_string = "Hello, World!";
    let c_string = CString::new(rust_string)?;
    let c_ptr = c_string.as_ptr();
    
    // C string back to Rust
    let back_to_rust = unsafe {
        CStr::from_ptr(c_ptr).to_str()?
    };
    
    println!("Original: {}", rust_string);
    println!("Round trip: {}", back_to_rust);
    
    Ok(())
}

// Safe wrapper for C string functions
fn safe_strlen(s: &str) -> usize {
    let c_string = CString::new(s).expect("CString::new failed");
    unsafe { strlen(c_string.as_ptr()) }
}

// Handle strings with potential null bytes
fn handle_binary_data() {
    let data = b"Hello\0World\0";
    
    // This will truncate at first null
    match CString::new(&data[..]) {
        Ok(c_string) => println!("C string: {:?}", c_string),
        Err(e) => println!("Error creating C string: {}", e),
    }
    
    // Work with the data as-is
    let c_str = unsafe { CStr::from_ptr(data.as_ptr() as *const c_char) };
    if let Ok(rust_str) = c_str.to_str() {
        println!("Converted: {}", rust_str);
    }
}

extern "C" {
    fn strlen(s: *const c_char) -> usize;
}

fn main() {
    string_conversions().unwrap();
    
    let test_string = "Testing strlen";
    println!("Length of '{}': {}", test_string, safe_strlen(test_string));
    
    handle_binary_data();
}

Memory Management Across FFI

Ownership and Allocation

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

// Allocate memory in Rust, free in C
#[no_mangle]
pub extern "C" fn rust_allocate_string(content: *const c_char) -> *mut c_char {
    if content.is_null() {
        return std::ptr::null_mut();
    }
    
    let c_str = unsafe { CStr::from_ptr(content) };
    if let Ok(rust_str) = c_str.to_str() {
        let owned_string = format!("Processed: {}", rust_str);
        match CString::new(owned_string) {
            Ok(c_string) => c_string.into_raw(),
            Err(_) => std::ptr::null_mut(),
        }
    } else {
        std::ptr::null_mut()
    }
}

// Free memory allocated by Rust
#[no_mangle]
pub extern "C" fn rust_free_string(ptr: *mut c_char) {
    if !ptr.is_null() {
        unsafe {
            let _ = CString::from_raw(ptr);
        }
    }
}

// Work with C-allocated memory
extern "C" {
    fn malloc(size: usize) -> *mut c_void;
    fn free(ptr: *mut c_void);
    fn strcpy(dest: *mut c_char, src: *const c_char) -> *mut c_char;
}

fn use_c_malloc() -> Result<(), &'static str> {
    let size = 256;
    let ptr = unsafe { malloc(size) as *mut c_char };
    
    if ptr.is_null() {
        return Err("Allocation failed");
    }
    
    // Use the allocated memory
    let content = CString::new("Hello from C malloc!").unwrap();
    unsafe {
        strcpy(ptr, content.as_ptr());
        
        // Read it back
        let result = CStr::from_ptr(ptr);
        if let Ok(rust_str) = result.to_str() {
            println!("From C memory: {}", rust_str);
        }
        
        // Free the C-allocated memory
        free(ptr as *mut c_void);
    }
    
    Ok(())
}

// RAII wrapper for C resources
struct CBuffer {
    ptr: *mut c_char,
    size: usize,
}

impl CBuffer {
    fn new(size: usize) -> Result<Self, &'static str> {
        let ptr = unsafe { malloc(size) as *mut c_char };
        if ptr.is_null() {
            Err("Allocation failed")
        } else {
            Ok(CBuffer { ptr, size })
        }
    }
    
    fn as_ptr(&self) -> *mut c_char {
        self.ptr
    }
    
    fn write_string(&mut self, s: &str) -> Result<(), &'static str> {
        let c_string = CString::new(s).map_err(|_| "Invalid string")?;
        let bytes = c_string.as_bytes_with_nul();
        
        if bytes.len() > self.size {
            return Err("String too long for buffer");
        }
        
        unsafe {
            std::ptr::copy_nonoverlapping(
                bytes.as_ptr(),
                self.ptr as *mut u8,
                bytes.len(),
            );
        }
        
        Ok(())
    }
}

impl Drop for CBuffer {
    fn drop(&mut self) {
        if !self.ptr.is_null() {
            unsafe { free(self.ptr as *mut c_void); }
        }
    }
}

fn main() {
    use_c_malloc().unwrap();
    
    // Test RAII wrapper
    let mut buffer = CBuffer::new(64).unwrap();
    buffer.write_string("Test content").unwrap();
    
    unsafe {
        let content = CStr::from_ptr(buffer.as_ptr());
        if let Ok(s) = content.to_str() {
            println!("Buffer content: {}", s);
        }
    }
    // Buffer is automatically freed when dropped
}

Using bindgen for C Headers

Generating Rust Bindings

First, add to Cargo.toml:

[build-dependencies]
bindgen = "0.68"

Example C header file (math_lib.h):

// math_lib.h
typedef struct {
    double x;
    double y;
} point_t;

typedef enum {
    RESULT_OK = 0,
    RESULT_ERROR = 1,
    RESULT_INVALID_INPUT = 2
} result_code_t;

double calculate_distance(const point_t* p1, const point_t* p2);
result_code_t normalize_point(point_t* point);
void free_point_array(point_t* points, size_t count);

#define MAX_POINTS 100
#define PI 3.14159265359

Build script (build.rs):

// build.rs
use bindgen;

fn main() {
    println!("cargo:rerun-if-changed=math_lib.h");
    
    let bindings = bindgen::Builder::default()
        .header("math_lib.h")
        .parse_callbacks(Box::new(bindgen::CargoCallbacks))
        .generate()
        .expect("Unable to generate bindings");
    
    let out_path = std::path::PathBuf::from(std::env::var("OUT_DIR").unwrap());
    bindings
        .write_to_file(out_path.join("bindings.rs"))
        .expect("Couldn't write bindings!");
}

Using the generated bindings:

// Include the generated bindings
include!(concat!(env!("OUT_DIR"), "/bindings.rs"));

fn main() {
    let mut p1 = point_t { x: 0.0, y: 0.0 };
    let p2 = point_t { x: 3.0, y: 4.0 };
    
    unsafe {
        let distance = calculate_distance(&p1, &p2);
        println!("Distance: {}", distance);
        
        let result = normalize_point(&mut p1);
        match result {
            result_code_t_RESULT_OK => println!("Point normalized successfully"),
            result_code_t_RESULT_ERROR => println!("Error normalizing point"),
            result_code_t_RESULT_INVALID_INPUT => println!("Invalid input"),
        }
    }
    
    println!("Constants: MAX_POINTS = {}, PI = {}", MAX_POINTS, PI);
}

Using cbindgen for Rust Headers

Generating C Headers from Rust

Add to Cargo.toml:

[build-dependencies]
cbindgen = "0.24"

Rust code to expose:

// lib.rs
use std::os::raw::{c_char, c_int, c_double};

#[repr(C)]
pub struct RustPoint {
    pub x: c_double,
    pub y: c_double,
}

#[repr(C)]
pub enum RustResult {
    Ok = 0,
    Error = 1,
    InvalidInput = 2,
}

/// Calculate the distance between two points
#[no_mangle]
pub extern "C" fn rust_calculate_distance(p1: *const RustPoint, p2: *const RustPoint) -> c_double {
    if p1.is_null() || p2.is_null() {
        return -1.0;
    }
    
    unsafe {
        let point1 = &*p1;
        let point2 = &*p2;
        let dx = point1.x - point2.x;
        let dy = point1.y - point2.y;
        (dx * dx + dy * dy).sqrt()
    }
}

/// Create a new point
#[no_mangle]
pub extern "C" fn rust_create_point(x: c_double, y: c_double) -> RustPoint {
    RustPoint { x, y }
}

/// Process a string and return its length
#[no_mangle]
pub extern "C" fn rust_process_string(input: *const c_char) -> c_int {
    if input.is_null() {
        return -1;
    }
    
    unsafe {
        let c_str = std::ffi::CStr::from_ptr(input);
        match c_str.to_str() {
            Ok(s) => s.len() as c_int,
            Err(_) => -1,
        }
    }
}

Build script to generate header:

// build.rs
extern crate cbindgen;

use std::env;

fn main() {
    let crate_dir = env::var("CARGO_MANIFEST_DIR").unwrap();
    
    cbindgen::Builder::new()
        .with_crate(crate_dir)
        .generate()
        .expect("Unable to generate bindings")
        .write_to_file("rust_math.h");
}

This generates a C header file (rust_math.h):

// Generated by cbindgen
#include <stdarg.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdlib.h>

typedef struct RustPoint {
  double x;
  double y;
} RustPoint;

typedef enum RustResult {
  Ok = 0,
  Error = 1,
  InvalidInput = 2,
} RustResult;

/**
 * Calculate the distance between two points
 */
double rust_calculate_distance(const struct RustPoint *p1, const struct RustPoint *p2);

/**
 * Create a new point
 */
struct RustPoint rust_create_point(double x, double y);

/**
 * Process a string and return its length
 */
int32_t rust_process_string(const char *input);

Error Handling Across FFI

Converting Rust Errors to C Error Codes

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

#[repr(C)]
pub enum ErrorCode {
    Success = 0,
    NullPointer = 1,
    InvalidUtf8 = 2,
    BufferTooSmall = 3,
    OutOfMemory = 4,
    UnknownError = 99,
}

#[no_mangle]
pub extern "C" fn safe_string_copy(
    src: *const c_char,
    dst: *mut c_char,
    dst_size: usize,
) -> ErrorCode {
    if src.is_null() || dst.is_null() {
        return ErrorCode::NullPointer;
    }
    
    if dst_size == 0 {
        return ErrorCode::BufferTooSmall;
    }
    
    let c_str = unsafe { CStr::from_ptr(src) };
    let rust_str = match c_str.to_str() {
        Ok(s) => s,
        Err(_) => return ErrorCode::InvalidUtf8,
    };
    
    if rust_str.len() >= dst_size {
        return ErrorCode::BufferTooSmall;
    }
    
    let c_string = match CString::new(rust_str) {
        Ok(s) => s,
        Err(_) => return ErrorCode::UnknownError,
    };
    
    unsafe {
        std::ptr::copy_nonoverlapping(
            c_string.as_ptr(),
            dst,
            rust_str.len() + 1, // Include null terminator
        );
    }
    
    ErrorCode::Success
}

// Error context for more detailed error information
#[repr(C)]
pub struct ErrorInfo {
    pub code: ErrorCode,
    pub message: [c_char; 256],
}

impl ErrorInfo {
    fn new(code: ErrorCode, message: &str) -> Self {
        let mut error_info = ErrorInfo {
            code,
            message: [0; 256],
        };
        
        if let Ok(c_string) = CString::new(message) {
            let bytes = c_string.as_bytes_with_nul();
            let copy_len = std::cmp::min(bytes.len(), 255);
            unsafe {
                std::ptr::copy_nonoverlapping(
                    bytes.as_ptr(),
                    error_info.message.as_mut_ptr() as *mut u8,
                    copy_len,
                );
            }
        }
        
        error_info
    }
}

#[no_mangle]
pub extern "C" fn complex_operation(
    input: *const c_char,
    error_info: *mut ErrorInfo,
) -> c_int {
    let error_info_ref = if error_info.is_null() {
        return -1;
    } else {
        unsafe { &mut *error_info }
    };
    
    if input.is_null() {
        *error_info_ref = ErrorInfo::new(ErrorCode::NullPointer, "Input pointer is null");
        return -1;
    }
    
    let c_str = unsafe { CStr::from_ptr(input) };
    let rust_str = match c_str.to_str() {
        Ok(s) => s,
        Err(_) => {
            *error_info_ref = ErrorInfo::new(ErrorCode::InvalidUtf8, "Invalid UTF-8 in input string");
            return -1;
        }
    };
    
    if rust_str.is_empty() {
        *error_info_ref = ErrorInfo::new(ErrorCode::BufferTooSmall, "Input string is empty");
        return -1;
    }
    
    // Success
    *error_info_ref = ErrorInfo::new(ErrorCode::Success, "Operation completed successfully");
    rust_str.len() as c_int
}

// Helper function for C code to get error message
#[no_mangle]
pub extern "C" fn get_error_message(error_info: *const ErrorInfo, buffer: *mut c_char, buffer_size: usize) -> ErrorCode {
    if error_info.is_null() || buffer.is_null() || buffer_size == 0 {
        return ErrorCode::NullPointer;
    }
    
    unsafe {
        let info = &*error_info;
        let src_len = info.message.iter().position(|&c| c == 0).unwrap_or(255);
        let copy_len = std::cmp::min(src_len, buffer_size - 1);
        
        std::ptr::copy_nonoverlapping(
            info.message.as_ptr(),
            buffer,
            copy_len,
        );
        
        // Null terminate
        *buffer.add(copy_len) = 0;
    }
    
    ErrorCode::Success
}

Callbacks and Function Pointers

Rust Calling C Callbacks

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

// C function pointer types
type ProgressCallback = extern "C" fn(progress: c_int, user_data: *mut c_void);
type ComparisonCallback = extern "C" fn(a: *const c_void, b: *const c_void) -> c_int;

// Function that takes a callback
#[no_mangle]
pub extern "C" fn process_with_callback(
    data: *const c_int,
    length: usize,
    callback: ProgressCallback,
    user_data: *mut c_void,
) {
    if data.is_null() {
        return;
    }
    
    unsafe {
        for i in 0..length {
            let value = *data.add(i);
            // Process the value (simplified)
            let progress = ((i + 1) * 100 / length) as c_int;
            callback(progress, user_data);
        }
    }
}

// Generic sort function with callback comparison
#[no_mangle]
pub extern "C" fn rust_qsort(
    base: *mut c_void,
    num_elements: usize,
    element_size: usize,
    compare: ComparisonCallback,
) {
    if base.is_null() || num_elements == 0 || element_size == 0 {
        return;
    }
    
    // Simple bubble sort for demonstration
    unsafe {
        for i in 0..num_elements {
            for j in 0..(num_elements - 1 - i) {
                let elem1 = (base as *mut u8).add(j * element_size) as *const c_void;
                let elem2 = (base as *mut u8).add((j + 1) * element_size) as *const c_void;
                
                if compare(elem1, elem2) > 0 {
                    // Swap elements
                    for k in 0..element_size {
                        let byte1_ptr = (base as *mut u8).add(j * element_size + k);
                        let byte2_ptr = (base as *mut u8).add((j + 1) * element_size + k);
                        let temp = *byte1_ptr;
                        *byte1_ptr = *byte2_ptr;
                        *byte2_ptr = temp;
                    }
                }
            }
        }
    }
}

// Example usage with Rust closures
extern "C" fn progress_printer(progress: c_int, _user_data: *mut c_void) {
    println!("Progress: {}%", progress);
}

extern "C" fn int_compare(a: *const c_void, b: *const c_void) -> c_int {
    unsafe {
        let int_a = *(a as *const c_int);
        let int_b = *(b as *const c_int);
        int_a - int_b
    }
}

fn main() {
    let data = [5, 2, 8, 1, 9, 3];
    process_with_callback(
        data.as_ptr(),
        data.len(),
        progress_printer,
        std::ptr::null_mut(),
    );
    
    let mut numbers = [64, 34, 25, 12, 22, 11, 90];
    rust_qsort(
        numbers.as_mut_ptr() as *mut c_void,
        numbers.len(),
        std::mem::size_of::<c_int>(),
        int_compare,
    );
    println!("Sorted: {:?}", numbers);
}

Best Practices for FFI

1. Safety and Error Handling

use std::panic;

// Prevent panics from crossing FFI boundaries
#[no_mangle]
pub extern "C" fn safe_rust_function(input: *const c_char) -> c_int {
    let result = panic::catch_unwind(|| {
        if input.is_null() {
            return -1;
        }
        
        // Your actual logic here
        unsafe {
            let c_str = CStr::from_ptr(input);
            match c_str.to_str() {
                Ok(s) => s.len() as c_int,
                Err(_) => -1,
            }
        }
    });
    
    match result {
        Ok(value) => value,
        Err(_) => -1, // Return error code instead of unwinding
    }
}

2. Documentation and Examples

/// Calculate the factorial of a number
/// 
/// # Arguments
/// * `n` - The input number (must be non-negative)
/// 
/// # Returns
/// * The factorial of n, or -1 if n is negative or overflow occurs
/// 
/// # Safety
/// This function is safe to call from C code.
/// 
/// # Example
/// ```c
/// #include "math_lib.h"
/// 
/// int main() {
///     long result = rust_factorial(5);
///     printf("5! = %ld\n", result);
///     return 0;
/// }
/// ```
#[no_mangle]
pub extern "C" fn rust_factorial(n: c_int) -> c_long {
    if n < 0 {
        return -1;
    }
    
    if n > 20 {
        return -1; // Prevent overflow
    }
    
    let mut result: c_long = 1;
    for i in 1..=n {
        result = match result.checked_mul(i as c_long) {
            Some(val) => val,
            None => return -1, // Overflow
        };
    }
    
    result
}

3. Testing FFI Code

#[cfg(test)]
mod tests {
    use super::*;
    use std::ffi::CString;
    
    #[test]
    fn test_safe_string_copy() {
        let src = CString::new("Hello, World!").unwrap();
        let mut dst = [0i8; 20];
        
        let result = safe_string_copy(
            src.as_ptr(),
            dst.as_mut_ptr(),
            dst.len(),
        );
        
        assert_eq!(result as i32, ErrorCode::Success as i32);
        
        let result_str = unsafe { CStr::from_ptr(dst.as_ptr()) };
        assert_eq!(result_str.to_str().unwrap(), "Hello, World!");
    }
    
    #[test]
    fn test_null_pointer_handling() {
        let mut dst = [0i8; 20];
        
        let result = safe_string_copy(
            std::ptr::null(),
            dst.as_mut_ptr(),
            dst.len(),
        );
        
        assert_eq!(result as i32, ErrorCode::NullPointer as i32);
    }
    
    #[test]
    fn test_factorial() {
        assert_eq!(rust_factorial(0), 1);
        assert_eq!(rust_factorial(5), 120);
        assert_eq!(rust_factorial(-1), -1);
        assert_eq!(rust_factorial(25), -1); // Overflow protection
    }
}

4. Build Configuration

Example Cargo.toml for FFI projects:

[package]
name = "rust_ffi_lib"
version = "0.1.0"
edition = "2021"

[lib]
name = "rust_ffi_lib"
crate-type = ["cdylib", "staticlib"]

[dependencies]
libc = "0.2"

[build-dependencies]
cbindgen = "0.24"
bindgen = "0.68"

[package.metadata.capi.header]
name = "rust_ffi_lib"
subdirectory = false
generation = true

FFI enables powerful interoperability between Rust and C, allowing you to leverage existing C libraries and expose Rust functionality to other languages. Always prioritize safety, proper error handling, and thorough testing when working across language boundaries. The bindgen and cbindgen tools can significantly simplify the process of generating bindings and headers.