| name | rust-ffi |
| description | Microsoft Pragmatic Rust FFI Guidelines. Use when working with C/C++ interop, creating cdylib, writing unsafe FFI code, or multi-DLL Rust projects. |
| allowed-tools | Read, Write, Edit, Bash, Grep, Glob |
Microsoft Pragmatic Rust - FFI Guidelines
Guidelines for Foreign Function Interface and cross-language interoperability.
Basic FFI Patterns
Calling C from Rust
// Declare external C function
extern "C" {
fn strlen(s: *const c_char) -> usize;
fn malloc(size: usize) -> *mut c_void;
fn free(ptr: *mut c_void);
}
// Safe wrapper
pub fn safe_strlen(s: &CStr) -> usize {
// SAFETY: CStr guarantees null-terminated valid UTF-8
unsafe { strlen(s.as_ptr()) }
}
Exposing Rust to C
/// # Safety
///
/// - `ptr` must be a valid pointer to a null-terminated C string
/// - The string must be valid UTF-8
#[no_mangle]
pub unsafe extern "C" fn process_string(ptr: *const c_char) -> i32 {
if ptr.is_null() {
return -1;
}
// SAFETY: Caller guarantees ptr is valid null-terminated string
let c_str = unsafe { CStr::from_ptr(ptr) };
match c_str.to_str() {
Ok(s) => {
// Process the string
s.len() as i32
}
Err(_) => -2, // Invalid UTF-8
}
}
Portable Data Types
Use repr(C) for ABI Compatibility
/// FFI-safe configuration structure.
#[repr(C)]
pub struct FfiConfig {
pub version: u32,
pub flags: u32,
pub timeout_ms: u64,
pub name: *const c_char, // Null-terminated
}
// Implement conversion from Rust types
impl FfiConfig {
/// Creates FFI config from Rust config.
///
/// # Safety
///
/// The returned struct contains a pointer to `name`.
/// The caller must ensure `name` outlives the FfiConfig.
pub unsafe fn from_config(config: &Config, name: &CStr) -> Self {
Self {
version: 1,
flags: config.flags,
timeout_ms: config.timeout.as_millis() as u64,
name: name.as_ptr(),
}
}
}
Enum Representation
// For C interop, use explicit discriminants
#[repr(C)]
pub enum FfiStatus {
Success = 0,
InvalidInput = 1,
NotFound = 2,
InternalError = 3,
}
// For error codes
#[repr(i32)]
pub enum FfiError {
Ok = 0,
NullPointer = -1,
InvalidUtf8 = -2,
BufferTooSmall = -3,
}
Opaque Types
// Hide internal structure from C
pub struct OpaqueHandle {
inner: Box<InternalState>,
}
// Export as opaque pointer
pub type Handle = *mut OpaqueHandle;
#[no_mangle]
pub extern "C" fn create_handle() -> Handle {
let handle = Box::new(OpaqueHandle {
inner: Box::new(InternalState::new()),
});
Box::into_raw(handle)
}
/// # Safety
///
/// `handle` must be a valid pointer created by `create_handle`
/// and not previously freed.
#[no_mangle]
pub unsafe extern "C" fn destroy_handle(handle: Handle) {
if !handle.is_null() {
// SAFETY: Caller guarantees handle validity
let _ = unsafe { Box::from_raw(handle) };
}
}
Memory Management
Ownership Transfer
/// Allocates and returns a string. Caller must free with `free_string`.
#[no_mangle]
pub extern "C" fn get_result() -> *mut c_char {
let result = "Hello from Rust";
let c_string = CString::new(result).unwrap();
c_string.into_raw() // Transfer ownership to caller
}
/// Frees a string allocated by this library.
///
/// # Safety
///
/// `ptr` must be a pointer returned by `get_result` and not previously freed.
#[no_mangle]
pub unsafe extern "C" fn free_string(ptr: *mut c_char) {
if !ptr.is_null() {
// SAFETY: Caller guarantees this was allocated by us
let _ = unsafe { CString::from_raw(ptr) };
}
}
Buffer Patterns
/// Writes result to caller-provided buffer.
///
/// Returns the number of bytes written, or -1 on error.
/// If buffer is null or too small, returns required size.
///
/// # Safety
///
/// If `buffer` is not null, it must point to at least `buffer_size` bytes.
#[no_mangle]
pub unsafe extern "C" fn get_data(
buffer: *mut u8,
buffer_size: usize,
) -> isize {
let data = b"Hello from Rust";
let required = data.len();
if buffer.is_null() || buffer_size < required {
return required as isize;
}
// SAFETY: Caller guarantees buffer validity and size
unsafe {
std::ptr::copy_nonoverlapping(data.as_ptr(), buffer, required);
}
required as isize
}
Error Handling
Error Codes Pattern
#[repr(i32)]
pub enum ErrorCode {
Success = 0,
NullPointer = 1,
InvalidArgument = 2,
OutOfMemory = 3,
IoError = 4,
Unknown = 99,
}
// Thread-local last error
thread_local! {
static LAST_ERROR: RefCell<Option<String>> = RefCell::new(None);
}
fn set_last_error(msg: impl Into<String>) {
LAST_ERROR.with(|e| *e.borrow_mut() = Some(msg.into()));
}
/// Returns the last error message, or null if no error.
/// The returned string is valid until the next API call.
#[no_mangle]
pub extern "C" fn get_last_error() -> *const c_char {
LAST_ERROR.with(|e| {
e.borrow()
.as_ref()
.map(|s| s.as_ptr() as *const c_char)
.unwrap_or(std::ptr::null())
})
}
Result Pattern
/// FFI-safe result type.
#[repr(C)]
pub struct FfiResult<T> {
pub success: bool,
pub value: T,
pub error_code: i32,
}
impl<T: Default> FfiResult<T> {
pub fn ok(value: T) -> Self {
Self {
success: true,
value,
error_code: 0,
}
}
pub fn err(code: i32) -> Self {
Self {
success: false,
value: T::default(),
error_code: code,
}
}
}
Multi-DLL Considerations
Avoid Passing Rust Types Across DLL Boundaries
// BAD - Rust types have unstable ABI
pub extern "C" fn get_string() -> String { ... } // Don't do this!
// GOOD - Use C-compatible types
pub extern "C" fn get_string() -> *mut c_char { ... }
Allocator Consistency
// Each DLL should free memory it allocated
// Don't free memory allocated by another DLL
// DLL A allocates
#[no_mangle]
pub extern "C" fn dll_a_allocate() -> *mut Data {
Box::into_raw(Box::new(Data::new()))
}
// DLL A frees (not DLL B!)
#[no_mangle]
pub unsafe extern "C" fn dll_a_free(ptr: *mut Data) {
if !ptr.is_null() {
let _ = unsafe { Box::from_raw(ptr) };
}
}
Callbacks
Function Pointers
/// Callback function type.
pub type Callback = extern "C" fn(data: *const u8, len: usize) -> i32;
/// Registers a callback for events.
///
/// # Safety
///
/// `callback` must be a valid function pointer that remains valid
/// for the duration of the registration.
#[no_mangle]
pub unsafe extern "C" fn register_callback(callback: Callback) {
// Store callback for later invocation
CALLBACK.store(callback as usize, Ordering::SeqCst);
}
Callbacks with User Data
pub type CallbackWithData = extern "C" fn(
user_data: *mut c_void,
event: *const Event,
) -> i32;
#[repr(C)]
pub struct CallbackRegistration {
pub callback: CallbackWithData,
pub user_data: *mut c_void,
}
/// # Safety
///
/// `user_data` must remain valid for the callback's lifetime.
#[no_mangle]
pub unsafe extern "C" fn register(reg: CallbackRegistration) {
// Invoke callback:
// (reg.callback)(reg.user_data, &event);
}
cbindgen for Header Generation
cbindgen.toml
language = "C"
include_guard = "MY_LIBRARY_H"
include_version = true
cpp_compat = true
[defines]
"feature = windows" = "MY_LIB_WINDOWS"
[export]
include = ["FfiConfig", "FfiResult", "FfiStatus"]
[fn]
rename_args = "SnakeCase"
Generate Header
cbindgen --config cbindgen.toml --output include/my_library.h
Testing FFI
Test Round-Trip
#[test]
fn test_ffi_roundtrip() {
let handle = create_handle();
assert!(!handle.is_null());
unsafe {
let result = do_operation(handle, 42);
assert_eq!(result, 0);
destroy_handle(handle);
}
}
Miri for Undefined Behavior
# Run tests under Miri
cargo +nightly miri test