Claude Code Plugins

Community-maintained marketplace

Feedback

Microsoft Pragmatic Rust FFI Guidelines. Use when working with C/C++ interop, creating cdylib, writing unsafe FFI code, or multi-DLL Rust projects.

Install Skill

1Download skill
2Enable skills in Claude

Open claude.ai/settings/capabilities and find the "Skills" section

3Upload to Claude

Click "Upload skill" and select the downloaded ZIP file

Note: Please verify skill by going through its instructions before using it.

SKILL.md

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