Rust FFI Omnibus
repository·master·Indexed 19 days ago
https://github.com/shepmaster/rust-ffi-omnibusA collection of examples and a specialized toolkit for creating Foreign Function Interfaces (FFI) in Rust. It provides standardized implementations for common data patterns—including integers, objects, slices, strings, tuples, and vectors—to facilitate safe data exchange between Rust and other languages such as C, Ruby, Python, Haskell, Node.js, C#, and Julia.
What's inside rust-ffi-omnibus
- The Rust FFI Omnibus is a collection of tools and patterns designed to facilitate Foreign Function Interface (FFI) operations between Rust and other languages. It provides standardized ways to handle common data types and return patterns across the FFI boundary.
How to return allocated strings from Rust via FFI
masterReturning an allocated string from Rust to another language is complex because the Rust allocator may differ from the caller's allocator. To safely return a string, you must transfer ownership to the caller via a raw pointer, and the caller must eventually return that pointer to Rust to ensure the memory is properly deallocated.
In Rust, use
CString::into_rawto convert aCStringinto a raw pointer for FFI. To reclaim ownership and deallocate the memory later, the caller must pass that pointer back to a Rust function that usesCString::from_raw.// Rust side logic pattern // 1. Create CString // 2. Call .into_raw() to pass ownership to caller // 3. Caller must eventually pass pointer back to Rust to call CString::from_raw()How opaque objects work across FFI
masterAn opaque object (or opaque pointer) is a pattern used to pass complex Rust structures to other languages without exposing their internal memory layout. Instead of sharing the struct definition, you pass a raw pointer to the memory address where the object lives on the heap.
Core Principles:
- Stability: The Rust object must be placed on the heap (e.g., using
Box::into_raw) to ensure it has a stable memory address that remains valid across FFI calls. - Namespacing: Since C-style FFI lacks native namespacing, functions should be prefixed with a package or type name (e.g.,
zip_code_database_...) to avoid collisions. - Ownership & Deallocation: Memory allocated by Rust must be deallocated by Rust. You must provide a specific deallocation function that uses
Box::from_rawto reclaim the pointer and allow Rust to drop the object. - Safety: The client language is responsible for ensuring pointers are not
NULLbefore use and for calling the deallocation function exactly once to prevent memory leaks or double-frees.
// Rust side pattern #[no_mangle] pub extern "C" fn zip_code_database_new() -> *mut ZipCodeDatabase { Box::into_raw(Box::new(ZipCodeDatabase::new())) } #[no_mangle] pub extern "C" fn zip_code_database_free(ptr: *mut ZipCodeDatabase) { if !ptr.is_null() { unsafe { Box::from_raw(ptr); } } }- Stability: The Rust object must be placed on the heap (e.g., using
Use allocated strings in C#
masterIn C#, wrap the Rust string in a subclass of
SafeHandle. Use a wrapper class to ensure the handle is disposed of properly.Note: Since C# does not have a native way to read a pointer directly as a UTF-8 string (it supports ANSI and UCS-2/Unicode), you must implement a custom method to read the pointer as UTF-8.
// C# pattern // Wrap in SafeHandle and use a wrapper to ensure disposalLocate Rust dynamic libraries when running examples
masterWhen calling Rust FFI functions from other languages, the system must be able to locate the compiled dynamic library. The method for doing this depends on your operating system:
macOS and Linux
Prefix your execution command with
LD_LIBRARY_PATHpointing to the directory containing your compiled library (typicallytarget/debug).Note: On macOS, System Integrity Protection (SIP) may prevent setting
LD_LIBRARY_PATHfor system-provided binaries. You may need to use a different binary or disable SIP.Windows
Copy the compiled
.dllfile directly into the current working directory before running your application.# macOS/Linux example LD_LIBRARY_PATH=target/debug python src/main.pyCompile the Rust integer library
masterTo use the integer library, compile the Rust source using
cargo build. This generates a dynamic library intarget/debug/. The filename pattern varies by platform:- Windows:
*.dll - macOS:
lib*.dylib - Linux:
lib*.so
cargo build- Windows:
Use allocated strings in Julia
masterIn Julia, use the
Cstringdata type to represent the NUL-terminated string. To avoid managing the Rust-allocated memory directly in Julia, useunsafe_stringto construct a copy of the string that is managed by Julia's garbage collector, then transfer the original Rust pointer back to Rust to be freed.# Julia pattern ptr = get_string_ffi() if ptr != C_NULL str = unsafe_string(ptr) free_rust_string(ptr) endUse allocated strings in Haskell
masterWhen calling the FFI method in Haskell, first check if the returned pointer is
NULL. If it is notNULL, usepeekCStringto convert the pointer into a HaskellString, and then immediately call the Rust function to free the string.-- Haskell pattern ptr <- get_string_ffi if ptr == nullPtr then return "" else do str <- peekCString ptr free_rust_string ptr return strImplement opaque objects in Node.js
masterIn Node.js, wrap the FFI functions in a class to provide an idiomatic JavaScript API (e.g., camelCase) and manage the pointer lifecycle.
Implementation Steps:
- Declare the
pointertype for returned or accepted arguments in your FFI definitions. - Create a wrapper class that maintains the pointer.
- Use a
try...finallyblock in your application code to ensure the deallocation method is called in thefinallyblock, preventing leaks.
const db = new ZipCodeDatabase(); try { const pop = db.getPopulation(zipCode); } finally { db.free(); }- Declare the
Implement opaque objects in C# using SafeHandle
masterFor robust memory management in C#, inherit from
System.Runtime.InteropServices.SafeHandle. This integrates the Rust pointer into the .NET lifecycle and ensures it is released correctly.Implementation Steps:
- Create a
Nativeclass to hold the FFI function definitions. - Create a subclass of
SafeHandle(e.g.,ZipCodeDatabaseHandle). - Implement
IsInvalid(returningfalseif your Rust function acceptsNULL) andReleaseHandle(calling the Rust deallocation function). - Implement the
IDisposablepattern on your high-level wrapper to forward disposal to theSafeHandle.
class ZipCodeDatabaseHandle : SafeHandle { public override bool IsInvalid => IsNull; protected override bool ReleaseHandle() { Native.zip_code_database_free(handle); return true; } }- Create a
Implement opaque objects in Julia
masterIn Julia, hide the handler pointer behind a new data type. You can manage the lifecycle using either a mapping constructor (for
doblocks) or a manualclosemethod.Implementation Steps:
- Define a type that wraps the pointer.
- Option 1 (Automatic): Implement a mapping constructor
ZipCodeDatabase(f)that handles both allocation and deallocation. This allows usage with thedosyntax, similar to Python'swith. - Option 2 (Manual): Implement a
closemethod to manually free the object when it is no longer needed.
# Option 1: Using 'do' syntax (Automatic) zip_code_database(zip) do db pop = get_population(db, zip) end # Option 2: Manual closing db = ZipCodeDatabase() # ... use db ... close(db)Implement Rust functions that accept string arguments
masterWhen accepting strings from C-compatible interfaces, you must bridge the gap between C's
NUL-terminatedchar*pointers and Rust's UTF-8 guaranteed&strslices.To safely convert a C pointer to a Rust string slice (
&str), follow these steps:- Null Check: Ensure the incoming C pointer is not
NULL, as Rust references cannot be null. - Wrap with
CStr: Usestd::ffi::CStrto wrap the raw pointer. This computes the length based on theNULterminator. This step requires anunsafeblock because you are dereferencing a raw pointer. - UTF-8 Validation: Convert the
CStrto a Rust string slice by ensuring it is valid UTF-8. - Usage: Use the resulting
&strwithin your logic.
Note on Ownership and Lifetimes: The Rust code does not own the string slice. The compiler ensures the slice lives only as long as the
CStrinstance. The caller is responsible for ensuring the memory remains valid for the duration of the function call.use std::ffi::CStr; use std::os::raw::c_char; #[no_mangle] pub unsafe extern "C" fn print_string(ptr: *const c_char) { if ptr.is_null() { return; } let c_str = CStr::from_ptr(ptr); if let Ok(s) = c_str.to_str() { println!("Rust received: {}", s); } else { // Handle invalid UTF-8 } }- Null Check: Ensure the incoming C pointer is not