Foreign Function Interface (FFI)

FFI and Ownership

Because C has no compile-time ownership tracker, crossing an FFI boundary requires an explicit ownership and allocator contract for every pointer: both sides must agree on whether a pointer is borrowed for the duration of a call or transferred ("moved") to the receiver, and the side that allocates heap memory must always be the side that deallocates it.

Rust-to-C Handle Lifecycle (`Box::into_raw` / `Box::from_raw`):

  1. Create: `Box::into_raw(Box::new(Engine::new()))` ---> Returns `*mut Engine` to C
     (Rust relinquishes ownership; `Engine` stays alive on Rust heap)
           |
           v
  2. Use:    C passes `*mut Engine` back into `engine_process(ptr, ...)`
     (Rust temporarily borrows `&mut *ptr` for the duration of the call)
           |
           v
  3. Free:   C calls `engine_destroy(ptr)` ---> `unsafe { drop(Box::from_raw(ptr)) }`
     (Rust reclaims ownership, runs `Engine::drop`, and frees Rust heap memory!)

Golden Rule of FFI Memory Management: Never Mix Allocators!

Allocation Origin Deallocation Function Why
Rust Box::into_raw(b) unsafe { drop(Box::from_raw(ptr)) } Rust's global allocator (System, jemalloc, mimalloc, or Windows HeapAlloc) tracks size and alignment layout (core::alloc::Layout). Passing ptr to C's free() is Undefined Behavior!
Rust CString::into_raw(s) unsafe { drop(CString::from_raw(ptr)) } CString stores allocation layout metadata incompatible with C free().
Rust Arc::into_raw(a) unsafe { drop(Arc::from_raw(ptr)) } Decrements the atomic reference count and frees only when count reaches 0.
C malloc / calloc C free(ptr) (or library-specific foo_free(ptr)) Must be returned to the exact C runtime heap that allocated it.

Implementing an Opaque Handle Pattern (create, use, destroy)

The cleanest way to expose a complex Rust struct (containing Vec, String, or HashMap fields) to C is the opaque handle pattern: C only ever sees a typed pointer (typedef struct KvEngine KvEngine;), while Rust manages construction, borrowing, and destruction through Box::into_raw and Box::from_raw:

use std::collections::HashMap;

pub struct KvEngine {
    entries: HashMap<u32, u64>,
}

/// Allocates a new `KvEngine` on the Rust heap and transfers ownership to C.
#[unsafe(no_mangle)]
pub extern "C" fn kv_engine_create() -> *mut KvEngine {
    let engine = Box::new(KvEngine {
        entries: HashMap::new(),
    });
    Box::into_raw(engine)
}

/// Inserts a key-value pair into the borrowed `KvEngine`.
///
/// # Safety
///
/// `engine_ptr` must be a valid, non-null, unaliased pointer returned by
/// `kv_engine_create` that has not yet been passed to `kv_engine_destroy`.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn kv_engine_insert(
    engine_ptr: Option<&mut KvEngine>,
    key: u32,
    value: u64,
) -> i32 {
    let Some(engine) = engine_ptr else {
        return -1;
    };
    engine.entries.insert(key, value);
    0
}

/// Looks up `key` in the borrowed `KvEngine`.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn kv_engine_get(
    engine_ptr: Option<&KvEngine>,
    key: u32,
    out_val: Option<&mut u64>,
) -> i32 {
    let (Some(engine), Some(out)) = (engine_ptr, out_val) else {
        return -1;
    };
    match engine.entries.get(&key) {
        Some(&v) => {
            *out = v;
            0
        }
        None => 1, // Not found
    }
}

/// Reclaims ownership of `engine_ptr` from C and drops the `KvEngine`.
///
/// # Safety
///
/// `engine_ptr` must either be null or a pointer returned by `kv_engine_create`
/// that has not yet been destroyed.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn kv_engine_destroy(engine_ptr: *mut KvEngine) {
    if !engine_ptr.is_null() {
        // SAFETY: Reconstructs the `Box<KvEngine>` and drops it immediately.
        unsafe {
            drop(Box::from_raw(engine_ptr));
        }
    }
}

fn main() {
    let handle = kv_engine_create();
    assert!(!handle.is_null());

    unsafe {
        assert_eq!(kv_engine_insert(handle.as_mut(), 42, 9999), 0);

        let mut fetched = 0u64;
        assert_eq!(kv_engine_get(handle.as_ref(), 42, Some(&mut fetched)), 0);
        println!("Fetched from opaque handle: key 42 -> {fetched}");

        kv_engine_destroy(handle);
    }
}
Fetched from opaque handle: key 42 -> 9999