| name | rust-ffi-bindings |
| description | FFI and language bindings patterns for Rust. Use when creating C APIs, C# bindings, Python bindings, WebAssembly exports, or multi-language bindings with UniFFI. Enforces safety, ABI stability, and proper memory management across language boundaries. Use when this capability is needed. |
| metadata | {"author":"glubus"} |
Rust FFI & Bindings Skill
This skill provides guidance on creating safe, stable, and ergonomic Foreign Function Interface (FFI) bindings for Rust libraries.
When to use this skill
- Creating C API exports from Rust
- Building C# / .NET bindings
- Implementing Python bindings (PyO3)
- Creating WebAssembly bindings
- Using UniFFI for multi-language bindings (Kotlin, Swift, Python, Ruby)
- Exposing Rust functionality to other languages
- Managing memory across language boundaries
Core Principles
1. Safety at the Boundary
Rule: All FFI functions must validate inputs and handle panics.
Why: Panics across FFI boundaries cause undefined behavior.
#[no_mangle]
pub extern "C" fn rox_decode_chart(
data_ptr: *const u8,
data_len: usize,
out_chart: *mut *mut RoxChart,
) -> i32 {
let result = std::panic::catch_unwind(|| {
if data_ptr.is_null() || out_chart.is_null() {
return -1;
}
let data = unsafe {
std::slice::from_raw_parts(data_ptr, data_len)
};
match decode_chart(data) {
Ok(chart) => {
unsafe {
*out_chart = Box::into_raw(Box::new(chart));
}
0
}
Err(_) => -2,
}
});
result.unwrap_or(-3)
}
2. ABI Stability
Rule: Use #[repr(C)] for all exported structs and enums.
Why: Ensures consistent memory layout across compilers and languages.
#[repr(C)]
pub struct RoxChartHandle {
inner: *mut RoxChart,
}
#[repr(C)]
pub enum RoxFormat {
Rox = 0,
Osu = 1,
StepMania = 2,
Quaver = 3,
}
3. Explicit Memory Ownership
Rule: Document and enforce ownership semantics for every FFI function.
Patterns:
- Borrow: Caller owns, function borrows temporarily
- Transfer: Ownership transfers to/from Rust
- Clone: Function creates independent copy
#[no_mangle]
pub extern "C" fn rox_chart_get_title(
chart: *const RoxChart,
out_ptr: *mut *const c_char,
) -> i32 {
}
#[no_mangle]
pub extern "C" fn rox_chart_create(
key_count: u8,
) -> *mut RoxChart {
Box::into_raw(Box::new(RoxChart::new(key_count)))
}
#[no_mangle]
pub extern "C" fn rox_chart_destroy(chart: *mut RoxChart) {
if !chart.is_null() {
unsafe {
let _ = Box::from_raw(chart);
}
}
}
4. Error Handling Across FFI
Rule: FFI functions return error codes or status, never panic.
#[repr(C)]
pub enum RoxError {
Success = 0,
NullPointer = -1,
InvalidInput = -2,
DecodeFailed = -3,
OutOfMemory = -4,
Panic = -99,
}
#[no_mangle]
pub extern "C" fn rox_get_last_error(
buffer: *mut c_char,
buffer_len: usize,
) -> i32 {
LAST_ERROR.with(|e| {
if let Some(ref err) = *e.borrow() {
let err_str = err.to_string();
let bytes = err_str.as_bytes();
let copy_len = bytes.len().min(buffer_len - 1);
unsafe {
std::ptr::copy_nonoverlapping(
bytes.as_ptr(),
buffer as *mut u8,
copy_len,
);
*buffer.add(copy_len) = 0;
}
copy_len
} {
}
})
}
Language-Specific Patterns
C API Pattern
use std::ffi::{CStr, CString};
use std::os::raw::c_char;
pub struct RoxChartHandle {
_private: [u8; 0],
}
#[no_mangle]
pub extern "C" fn rox_chart_new(key_count: u8) -> *mut RoxChartHandle {
let chart = Box::new(RoxChart::new(key_count));
Box::into_raw(chart) as *mut RoxChartHandle
}
#[no_mangle]
pub extern "C" fn rox_chart_free(handle: *mut RoxChartHandle) {
if !handle.is_null() {
unsafe {
let _ = Box::from_raw(handle as *mut RoxChart);
}
}
}
#[no_mangle]
pub extern "C" fn rox_chart_add_note(
handle: *mut RoxChartHandle,
time_us: i64,
column: ,
) {
handle.() {
RoxError::NullPointer ;
}
= { & *(handle * RoxChart) };
chart.notes.(Note::(time_us, column));
RoxError::Success
}
C# Bindings Pattern (using CsBindgen or manual P/Invoke)
#[repr(C)]
pub struct RoxMetadata {
pub title: *const c_char,
pub artist: *const c_char,
pub creator: *const c_char,
pub difficulty_name: *const c_char,
pub audio_file: *const c_char,
}
#[no_mangle]
pub extern "C" fn rox_chart_get_metadata(
chart: *const RoxChart,
out_metadata: *mut RoxMetadata,
) -> i32 {
if chart.is_null() || out_metadata.is_null() {
return -1;
}
let chart = unsafe { &*chart };
let metadata = RoxMetadata {
title: CString::new(chart.metadata.title.as_str())
.unwrap()
.into_raw(),
artist: CString::new(chart.metadata.artist.as_str())
.unwrap()
.into_raw(),
};
{
*out_metadata = metadata;
}
}
(s: * c_char) {
!s.() {
{
= CString::(s);
}
}
}
Python Bindings Pattern (PyO3)
use pyo3::prelude::*;
use pyo3::exceptions::PyValueError;
#[pyclass]
pub struct PyRoxChart {
inner: RoxChart,
}
#[pymethods]
impl PyRoxChart {
#[new]
fn new(key_count: u8) -> PyResult<Self> {
if key_count == 0 || key_count > 18 {
return Err(PyValueError::new_err(
"Key count must be between 1 and 18"
));
}
Ok(Self {
inner: RoxChart::new(key_count),
})
}
fn add_note(&mut self, time_us: i64, column: u8) -> PyResult<()> {
if column >= self.inner.key_count {
return Err(PyValueError::new_err(
format!("Column {} out of range", column)
));
}
self.inner.notes.push(Note::tap(time_us, column));
Ok(())
}
#[getter]
fn (&) {
.inner.metadata.title.()
}
(& , title: ) {
.inner.metadata.title = title;
}
(&) {
(
,
.inner.key_count,
.inner.notes.()
)
}
}
(_py: Python, m: &PyModule) PyResult<()> {
m.add_class::<PyRoxChart>()?;
(())
}
WebAssembly Pattern (wasm-bindgen)
use wasm_bindgen::prelude::*;
#[wasm_bindgen]
pub struct WasmRoxChart {
inner: RoxChart,
}
#[wasm_bindgen]
impl WasmRoxChart {
#[wasm_bindgen(constructor)]
pub fn new(key_count: u8) -> Result<WasmRoxChart, JsValue> {
if key_count == 0 || key_count > 18 {
return Err(JsValue::from_str("Invalid key count"));
}
Ok(Self {
inner: RoxChart::new(key_count),
})
}
#[wasm_bindgen(js_name = addNote)]
pub fn add_note(&mut self, time_us: i64, column: u8) -> Result<(), JsValue> {
if column >= self.inner.key_count {
return Err(JsValue::from_str("Column out of range"));
}
self.inner.notes.push(Note::tap(time_us, column));
Ok(())
}
#[wasm_bindgen(getter)]
(&) {
.inner.metadata.title.()
}
(&) <<>, JsValue> {
(&.inner)
.(|e| JsValue::(&e.()))
}
(data: &[]) <WasmRoxChart, JsValue> {
= (data)
.(|e| JsValue::(&e.()))?;
( { inner })
}
}
UniFFI Pattern (Multi-Target FFI)
When to use: When you need bindings for multiple languages (Kotlin, Swift, Python, Ruby) with minimal boilerplate.
Why UniFFI: Generates FFI bindings automatically from a UDL (Universal Definition Language) file or Rust macros, reducing manual C API work.
use uniffi;
#[uniffi::export]
pub fn create_chart(key_count: u8) -> Result<RoxChart, RoxError> {
if key_count == 0 || key_count > 18 {
return Err(RoxError::InvalidKeyCount(key_count));
}
Ok(RoxChart::new(key_count))
}
#[derive(uniffi::Object)]
pub struct RoxChart {
key_count: u8,
notes: Vec<Note>,
metadata: Metadata,
}
#[uniffi::export]
impl RoxChart {
#[uniffi::constructor]
pub fn new(key_count: u8) -> Self {
Self {
key_count,
notes: Vec::new(),
metadata: Metadata::default(),
}
}
pub fn add_note(&mut self, time_us: i64, column: u8) -> Result<(), RoxError> {
if column >= self.key_count {
(RoxError::InvalidColumn {
column,
max: .key_count
});
}
.notes.(Note::(time_us, column));
(())
}
(&) {
.metadata.title.()
}
(& , title: ) {
.metadata.title = title;
}
(&) {
.notes.()
}
}
{
title: ,
artist: ,
creator: ,
difficulty_name: ,
audio_file: ,
}
{
(),
InvalidColumn { column: , max: },
(),
}
uniffi::setup_scaffolding!();
Alternative: UDL File Approach
// rox.udl
namespace rox {
RoxChart create_chart(u8 key_count);
};
dictionary Metadata {
string title;
string artist;
string creator;
string difficulty_name;
string audio_file;
};
[Error]
enum RoxError {
"InvalidKeyCount",
"InvalidColumn",
"DecodeFailed",
};
interface RoxChart {
constructor(u8 key_count);
[Throws=RoxError]
void add_note(i64 time_us, u8 column);
string get_title();
void set_title(string title);
u32 note_count();
};
Cargo.toml configuration:
[dependencies]
uniffi = "0.25"
[build-dependencies]
uniffi = { version = "0.25", features = ["build"] }
[lib]
crate-type = ["cdylib", "staticlib"]
name = "rox_uniffi"
build.rs:
fn main() {
uniffi::generate_scaffolding("src/rox.udl").unwrap();
}
Generated bindings usage:
val chart = RoxChart(4u)
chart.addNote(1000000, 0u)
println("Title: ${chart.getTitle()}")
let chart = RoxChart(keyCount: 4)
try chart.addNote(timeUs: 1000000, column: 0)
print("Title: \(chart.getTitle())")
from rox_uniffi import RoxChart
chart = RoxChart(4)
chart.add_note(1000000, 0)
print(f"Title: {chart.get_title()}")
UniFFI vs Manual FFI Decision Tree:
-
Do you need multiple language targets?
- Yes, 3+ languages → Use UniFFI
- No, 1-2 languages → Consider manual FFI
-
Do you need fine-grained control over FFI?
- Yes → Manual C API
- No → UniFFI
-
Is your API complex with many types?
- Yes → UniFFI (less boilerplate)
- No → Either works
-
Target languages:
- Kotlin/Swift/Python/Ruby → UniFFI excellent choice
- C/C++ only → Manual C API
- C# → Manual C API or CsBindgen
- JavaScript/WASM → wasm-bindgen
Memory Management Patterns
Pattern 1: Opaque Handles
pub struct OpaqueHandle {
_private: [u8; 0],
}
impl OpaqueHandle {
pub fn from_chart(chart: RoxChart) -> *mut Self {
Box::into_raw(Box::new(chart)) as *mut Self
}
pub unsafe fn as_chart<'a>(handle: *const Self) -> &'a RoxChart {
&*(handle as *const RoxChart)
}
pub unsafe fn as_chart_mut<'a>(handle: *mut Self) -> &'a mut RoxChart {
&mut *(handle as *mut RoxChart)
}
}
Pattern 2: Buffer Management
#[no_mangle]
pub extern "C" fn rox_chart_encode(
chart: *const RoxChart,
buffer: *mut u8,
buffer_len: usize,
out_written: *mut usize,
) -> i32 {
if chart.is_null() || buffer.is_null() || out_written.is_null() {
return -1;
}
let chart = unsafe { &*chart };
match encode_chart(chart) {
Ok(data) => {
if data.len() > buffer_len {
return -2;
}
unsafe {
std::ptr::copy_nonoverlapping(
data.as_ptr(),
buffer,
data.len(),
);
*out_written = data.len();
}
0
}
Err(_) => -3,
}
}
#[no_mangle]
pub extern (
chart: * RoxChart,
out_buffer: * * ,
out_len: * ,
) {
chart.() || out_buffer.() || out_len.() {
-;
}
= { &*chart };
(chart) {
(data) => {
= data.();
= data.();
{
*out_buffer = ptr * ;
*out_len = len;
}
std::mem::forget(data);
}
(_) => -,
}
}
(buffer: * , len: ) {
!buffer.() {
{
= ::(buffer, len, len);
}
}
}
Testing FFI
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_ffi_lifecycle() {
let chart = rox_chart_new(4);
assert!(!chart.is_null());
let result = rox_chart_add_note(chart, 1000000, 0);
assert_eq!(result, 0);
rox_chart_free(chart);
}
#[test]
fn test_null_safety() {
let result = rox_chart_add_note(std::ptr::null_mut(), 0, 0);
assert_eq!(result, RoxError::NullPointer as i32);
}
}
Common Mistakes to Avoid
❌ Exposing Rust types directly
#[no_mangle]
pub extern "C" fn bad_function(s: String) -> Vec<u8> {
}
#[no_mangle]
pub extern "C" fn good_function(
s: *const c_char,
out_buffer: *mut u8,
buffer_len: usize,
) -> i32 {
}
❌ Panicking across FFI
#[no_mangle]
pub extern "C" fn bad_decode(data: *const u8, len: usize) -> *mut RoxChart {
let slice = unsafe { std::slice::from_raw_parts(data, len) };
let chart = decode_chart(slice).unwrap();
Box::into_raw(Box::new(chart))
}
#[no_mangle]
pub extern "C" fn good_decode(
data: *const u8,
len: usize,
out_chart: *mut *mut RoxChart,
) -> i32 {
std::panic::catch_unwind(|| {
}).unwrap_or(-99)
}
❌ Memory leaks
#[no_mangle]
pub extern "C" fn get_title(chart: *const RoxChart) -> *const c_char {
let chart = unsafe { &*chart };
CString::new(chart.metadata.title.as_str())
.unwrap()
.into_raw()
}
#[no_mangle]
pub extern "C" fn get_title(
chart: *const RoxChart,
buffer: *mut c_char,
buffer_len: usize,
) -> i32 {
}
Checklist
When creating FFI bindings:
References
Converted and distributed by TomeVault — claim your Tome and manage your conversions.