Foreign Function Interface (FFI)
On this page 37
Home's FFI system enables seamless interoperability with C, C++, and other languages. It provides safe abstractions over foreign code while maintaining Home's safety guarantees at the boundary.
Overview
Home FFI offers:
- C ABI compatibility: Direct calling of C functions
- Safe wrappers: Type-safe interfaces over unsafe foreign code
- Bidirectional bindings: Call foreign code and expose Home functions
- Automatic binding generation: Generate bindings from C headers
- Platform-specific linking: Configure library linking per platform
Basic C FFI
Declaring External Functions
// Declare external C functions
extern "C" {
fn puts(s: *const c*char) -> c*int
fn printf(format: *const c*char, ...) -> c*int
fn malloc(size: usize) -> *mut void
fn free(ptr: *mut void)
}
// Usage
fn main() {
unsafe {
let msg = c"Hello from Home!"
puts(msg)
}
}
Linking Libraries
// Link to system library
# [link(name = "m")]
extern "C" {
fn sin(x: f64) -> f64
fn cos(x: f64) -> f64
fn sqrt(x: f64) -> f64
}
// Link to static library
# [link(name = "mylib", kind = "static")]
extern "C" {
fn custom*function(x: i32) -> i32
}
// Link to dynamic library
# [link(name = "openssl")]
extern "C" {
fn SSL*library*init() -> c*int
}
C Type Mappings
// C-compatible type aliases
type c*char = i8
type c*short = i16
type c*int = i32
type c*long = i64 // Platform-dependent in C, fixed in Home
type c*float = f32
type c*double = f64
type c*void = void
type size*t = usize
type ssize*t = isize
// Pointer types
type c*str = *const c*char
type c*str*mut = *mut c*char
Struct Layout
C-Compatible Structs
// Ensure C-compatible layout
# [repr(C)]
struct Point {
x: f64,
y: f64,
}
# [repr(C)]
struct Rectangle {
origin: Point,
size: Point,
}
extern "C" {
fn draw*rectangle(rect: *const Rectangle)
}
fn main() {
let rect = Rectangle {
origin: Point { x: 0.0, y: 0.0 },
size: Point { x: 100.0, y: 50.0 },
}
unsafe {
draw*rectangle(&rect)
}
}
Packed Structs
// Remove padding
# [repr(C, packed)]
struct PackedData {
flag: u8,
value: u32,
// No padding between flag and value
}
// Specify alignment
# [repr(C, align(16))]
struct AlignedData {
data: [f32; 4],
}
Union Types
# [repr(C)]
union Value {
i: i64,
f: f64,
p: *mut void,
}
fn use*union() {
let mut v = Value { i: 42 }
unsafe {
print("as int: {}", v.i)
v.f = 3.14
print("as float: {}", v.f)
}
}
Callbacks and Function Pointers
Passing Callbacks to C
// C function type
type CCallback = extern "C" fn(data: *mut void, value: i32) -> i32
extern "C" {
fn register*callback(cb: CCallback, data: *mut void)
fn trigger*callbacks()
}
// Home callback with C calling convention
extern "C" fn my*callback(data: *mut void, value: i32) -> i32 {
let counter = unsafe { &mut *(data as *mut i32) }
*counter += value
*counter
}
fn main() {
let mut counter = 0i32
unsafe {
register*callback(my*callback, &mut counter as *mut i32 as *mut void)
trigger*callbacks()
print("Counter: {counter}")
}
}
Closures as Callbacks
// Wrapper for closure-based callbacks
struct CallbackWrapper<F> {
callback: F,
}
extern "C" fn trampoline<F: Fn(i32) -> i32>(data: *mut void, value: i32) -> i32 {
let wrapper = unsafe { &*(data as *const CallbackWrapper<F>) }
(wrapper.callback)(value)
}
fn with*callback<F: Fn(i32) -> i32>(callback: F) {
let wrapper = CallbackWrapper { callback }
unsafe {
register*callback(
trampoline::<F>,
&wrapper as *const * as *mut void
)
trigger*callbacks()
}
}
String Handling
C Strings
use std.ffi.{CString, CStr}
fn c*string*example() {
// Create C string from Home string
let home*str = "Hello, World!"
let c*str = CString.new(home*str).unwrap()
unsafe {
puts(c*str.as*ptr())
}
// Convert C string back to Home string
unsafe {
let ptr = get*c*string() // Returns *const c*char
let c*str = CStr.from*ptr(ptr)
let home*str = c*str.to*string()
}
}
String Literals
// C string literals
let c*literal = c"This is a C string\0" // *const c*char
// Wide string literals
let wide*literal = w"Wide string" // *const wchar*t
// UTF-16 literals (for Windows)
let utf16*literal = u16"UTF-16 string" // *const u16
Memory Management
Ownership Across FFI
extern "C" {
// C allocates, we must free
fn create*buffer(size: usize) -> *mut u8
fn destroy*buffer(ptr: *mut u8)
// We allocate, C uses
fn process*data(data: *const u8, len: usize)
}
fn safe*buffer*usage() {
// RAII wrapper for C-allocated memory
struct CBuffer {
ptr: *mut u8,
len: usize,
}
impl CBuffer {
fn new(size: usize) -> Self {
let ptr = unsafe { create*buffer(size) }
CBuffer { ptr, len: size }
}
}
impl Drop for CBuffer {
fn drop(mut self) {
unsafe { destroy*buffer(self.ptr) }
}
}
let buffer = CBuffer.new(1024)
// Automatically freed when buffer goes out of scope
}
Box and FFI
extern "C" {
fn store*data(data: *mut Data)
fn retrieve*data() -> *mut Data
}
fn box*ffi() {
// Pass owned data to C
let data = Box.new(Data { value: 42 })
unsafe {
store*data(Box.into*raw(data))
}
// Retrieve and take ownership back
unsafe {
let ptr = retrieve*data()
let data = Box.from*raw(ptr)
// data is now managed by Home again
}
}
Error Handling
C Error Codes
extern "C" {
fn open*file(path: *const c*char) -> c*int
fn get*last*error() -> c*int
fn error*message(code: c*int) -> *const c*char
}
fn safe*open(path: &str) -> Result<FileHandle, Error> {
let c*path = CString.new(path)?
let fd = unsafe { open*file(c*path.as*ptr()) }
if fd < 0 {
let code = unsafe { get*last*error() }
let msg = unsafe {
CStr.from*ptr(error*message(code)).to*string()
}
Err(Error.new(msg))
} else {
Ok(FileHandle { fd })
}
}
errno Handling
use std.ffi.errno
fn with*errno<T>(f: fn() -> T, success: fn(T) -> bool) -> Result<T, Error> {
errno.set(0)
let result = f()
if success(result) {
Ok(result)
} else {
let code = errno.get()
Err(Error.from*errno(code))
}
}
Opaque Types
Working with Opaque Pointers
// Opaque type (size unknown)
# [repr(C)]
struct OpaqueHandle {
*private: [u8; 0],
}
extern "C" {
fn create*handle() -> *mut OpaqueHandle
fn use*handle(handle: *mut OpaqueHandle)
fn destroy*handle(handle: *mut OpaqueHandle)
}
// Safe wrapper
struct Handle {
raw: *mut OpaqueHandle,
}
impl Handle {
fn new() -> Self {
Handle { raw: unsafe { create*handle() } }
}
fn use*it(&self) {
unsafe { use*handle(self.raw) }
}
}
impl Drop for Handle {
fn drop(mut self) {
unsafe { destroy*handle(self.raw) }
}
}
Platform-Specific Code
Conditional Compilation
# [cfg(target*os = "windows")]
# [link(name = "kernel32")]
extern "C" {
fn GetLastError() -> u32
fn SetLastError(code: u32)
}
# [cfg(target*os = "linux")]
# [link(name = "pthread")]
extern "C" {
fn pthread*create(...) -> c*int
fn pthread*join(...) -> c*int
}
# [cfg(target*os = "macos")]
# [link(name = "System")]
extern "C" {
fn dispatch*async(...)
}
Platform-Specific Types
# [cfg(target*os = "windows")]
type RawHandle = *mut void
# [cfg(target*os = "windows")]
const INVALID*HANDLE: RawHandle = -1 as *mut void
# [cfg(unix)]
type RawFd = c*int
# [cfg(unix)]
const INVALID*FD: RawFd = -1
Automatic Binding Generation
Using bindgen
// build.home
fn main() {
bindgen.builder()
.header("wrapper.h")
.allowlist*function("mylib*.*")
.allowlist*type("MyLib.*")
.generate()
.write*to*file("src/bindings.home")
}
// Generates type-safe bindings from C headers
Inline Headers
# [ffi*header(r#"
typedef struct {
int x;
int y;
} Point;
Point* create*point(int x, int y);
void destroy*point(Point* p);
"#)]
mod c*bindings {}
// Automatically generates Home bindings
Exporting Home Functions
C-Callable Functions
// Export function with C ABI
# [no*mangle]
pub extern "C" fn home*add(a: i32, b: i32) -> i32 {
a + b
}
// Export with custom name
# [no*mangle]
# [export*name = "calculate"]
pub extern "C" fn home*calculate(x: f64) -> f64 {
x * x + 2.0 * x + 1.0
}
Creating Shared Libraries
// lib.home
# [no*mangle]
pub extern "C" fn library*init() -> c*int {
// Initialize library
0
}
# [no*mangle]
pub extern "C" fn library*process(data: *const u8, len: usize) -> *mut u8 {
// Process data
}
# [no*mangle]
pub extern "C" fn library*cleanup() {
// Cleanup
}
Edge Cases
Varargs Functions
extern "C" {
fn printf(format: *const c*char, ...) -> c*int
}
fn call*printf() {
unsafe {
printf(c"Integer: %d, Float: %f\n", 42i32, 3.14f64)
}
}
Bitfields
// Bitfields require manual handling
# [repr(C)]
struct Flags {
bits: u32,
}
impl Flags {
fn flag*a(self) -> bool { (self.bits & 0x1) != 0 }
fn flag*b(self) -> bool { (self.bits & 0x2) != 0 }
fn set*flag*a(mut self, val: bool) {
if val { self.bits |= 0x1 } else { self.bits &= !0x1 }
}
}
Callbacks with Lifetime Issues
// Be careful with callback lifetimes
extern "C" {
fn async*operation(callback: extern "C" fn(*mut void), data: *mut void)
}
fn dangerous*example() {
let local*data = 42
// WRONG: local*data may be gone when callback fires
// async*operation(my*callback, &local*data as *const * as *mut void)
// CORRECT: Use heap allocation
let boxed = Box.new(42)
async*operation(my*callback, Box.into*raw(boxed) as *mut void)
}
Best Practices
-
Wrap unsafe FFI in safe interfaces:
// Internal unsafe implementation mod ffi { extern "C" { pub fn dangerous*function(ptr: *mut u8) } } // Safe public API pub fn safe*function(data: &mut [u8]) { unsafe { ffi.dangerous*function(data.as*mut*ptr()) } } -
Document safety requirements:
/// Calls the C function `process*buffer`. /// /// # Safety /// - `ptr` must be valid for reads of `len` bytes /// - `ptr` must be properly aligned /// - The memory must not be mutated during this call unsafe fn process(ptr: *const u8, len: usize) { ffi.process*buffer(ptr, len) } -
Use RAII for resource management:
struct CResource { handle: *mut void, } impl Drop for CResource { fn drop(mut self) { if !self.handle.is*null() { unsafe { free*resource(self.handle) } } } } -
Validate at the FFI boundary:
#[no*mangle] pub extern "C" fn api*function(ptr: *const u8, len: usize) -> c*int { if ptr.is*null() { return -1 // Error code } let slice = unsafe { std.slice.from*raw*parts(ptr, len) } match internal*function(slice) { Ok(*) => 0, Err(*) => -2, } } -
Test FFI code thoroughly:
#[test] fn test*c*interop() { let data = [1u8, 2, 3, 4] let result = unsafe { c*function(data.as*ptr(), data.len()) } assert(result == expected_value) }