Ffi
Foreign function interfaces for calling between C, C++, Rust, Zig, Python, and other languages safely
You are an expert in foreign function interfaces (FFI) for bridging systems-level code across language boundaries. ## Key Points - Always use `#[repr(C)]` in Rust or `packed struct` / `extern struct` in Zig for types crossing FFI boundaries. - Document memory ownership explicitly: who allocates, who frees, using which allocator. - Never throw C++ exceptions across `extern "C"` boundaries; catch and convert to error codes. - Use opaque handle types (forward-declared structs) to hide implementation details across the C boundary. - Pin callback user data so it is not moved or freed while the foreign library holds a pointer. - Test FFI code under AddressSanitizer to catch memory errors at the boundary. - Passing Rust `String` or `Vec` directly across FFI; they have non-C-compatible layouts. Use `CString` and raw pointers. - Freeing memory with the wrong allocator (e.g., calling `free()` on memory allocated by Rust's allocator). - Forgetting that C strings are null-terminated but Rust/Zig slices are not; conversion must add/remove the null. - Struct padding and alignment differences between languages; always use `#[repr(C)]` or equivalent. - Passing stack-allocated data to a foreign function that stores the pointer for later use (dangling pointer). - Thread safety assumptions: C libraries may not be thread-safe even if the calling language uses threads freely. ## Quick Example ```c // C header typedef void (*LogCallback)(int level, const char *message, void *user_data); void set_log_callback(LogCallback cb, void *user_data); ```
skilldb get systems-programming-skills/FfiFull skill: 302 linesForeign Function Interfaces — Systems Programming
You are an expert in foreign function interfaces (FFI) for bridging systems-level code across language boundaries.
Core Philosophy
Overview
FFI enables code written in one language to call functions in another. The C ABI is the universal lingua franca: nearly every language can call C functions and expose C-compatible interfaces. FFI involves managing calling conventions, data layout, memory ownership, error handling across boundaries, and build system integration. Getting any of these wrong leads to crashes, memory corruption, or undefined behavior.
Core Concepts
The C ABI as Common Ground
All cross-language FFI ultimately passes through C-compatible function signatures and data layouts.
// shared_lib.h - The C interface contract
#ifndef SHARED_LIB_H
#define SHARED_LIB_H
#include <stdint.h>
#include <stddef.h>
#ifdef __cplusplus
extern "C" {
#endif
typedef struct {
const char *name;
uint32_t age;
} Person;
// Caller-allocated buffer pattern
int32_t person_to_json(const Person *p, char *buf, size_t buf_size);
// Library-allocated, library-freed pattern
char *person_to_json_alloc(const Person *p);
void free_string(char *s);
#ifdef __cplusplus
}
#endif
#endif
Rust FFI
Exposing Rust functions to C:
use std::ffi::{CStr, CString};
use std::os::raw::c_char;
#[repr(C)]
pub struct Point {
pub x: f64,
pub y: f64,
}
#[no_mangle]
pub extern "C" fn point_distance(a: *const Point, b: *const Point) -> f64 {
let a = unsafe { &*a };
let b = unsafe { &*b };
((a.x - b.x).powi(2) + (a.y - b.y).powi(2)).sqrt()
}
#[no_mangle]
pub extern "C" fn greet(name: *const c_char) -> *mut c_char {
let name = unsafe { CStr::from_ptr(name) }.to_str().unwrap_or("unknown");
let greeting = format!("Hello, {name}!");
CString::new(greeting).unwrap().into_raw()
}
#[no_mangle]
pub extern "C" fn free_rust_string(s: *mut c_char) {
if !s.is_null() {
unsafe { drop(CString::from_raw(s)); }
}
}
Calling C from Rust:
// build.rs
fn main() {
println!("cargo:rustc-link-lib=sodium");
println!("cargo:rustc-link-search=/usr/local/lib");
}
// src/lib.rs
extern "C" {
fn crypto_secretbox_keygen(key: *mut u8);
fn crypto_secretbox_easy(
c: *mut u8,
m: *const u8,
mlen: u64,
n: *const u8,
k: *const u8,
) -> i32;
}
pub fn generate_key() -> [u8; 32] {
let mut key = [0u8; 32];
unsafe { crypto_secretbox_keygen(key.as_mut_ptr()); }
key
}
Zig FFI
Zig has first-class C interop with no bindings generation needed:
const c = @cImport({
@cInclude("sqlite3.h");
});
pub fn openDatabase(path: [*:0]const u8) !*c.sqlite3 {
var db: ?*c.sqlite3 = null;
const rc = c.sqlite3_open(path, &db);
if (rc != c.SQLITE_OK) {
if (db) |d| c.sqlite3_close(d);
return error.SqliteOpenFailed;
}
return db.?;
}
// Exposing Zig to C
export fn zig_add(a: i32, b: i32) i32 {
return a + b;
}
Python ctypes and cffi
# Using ctypes for simple C calls
import ctypes
lib = ctypes.CDLL("./libmath.so")
# Define argument and return types
lib.point_distance.argtypes = [
ctypes.POINTER(ctypes.c_double * 2),
ctypes.POINTER(ctypes.c_double * 2),
]
lib.point_distance.restype = ctypes.c_double
# Using cffi for more complex interfaces
from cffi import FFI
ffi = FFI()
ffi.cdef("""
typedef struct {
const char *name;
uint32_t age;
} Person;
char *person_to_json_alloc(const Person *p);
void free_string(char *s);
""")
lib = ffi.dlopen("./libperson.so")
person = ffi.new("Person *", {b"name": b"Alice", "age": 30})
json_ptr = lib.person_to_json_alloc(person)
json_str = ffi.string(json_ptr).decode("utf-8")
lib.free_string(json_ptr) # Must free with the library's allocator
C++ to C Wrapper Pattern
// engine.hpp - C++ API
class Engine {
public:
Engine(const Config& config);
~Engine();
Result process(std::span<const uint8_t> input);
private:
struct Impl;
std::unique_ptr<Impl> impl_;
};
// engine_c.h - C wrapper
typedef struct EngineHandle EngineHandle;
EngineHandle *engine_create(const char *config_json);
void engine_destroy(EngineHandle *engine);
int32_t engine_process(EngineHandle *engine, const uint8_t *data,
size_t len, uint8_t *out, size_t *out_len);
// engine_c.cpp - Implementation
extern "C" {
EngineHandle *engine_create(const char *config_json) {
try {
auto config = Config::from_json(config_json);
return reinterpret_cast<EngineHandle*>(new Engine(config));
} catch (...) {
return nullptr;
}
}
void engine_destroy(EngineHandle *engine) {
delete reinterpret_cast<Engine*>(engine);
}
int32_t engine_process(EngineHandle *engine, const uint8_t *data,
size_t len, uint8_t *out, size_t *out_len) {
try {
auto* eng = reinterpret_cast<Engine*>(engine);
auto result = eng->process({data, len});
if (result.size() > *out_len) return -1;
std::memcpy(out, result.data(), result.size());
*out_len = result.size();
return 0;
} catch (...) {
return -1;
}
}
} // extern "C"
Implementation Patterns
Callback Functions Across FFI
// C header
typedef void (*LogCallback)(int level, const char *message, void *user_data);
void set_log_callback(LogCallback cb, void *user_data);
// Rust: wrapping a Rust closure as a C callback
extern "C" fn log_trampoline(level: i32, msg: *const c_char, user_data: *mut std::ffi::c_void) {
let closure: &mut Box<dyn FnMut(i32, &str)> = unsafe { &mut *(user_data as *mut _) };
let msg = unsafe { CStr::from_ptr(msg) }.to_str().unwrap_or("");
closure(level, msg);
}
pub fn set_logger(mut callback: impl FnMut(i32, &str) + 'static) {
let boxed: Box<Box<dyn FnMut(i32, &str)>> = Box::new(Box::new(callback));
let raw = Box::into_raw(boxed) as *mut std::ffi::c_void;
unsafe { set_log_callback(log_trampoline, raw); }
// Note: must ensure raw is eventually freed (e.g., on unset)
}
Error Handling Across Boundaries
// Pattern: error code + thread-local error message
typedef enum {
LIB_OK = 0,
LIB_ERR_INVALID_ARG = -1,
LIB_ERR_IO = -2,
LIB_ERR_INTERNAL = -3,
} LibError;
LibError lib_do_work(const uint8_t *data, size_t len);
const char *lib_last_error(void); // Returns thread-local error string
Best Practices
- Always use
#[repr(C)]in Rust orpacked struct/extern structin Zig for types crossing FFI boundaries. - Document memory ownership explicitly: who allocates, who frees, using which allocator.
- Never throw C++ exceptions across
extern "C"boundaries; catch and convert to error codes. - Use opaque handle types (forward-declared structs) to hide implementation details across the C boundary.
- Pin callback user data so it is not moved or freed while the foreign library holds a pointer.
- Test FFI code under AddressSanitizer to catch memory errors at the boundary.
Common Pitfalls
- Passing Rust
StringorVecdirectly across FFI; they have non-C-compatible layouts. UseCStringand raw pointers. - Freeing memory with the wrong allocator (e.g., calling
free()on memory allocated by Rust's allocator). - Forgetting that C strings are null-terminated but Rust/Zig slices are not; conversion must add/remove the null.
- Struct padding and alignment differences between languages; always use
#[repr(C)]or equivalent. - Passing stack-allocated data to a foreign function that stores the pointer for later use (dangling pointer).
- Thread safety assumptions: C libraries may not be thread-safe even if the calling language uses threads freely.
Anti-Patterns
Over-engineering for hypothetical scale. Building for millions of users when you have hundreds adds complexity without value. Solve today's problems first.
Ignoring the existing ecosystem. Reinventing functionality that mature libraries already provide well wastes time and introduces unnecessary risk.
Premature abstraction. Creating elaborate frameworks and utilities before you have enough concrete cases to know what the abstraction should look like produces the wrong abstraction.
Neglecting error handling at boundaries. Internal code can trust its inputs, but system boundaries (user input, APIs, file I/O) require defensive validation.
Skipping documentation for obvious code. What is obvious to you today will not be obvious to your colleague next month or to you next year.
Install this skill directly: skilldb add systems-programming-skills
Related Skills
Build Systems
Build systems for systems programming including CMake, Meson, and Zig build with cross-compilation
Cpp Modern
Modern C++ (C++20/23) patterns including concepts, ranges, coroutines, and modules
Memory Safety
Techniques for achieving memory safety without garbage collection including ownership models, arenas, and static analysis
Zig Basics
Zig language fundamentals for systems programming including comptime, error handling, and manual memory management
Adversarial Code Review
Adversarial implementation review methodology that validates code completeness against requirements with fresh objectivity. Uses a coach-player dialectical loop to catch real gaps in security, logic, and data flow.
API Design Testing
Design, document, and test APIs following RESTful principles, consistent