- Introduction
- FFI Architecture Overview
- Core FFI Patterns
- Memory Management
- Error Handling
- Type Conversions
- Safety Considerations
- Best Practices
- Common Pitfalls
- Testing FFI Code
- Examples
This guide documents the Foreign Function Interface (FFI) patterns and best practices used in JUCE-RS. The FFI layer enables interoperability between Rust and C++ code, primarily for testing the Rust implementation against the original JUCE C++ framework.
- Correctness Testing: Compare Rust implementations against C++ JUCE behavior
- Gradual Migration: Allow incremental porting of C++ code to Rust
- Platform Integration: Interface with platform-specific C/C++ APIs
- Plugin Hosting: Load and interact with native audio plugins
- Safety First: All FFI boundaries are carefully validated
- Zero-Cost Abstraction: Minimal overhead for FFI calls
- Memory Safety: Proper ownership tracking across language boundaries
- Error Propagation: Consistent error handling between Rust and C++
- Type Safety: Strong typing even across FFI boundaries
┌─────────────────────────────────────────────────────────┐
│ Rust Public API │
│ (Safe, idiomatic Rust) │
└─────────────────────────────────────────────────────────┘
│
┌─────────────────────────────────────────────────────────┐
│ FFI Bridge Layer │
│ ┌────────────────────────────────────────────────┐ │
│ │ Opaque Pointers & C-Compatible Types │ │
│ │ - Memory management │ │
│ │ - Error handling │ │
│ │ - Type conversions │ │
│ └────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────┘
│
┌─────────────────────────────────────────────────────────┐
│ C++ JUCE Implementation │
│ (For testing only) │
└─────────────────────────────────────────────────────────┘
Each JUCE module has a corresponding FFI crate:
juce-core → juce-core-ffi
juce-audio-basics → juce-audio-basics-ffi
juce-graphics → juce-graphics-ffi
...
The juce-ffi-common crate provides shared FFI infrastructure used by all module FFI crates.
Opaque pointers hide Rust implementation details from C/C++ code while maintaining type safety.
Definition:
// In juce-ffi-common/src/opaque.rs
#[repr(C)]
pub struct OpaqueJString {
_private: [u8; 0], // Zero-sized, prevents instantiation
}Usage:
// Rust side - converting to opaque pointer
#[no_mangle]
pub extern "C" fn jstring_new(text: *const c_char) -> *mut OpaqueJString {
let rust_string = unsafe {
CStr::from_ptr(text).to_string_lossy().into_owned()
};
let jstring = JString::new(&rust_string);
Box::into_raw(Box::new(jstring)) as *mut OpaqueJString
}
// Rust side - using opaque pointer
#[no_mangle]
pub extern "C" fn jstring_length(ptr: *const OpaqueJString) -> usize {
let jstring = unsafe { &*(ptr as *const JString) };
jstring.len()
}
// Rust side - freeing opaque pointer
#[no_mangle]
pub extern "C" fn jstring_free(ptr: *mut OpaqueJString) {
if !ptr.is_null() {
unsafe {
let _ = Box::from_raw(ptr as *mut JString);
}
}
}C++ side:
// Forward declaration
struct OpaqueJString;
// Usage
extern "C" {
OpaqueJString* jstring_new(const char* text);
size_t jstring_length(const OpaqueJString* ptr);
void jstring_free(OpaqueJString* ptr);
}
void example() {
OpaqueJString* str = jstring_new("Hello");
size_t len = jstring_length(str);
jstring_free(str);
}For simple data types, use C-compatible struct representations.
Definition:
#[repr(C)]
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct CRectangle {
pub x: f32,
pub y: f32,
pub width: f32,
pub height: f32,
}Conversion Traits:
pub trait ToCRepr {
type CType;
fn to_c(&self) -> Self::CType;
}
pub trait FromCRepr {
type CType;
fn from_c(c: Self::CType) -> Self;
}
impl ToCRepr for Rectangle {
type CType = CRectangle;
fn to_c(&self) -> Self::CType {
CRectangle {
x: self.x,
y: self.y,
width: self.width,
height: self.height,
}
}
}
impl FromCRepr for Rectangle {
type CType = CRectangle;
fn from_c(c: Self::CType) -> Self {
Rectangle {
x: c.x,
y: c.y,
width: c.width,
height: c.height,
}
}
}FfiBox provides automatic memory tracking and cleanup.
use juce_ffi_common::FfiBox;
#[no_mangle]
pub extern "C" fn create_audio_buffer(
channels: usize,
samples: usize
) -> *mut OpaqueAudioBuffer {
let buffer = AudioBuffer::<f32>::new(channels, samples);
FfiBox::new(buffer).into_raw() as *mut OpaqueAudioBuffer
}
#[no_mangle]
pub extern "C" fn free_audio_buffer(ptr: *mut OpaqueAudioBuffer) {
if !ptr.is_null() {
unsafe {
let _ = FfiBox::<AudioBuffer<f32>>::from_raw(ptr as *mut AudioBuffer<f32>);
}
}
}- Rust Owns: Objects created in Rust are owned by Rust
- Caller Frees: The caller (C++ or Rust) that allocates must free
- No Shared Ownership: Avoid shared ownership across FFI boundaries
- Explicit Cleanup: Always provide explicit free functions
JUCE-RS includes a global memory manager for tracking FFI allocations:
use juce_ffi_common::{register_global_allocation, unregister_global_allocation};
#[no_mangle]
pub extern "C" fn create_object() -> *mut OpaqueObject {
let obj = Object::new();
let ptr = Box::into_raw(Box::new(obj));
register_global_allocation(ptr);
ptr as *mut OpaqueObject
}
#[no_mangle]
pub extern "C" fn free_object(ptr: *mut OpaqueObject) {
if !ptr.is_null() {
let typed_ptr = ptr as *mut Object;
if unregister_global_allocation(typed_ptr) {
unsafe {
let _ = Box::from_raw(typed_ptr);
}
}
}
}use juce_ffi_common::{get_global_memory_stats, check_global_leaks};
#[test]
fn test_no_memory_leaks() {
let (initial_allocs, _) = get_global_memory_stats();
// Perform operations
let ptr = create_object();
free_object(ptr);
let (final_allocs, _) = get_global_memory_stats();
assert_eq!(initial_allocs, final_allocs);
let leaks = check_global_leaks();
assert!(leaks.is_empty(), "Memory leaks detected: {:?}", leaks);
}Short-lived objects:
#[no_mangle]
pub extern "C" fn process_string(input: *const c_char) -> *mut c_char {
let rust_str = unsafe { CStr::from_ptr(input).to_string_lossy() };
let result = rust_str.to_uppercase();
string_to_c(&result).unwrap_or(std::ptr::null_mut())
}Long-lived objects:
#[no_mangle]
pub extern "C" fn audio_processor_new() -> *mut OpaqueAudioProcessor {
let processor = AudioProcessor::new();
FfiBox::new(processor).into_raw() as *mut OpaqueAudioProcessor
}
#[no_mangle]
pub extern "C" fn audio_processor_free(ptr: *mut OpaqueAudioProcessor) {
if !ptr.is_null() {
unsafe {
let _ = FfiBox::<AudioProcessor>::from_raw(ptr as *mut AudioProcessor);
}
}
}JUCE-RS uses a standardized error code system:
#[repr(C)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CErrorCode {
Success = 0,
InvalidParameter = 1,
OutOfMemory = 2,
IoError = 3,
StringConversion = 4,
Threading = 5,
Serialization = 6,
NotSupported = 7,
GenericError = 99,
}Rust side:
use juce_ffi_common::{CErrorCode, set_last_error, clear_last_error};
#[no_mangle]
pub extern "C" fn file_read(
path: *const c_char,
buffer: *mut u8,
size: usize
) -> CErrorCode {
// Validate parameters
if path.is_null() || buffer.is_null() {
set_last_error("Null pointer parameter".to_string());
return CErrorCode::InvalidParameter;
}
// Convert path
let path_str = match unsafe { CStr::from_ptr(path).to_str() } {
Ok(s) => s,
Err(e) => {
set_last_error(format!("Invalid UTF-8: {}", e));
return CErrorCode::StringConversion;
}
};
// Perform operation
match std::fs::read(path_str) {
Ok(data) => {
let copy_size = data.len().min(size);
unsafe {
std::ptr::copy_nonoverlapping(data.as_ptr(), buffer, copy_size);
}
clear_last_error();
CErrorCode::Success
}
Err(e) => {
set_last_error(format!("IO error: {}", e));
CErrorCode::IoError
}
}
}C++ side:
extern "C" {
CErrorCode file_read(const char* path, uint8_t* buffer, size_t size);
CErrorCode juce_rs_get_last_error_message(char* buffer, size_t size);
}
void example() {
uint8_t buffer[1024];
CErrorCode result = file_read("test.txt", buffer, sizeof(buffer));
if (result != CErrorCode::Success) {
char error_msg[256];
juce_rs_get_last_error_message(error_msg, sizeof(error_msg));
std::cerr << "Error: " << error_msg << std::endl;
}
}use juce_ffi_common::{ffi_try, ffi_try_ptr, ffi_try_default};
#[no_mangle]
pub extern "C" fn parse_json(json_str: *const c_char) -> *mut OpaqueJsonValue {
let rust_str = ffi_try_ptr!(unsafe {
CStr::from_ptr(json_str).to_str().map_err(|e|
juce_core::Error::StringConversion(e.to_string())
)
});
let json_value = ffi_try_ptr!(
serde_json::from_str(rust_str).map_err(|e|
juce_core::Error::Serialization(e.to_string())
)
);
FfiBox::new(json_value).into_raw() as *mut OpaqueJsonValue
}Rust to C:
use juce_ffi_common::string_to_c;
#[no_mangle]
pub extern "C" fn get_version_string() -> *mut c_char {
let version = "1.0.0";
string_to_c(version).unwrap_or(std::ptr::null_mut())
}C to Rust:
use juce_ffi_common::string_from_c;
#[no_mangle]
pub extern "C" fn set_name(name: *const c_char) -> CErrorCode {
if name.is_null() {
return CErrorCode::InvalidParameter;
}
let rust_name = match unsafe { string_from_c(name) } {
Ok(s) => s,
Err(e) => {
set_last_error(format!("String conversion failed: {}", e));
return CErrorCode::StringConversion;
}
};
// Use rust_name...
CErrorCode::Success
}Cleanup:
use juce_ffi_common::free_c_string;
// C++ side must call this
#[no_mangle]
pub extern "C" fn free_string(ptr: *mut c_char) {
unsafe { free_c_string(ptr); }
}Boolean:
use juce_ffi_common::{CBool, bool_to_c, bool_from_c};
#[no_mangle]
pub extern "C" fn is_valid(ptr: *const OpaqueObject) -> CBool {
if ptr.is_null() {
return bool_to_c(false);
}
let obj = unsafe { &*(ptr as *const Object) };
bool_to_c(obj.is_valid())
}
#[no_mangle]
pub extern "C" fn set_enabled(ptr: *mut OpaqueObject, enabled: CBool) {
if !ptr.is_null() {
let obj = unsafe { &mut *(ptr as *mut Object) };
obj.set_enabled(bool_from_c(enabled));
}
}Floating Point:
// f32 and f64 are directly compatible with C
#[no_mangle]
pub extern "C" fn calculate_gain(input: f32, factor: f32) -> f32 {
input * factor
}Passing arrays from C to Rust:
#[no_mangle]
pub extern "C" fn process_samples(
samples: *const f32,
num_samples: usize,
output: *mut f32
) -> CErrorCode {
if samples.is_null() || output.is_null() {
return CErrorCode::InvalidParameter;
}
let input_slice = unsafe { std::slice::from_raw_parts(samples, num_samples) };
let output_slice = unsafe { std::slice::from_raw_parts_mut(output, num_samples) };
for (i, &sample) in input_slice.iter().enumerate() {
output_slice[i] = sample * 2.0; // Example processing
}
CErrorCode::Success
}Returning arrays from Rust to C:
#[no_mangle]
pub extern "C" fn generate_samples(
num_samples: usize,
output: *mut f32
) -> CErrorCode {
if output.is_null() {
return CErrorCode::InvalidParameter;
}
let output_slice = unsafe { std::slice::from_raw_parts_mut(output, num_samples) };
for (i, sample) in output_slice.iter_mut().enumerate() {
*sample = (i as f32 * 0.1).sin();
}
CErrorCode::Success
}Using ToCRepr/FromCRepr:
#[no_mangle]
pub extern "C" fn get_bounds(ptr: *const OpaqueComponent) -> CRectangle {
if ptr.is_null() {
return CRectangle { x: 0.0, y: 0.0, width: 0.0, height: 0.0 };
}
let component = unsafe { &*(ptr as *const Component) };
component.bounds().to_c()
}
#[no_mangle]
pub extern "C" fn set_bounds(ptr: *mut OpaqueComponent, bounds: CRectangle) {
if !ptr.is_null() {
let component = unsafe { &mut *(ptr as *mut Component) };
component.set_bounds(Rectangle::from_c(bounds));
}
}- Always Validate Pointers: Check for null before dereferencing
- Validate Array Bounds: Ensure indices are within valid ranges
- Check String Encoding: Validate UTF-8 when converting from C
- Prevent Data Races: Use proper synchronization for shared data
- Document Unsafe: Clearly document safety requirements
#[no_mangle]
pub extern "C" fn safe_operation(ptr: *mut OpaqueObject) -> CErrorCode {
// Step 1: Null check
if ptr.is_null() {
set_last_error("Null pointer passed".to_string());
return CErrorCode::InvalidParameter;
}
// Step 2: Cast and dereference (unsafe block)
let obj = unsafe { &mut *(ptr as *mut Object) };
// Step 3: Validate object state
if !obj.is_initialized() {
set_last_error("Object not initialized".to_string());
return CErrorCode::InvalidParameter;
}
// Step 4: Perform operation
obj.do_something();
CErrorCode::Success
}Atomic Operations:
use std::sync::atomic::{AtomicBool, Ordering};
pub struct ThreadSafeObject {
is_processing: AtomicBool,
// ... other fields
}
#[no_mangle]
pub extern "C" fn start_processing(ptr: *mut OpaqueObject) -> CErrorCode {
if ptr.is_null() {
return CErrorCode::InvalidParameter;
}
let obj = unsafe { &*(ptr as *const ThreadSafeObject) };
// Atomic compare-and-swap
if obj.is_processing.compare_exchange(
false, true, Ordering::SeqCst, Ordering::SeqCst
).is_err() {
set_last_error("Already processing".to_string());
return CErrorCode::GenericError;
}
CErrorCode::Success
}Mutex Protection:
use std::sync::Mutex;
pub struct ProtectedObject {
data: Mutex<Vec<f32>>,
}
#[no_mangle]
pub extern "C" fn add_sample(ptr: *mut OpaqueObject, sample: f32) -> CErrorCode {
if ptr.is_null() {
return CErrorCode::InvalidParameter;
}
let obj = unsafe { &*(ptr as *const ProtectedObject) };
match obj.data.lock() {
Ok(mut data) => {
data.push(sample);
CErrorCode::Success
}
Err(e) => {
set_last_error(format!("Lock failed: {}", e));
CErrorCode::Threading
}
}
}DO:
- ✅ Always check for null pointers
- ✅ Validate array indices and sizes
- ✅ Use
#[deny(unsafe_op_in_unsafe_fn)] - ✅ Document safety requirements
- ✅ Use
NonNull<T>when pointer must not be null - ✅ Validate UTF-8 encoding for strings
DON'T:
- ❌ Assume pointers are valid
- ❌ Dereference without null checks
- ✅ Create references with unbounded lifetimes
- ❌ Share mutable state without synchronization
- ❌ Panic in FFI functions
- ❌ Leak memory
/// # Safety
///
/// This function is unsafe because:
/// - `ptr` must be a valid pointer to an initialized `Object`
/// - `ptr` must not be accessed concurrently from other threads
/// - The caller must ensure `ptr` remains valid for the duration of this call
#[no_mangle]
pub unsafe extern "C" fn unsafe_operation(ptr: *mut OpaqueObject) -> CErrorCode {
// Implementation with unsafe operations
if ptr.is_null() {
return CErrorCode::InvalidParameter;
}
// SAFETY: We've checked for null, and the caller guarantees validity
let obj = unsafe { &mut *ptr };
// ... rest of implementation
CErrorCode::Success
}Function Names:
- Constructor:
<type>_newor<type>_create - Destructor:
<type>_freeor<type>_destroy - Getter:
<type>_get_<property> - Setter:
<type>_set_<property> - Method:
<type>_<action>
Example:
#[no_mangle]
pub extern "C" fn audio_buffer_new(channels: usize, samples: usize) -> *mut OpaqueAudioBuffer;
#[no_mangle]
pub extern "C" fn audio_buffer_get_num_channels(ptr: *const OpaqueAudioBuffer) -> usize;
#[no_mangle]
pub extern "C" fn audio_buffer_set_sample(ptr: *mut OpaqueAudioBuffer, channel: usize, index: usize, value: f32);
#[no_mangle]
pub extern "C" fn audio_buffer_clear(ptr: *mut OpaqueAudioBuffer);
#[no_mangle]
pub extern "C" fn audio_buffer_free(ptr: *mut OpaqueAudioBuffer);Every allocation function must have a corresponding free function:
// ✅ Good
#[no_mangle]
pub extern "C" fn object_new() -> *mut OpaqueObject { /* ... */ }
#[no_mangle]
pub extern "C" fn object_free(ptr: *mut OpaqueObject) { /* ... */ }
// ❌ Bad - no free function
#[no_mangle]
pub extern "C" fn object_new() -> *mut OpaqueObject { /* ... */ }// ✅ Good - opaque pointer
#[no_mangle]
pub extern "C" fn processor_new() -> *mut OpaqueAudioProcessor;
// ❌ Bad - exposing internal structure
#[repr(C)]
pub struct AudioProcessor {
pub internal_state: *mut InternalState,
pub buffer: *mut f32,
// ...
}#[no_mangle]
pub extern "C" fn process_audio(
ptr: *mut OpaqueProcessor,
input: *const f32,
output: *mut f32,
num_samples: usize
) -> CErrorCode {
// Validate all parameters
if ptr.is_null() || input.is_null() || output.is_null() {
return CErrorCode::InvalidParameter;
}
if num_samples == 0 || num_samples > MAX_SAMPLES {
set_last_error(format!("Invalid sample count: {}", num_samples));
return CErrorCode::InvalidParameter;
}
// ... rest of implementation
}// ✅ Good - returns error code
#[no_mangle]
pub extern "C" fn divide(a: f32, b: f32, result: *mut f32) -> CErrorCode {
if result.is_null() {
return CErrorCode::InvalidParameter;
}
if b == 0.0 {
set_last_error("Division by zero".to_string());
return CErrorCode::InvalidParameter;
}
unsafe { *result = a / b; }
CErrorCode::Success
}
// ❌ Bad - can panic
#[no_mangle]
pub extern "C" fn divide_bad(a: f32, b: f32) -> f32 {
assert!(b != 0.0, "Division by zero"); // DON'T PANIC IN FFI!
a / b
}// ✅ Good
#[no_mangle]
pub extern "C" fn my_function() { }
// ❌ Bad - will be name-mangled
pub fn my_function() { }
// ❌ Bad - wrong calling convention
#[no_mangle]
pub fn my_function() { }/// Create a new audio buffer
///
/// # Parameters
/// - `channels`: Number of audio channels (must be > 0)
/// - `samples`: Number of samples per channel (must be > 0)
///
/// # Returns
/// Pointer to the new audio buffer, or null on failure
///
/// # Safety
/// The returned pointer must be freed with `audio_buffer_free`
///
/// # Example
/// ```c
/// OpaqueAudioBuffer* buffer = audio_buffer_new(2, 512);
/// if (buffer != NULL) {
/// // Use buffer...
/// audio_buffer_free(buffer);
/// }
/// ```
#[no_mangle]
pub extern "C" fn audio_buffer_new(
channels: usize,
samples: usize
) -> *mut OpaqueAudioBuffer {
if channels == 0 || samples == 0 {
set_last_error("Invalid buffer dimensions".to_string());
return std::ptr::null_mut();
}
let buffer = AudioBuffer::<f32>::new(channels, samples);
FfiBox::new(buffer).into_raw() as *mut OpaqueAudioBuffer
}pub type AudioSample = f32;
pub type SampleRate = f64;
pub type ChannelIndex = usize;
#[no_mangle]
pub extern "C" fn process_channel(
ptr: *mut OpaqueProcessor,
channel: ChannelIndex,
samples: *mut AudioSample,
num_samples: usize,
sample_rate: SampleRate
) -> CErrorCode {
// Implementation
}// ❌ Bad - memory leak
void bad_example() {
OpaqueString* str = jstring_new("Hello");
// Forgot to call jstring_free(str)!
}
// ✅ Good - proper cleanup
void good_example() {
OpaqueString* str = jstring_new("Hello");
// Use str...
jstring_free(str);
}// ❌ Bad - use after free
void bad_example() {
OpaqueString* str = jstring_new("Hello");
jstring_free(str);
size_t len = jstring_length(str); // UNDEFINED BEHAVIOR!
}
// ✅ Good - don't use after free
void good_example() {
OpaqueString* str = jstring_new("Hello");
size_t len = jstring_length(str);
jstring_free(str);
// Don't use str after this point
}// ❌ Bad - double free
void bad_example() {
OpaqueString* str = jstring_new("Hello");
jstring_free(str);
jstring_free(str); // UNDEFINED BEHAVIOR!
}
// ✅ Good - set to null after free
void good_example() {
OpaqueString* str = jstring_new("Hello");
jstring_free(str);
str = NULL; // Prevent accidental reuse
}// ❌ Bad - buffer overrun
void bad_example() {
float buffer[512];
process_samples(input, 1024, buffer); // Writing 1024 samples to 512-element buffer!
}
// ✅ Good - correct size
void good_example() {
float buffer[512];
process_samples(input, 512, buffer);
}// ❌ Bad - ignoring errors
void bad_example() {
file_read("test.txt", buffer, size); // Ignoring return value
}
// ✅ Good - checking errors
void good_example() {
CErrorCode result = file_read("test.txt", buffer, size);
if (result != CErrorCode::Success) {
char error_msg[256];
juce_rs_get_last_error_message(error_msg, sizeof(error_msg));
handle_error(error_msg);
}
}// ❌ Bad - not freeing returned string
void bad_example() {
char* version = get_version_string();
printf("%s\n", version);
// Memory leak - forgot to free!
}
// ✅ Good - proper string cleanup
void good_example() {
char* version = get_version_string();
if (version != NULL) {
printf("%s\n", version);
free_string(version);
}
}// ❌ Bad - concurrent access without synchronization
void thread1() {
set_value(obj, 42);
}
void thread2() {
int val = get_value(obj); // Data race!
}
// ✅ Good - proper synchronization
void thread1() {
lock_mutex(obj_mutex);
set_value(obj, 42);
unlock_mutex(obj_mutex);
}
void thread2() {
lock_mutex(obj_mutex);
int val = get_value(obj);
unlock_mutex(obj_mutex);
}// ❌ Bad - returning pointer to stack memory
#[no_mangle]
pub extern "C" fn get_name() -> *const c_char {
let name = "JUCE-RS";
name.as_ptr() as *const c_char // DANGLING POINTER!
}
// ✅ Good - heap allocation
#[no_mangle]
pub extern "C" fn get_name() -> *mut c_char {
let name = "JUCE-RS";
string_to_c(name).unwrap_or(std::ptr::null_mut())
}#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_string_creation_and_cleanup() {
let c_str = b"Hello, World!\0";
let ptr = jstring_new(c_str.as_ptr() as *const c_char);
assert!(!ptr.is_null());
let len = jstring_length(ptr);
assert_eq!(len, 13);
jstring_free(ptr);
}
#[test]
fn test_null_pointer_handling() {
let len = jstring_length(std::ptr::null());
assert_eq!(len, 0); // Should handle null gracefully
}
}use proptest::prelude::*;
proptest! {
#[test]
fn prop_string_round_trip(s in "\\PC*") {
let c_str = string_to_c(&s).unwrap();
let recovered = unsafe { string_from_c(c_str).unwrap() };
unsafe { free_c_string(c_str); }
assert_eq!(s, recovered);
}
#[test]
fn prop_buffer_operations(
channels in 1usize..8,
samples in 1usize..1024
) {
let ptr = audio_buffer_new(channels, samples);
assert!(!ptr.is_null());
let num_channels = audio_buffer_get_num_channels(ptr);
assert_eq!(num_channels, channels);
let num_samples = audio_buffer_get_num_samples(ptr);
assert_eq!(num_samples, samples);
audio_buffer_free(ptr);
}
}#[test]
fn test_no_memory_leaks() {
use juce_ffi_common::{get_global_memory_stats, check_global_leaks};
let (initial_allocs, _) = get_global_memory_stats();
// Perform many allocations and deallocations
for _ in 0..1000 {
let ptr = audio_buffer_new(2, 512);
audio_buffer_free(ptr);
}
let (final_allocs, _) = get_global_memory_stats();
assert_eq!(initial_allocs, final_allocs, "Memory leak detected");
let leaks = check_global_leaks();
assert!(leaks.is_empty(), "Leaks found: {:?}", leaks);
}#[test]
fn test_rust_cpp_equivalence() {
// Create objects in both Rust and C++
let rust_ptr = rust_audio_buffer_new(2, 512);
let cpp_ptr = unsafe { cpp_audio_buffer_new(2, 512) };
// Perform same operations
rust_audio_buffer_clear(rust_ptr);
unsafe { cpp_audio_buffer_clear(cpp_ptr); }
// Compare results
for channel in 0..2 {
for sample in 0..512 {
let rust_val = rust_audio_buffer_get_sample(rust_ptr, channel, sample);
let cpp_val = unsafe { cpp_audio_buffer_get_sample(cpp_ptr, channel, sample) };
assert_eq!(rust_val, cpp_val);
}
}
// Cleanup
rust_audio_buffer_free(rust_ptr);
unsafe { cpp_audio_buffer_free(cpp_ptr); }
}#[cfg(fuzzing)]
pub fn fuzz_parse_json(data: &[u8]) {
if let Ok(s) = std::str::from_utf8(data) {
let c_str = string_to_c(s).unwrap_or(std::ptr::null_mut());
if !c_str.is_null() {
let ptr = parse_json(c_str);
if !ptr.is_null() {
json_value_free(ptr);
}
unsafe { free_c_string(c_str); }
}
}
}Rust Implementation:
use juce_ffi_common::*;
use std::ffi::{CStr, c_char};
#[repr(C)]
pub struct OpaqueJString {
_private: [u8; 0],
}
#[no_mangle]
pub extern "C" fn jstring_new(text: *const c_char) -> *mut OpaqueJString {
if text.is_null() {
set_last_error("Null text pointer".to_string());
return std::ptr::null_mut();
}
let rust_str = match unsafe { CStr::from_ptr(text).to_str() } {
Ok(s) => s,
Err(e) => {
set_last_error(format!("Invalid UTF-8: {}", e));
return std::ptr::null_mut();
}
};
let jstring = juce_core::JString::new(rust_str);
FfiBox::new(jstring).into_raw() as *mut OpaqueJString
}
#[no_mangle]
pub extern "C" fn jstring_length(ptr: *const OpaqueJString) -> usize {
if ptr.is_null() {
return 0;
}
let jstring = unsafe { &*(ptr as *const juce_core::JString) };
jstring.len()
}
#[no_mangle]
pub extern "C" fn jstring_to_uppercase(ptr: *const OpaqueJString) -> *mut OpaqueJString {
if ptr.is_null() {
set_last_error("Null pointer".to_string());
return std::ptr::null_mut();
}
let jstring = unsafe { &*(ptr as *const juce_core::JString) };
let upper = jstring.to_uppercase();
FfiBox::new(upper).into_raw() as *mut OpaqueJString
}
#[no_mangle]
pub extern "C" fn jstring_free(ptr: *mut OpaqueJString) {
if !ptr.is_null() {
unsafe {
let _ = FfiBox::<juce_core::JString>::from_raw(ptr as *mut juce_core::JString);
}
}
}C++ Usage:
#include <iostream>
#include <cassert>
// Forward declarations
struct OpaqueJString;
extern "C" {
OpaqueJString* jstring_new(const char* text);
size_t jstring_length(const OpaqueJString* ptr);
OpaqueJString* jstring_to_uppercase(const OpaqueJString* ptr);
void jstring_free(OpaqueJString* ptr);
}
int main() {
// Create string
OpaqueJString* str = jstring_new("hello world");
assert(str != nullptr);
// Get length
size_t len = jstring_length(str);
assert(len == 11);
// Convert to uppercase
OpaqueJString* upper = jstring_to_uppercase(str);
assert(upper != nullptr);
// Cleanup
jstring_free(str);
jstring_free(upper);
return 0;
}Rust Implementation:
use juce_ffi_common::*;
use juce_audio_basics::AudioBuffer;
#[repr(C)]
pub struct OpaqueAudioBuffer {
_private: [u8; 0],
}
#[no_mangle]
pub extern "C" fn audio_buffer_new(
channels: usize,
samples: usize
) -> *mut OpaqueAudioBuffer {
if channels == 0 || samples == 0 {
set_last_error("Invalid buffer dimensions".to_string());
return std::ptr::null_mut();
}
let buffer = AudioBuffer::<f32>::new(channels, samples);
FfiBox::new(buffer).into_raw() as *mut OpaqueAudioBuffer
}
#[no_mangle]
pub extern "C" fn audio_buffer_get_num_channels(
ptr: *const OpaqueAudioBuffer
) -> usize {
if ptr.is_null() {
return 0;
}
let buffer = unsafe { &*(ptr as *const AudioBuffer<f32>) };
buffer.num_channels()
}
#[no_mangle]
pub extern "C" fn audio_buffer_get_num_samples(
ptr: *const OpaqueAudioBuffer
) -> usize {
if ptr.is_null() {
return 0;
}
let buffer = unsafe { &*(ptr as *const AudioBuffer<f32>) };
buffer.num_samples()
}
#[no_mangle]
pub extern "C" fn audio_buffer_clear(ptr: *mut OpaqueAudioBuffer) -> CErrorCode {
if ptr.is_null() {
return CErrorCode::InvalidParameter;
}
let buffer = unsafe { &mut *(ptr as *mut AudioBuffer<f32>) };
buffer.clear();
CErrorCode::Success
}
#[no_mangle]
pub extern "C" fn audio_buffer_apply_gain(
ptr: *mut OpaqueAudioBuffer,
gain: f32
) -> CErrorCode {
if ptr.is_null() {
return CErrorCode::InvalidParameter;
}
let buffer = unsafe { &mut *(ptr as *mut AudioBuffer<f32>) };
buffer.apply_gain(gain);
CErrorCode::Success
}
#[no_mangle]
pub extern "C" fn audio_buffer_get_read_pointer(
ptr: *const OpaqueAudioBuffer,
channel: usize
) -> *const f32 {
if ptr.is_null() {
return std::ptr::null();
}
let buffer = unsafe { &*(ptr as *const AudioBuffer<f32>) };
if channel >= buffer.num_channels() {
set_last_error(format!("Invalid channel index: {}", channel));
return std::ptr::null();
}
buffer.read_pointer(channel).as_ptr()
}
#[no_mangle]
pub extern "C" fn audio_buffer_get_write_pointer(
ptr: *mut OpaqueAudioBuffer,
channel: usize
) -> *mut f32 {
if ptr.is_null() {
return std::ptr::null_mut();
}
let buffer = unsafe { &mut *(ptr as *mut AudioBuffer<f32>) };
if channel >= buffer.num_channels() {
set_last_error(format!("Invalid channel index: {}", channel));
return std::ptr::null_mut();
}
buffer.write_pointer(channel).as_mut_ptr()
}
#[no_mangle]
pub extern "C" fn audio_buffer_free(ptr: *mut OpaqueAudioBuffer) {
if !ptr.is_null() {
unsafe {
let _ = FfiBox::<AudioBuffer<f32>>::from_raw(ptr as *mut AudioBuffer<f32>);
}
}
}C++ Usage:
#include <iostream>
#include <cmath>
struct OpaqueAudioBuffer;
extern "C" {
OpaqueAudioBuffer* audio_buffer_new(size_t channels, size_t samples);
size_t audio_buffer_get_num_channels(const OpaqueAudioBuffer* ptr);
size_t audio_buffer_get_num_samples(const OpaqueAudioBuffer* ptr);
CErrorCode audio_buffer_clear(OpaqueAudioBuffer* ptr);
CErrorCode audio_buffer_apply_gain(OpaqueAudioBuffer* ptr, float gain);
const float* audio_buffer_get_read_pointer(const OpaqueAudioBuffer* ptr, size_t channel);
float* audio_buffer_get_write_pointer(OpaqueAudioBuffer* ptr, size_t channel);
void audio_buffer_free(OpaqueAudioBuffer* ptr);
}
void process_audio(OpaqueAudioBuffer* buffer) {
size_t channels = audio_buffer_get_num_channels(buffer);
size_t samples = audio_buffer_get_num_samples(buffer);
for (size_t ch = 0; ch < channels; ++ch) {
float* data = audio_buffer_get_write_pointer(buffer, ch);
for (size_t i = 0; i < samples; ++i) {
// Generate sine wave
data[i] = std::sin(2.0f * M_PI * i / samples);
}
}
// Apply gain
audio_buffer_apply_gain(buffer, 0.5f);
}
int main() {
// Create buffer
OpaqueAudioBuffer* buffer = audio_buffer_new(2, 512);
// Clear buffer
audio_buffer_clear(buffer);
// Process audio
process_audio(buffer);
// Cleanup
audio_buffer_free(buffer);
return 0;
}Rust Implementation:
use juce_ffi_common::*;
use std::fs;
#[no_mangle]
pub extern "C" fn read_file_to_string(
path: *const c_char,
output: *mut *mut c_char
) -> CErrorCode {
// Validate parameters
if path.is_null() || output.is_null() {
set_last_error("Null pointer parameter".to_string());
return CErrorCode::InvalidParameter;
}
// Convert path
let path_str = match unsafe { CStr::from_ptr(path).to_str() } {
Ok(s) => s,
Err(e) => {
set_last_error(format!("Invalid UTF-8 in path: {}", e));
return CErrorCode::StringConversion;
}
};
// Read file
let contents = match fs::read_to_string(path_str) {
Ok(s) => s,
Err(e) => {
set_last_error(format!("Failed to read file: {}", e));
return CErrorCode::IoError;
}
};
// Convert to C string
match string_to_c(&contents) {
Ok(c_str) => {
unsafe { *output = c_str; }
clear_last_error();
CErrorCode::Success
}
Err(e) => {
set_last_error(format!("String conversion failed: {}", e));
CErrorCode::StringConversion
}
}
}C++ Usage:
#include <iostream>
#include <cstring>
extern "C" {
CErrorCode read_file_to_string(const char* path, char** output);
CErrorCode juce_rs_get_last_error_message(char* buffer, size_t size);
void free_string(char* ptr);
}
bool read_file(const char* path, std::string& output) {
char* contents = nullptr;
CErrorCode result = read_file_to_string(path, &contents);
if (result != CErrorCode::Success) {
char error_msg[512];
juce_rs_get_last_error_message(error_msg, sizeof(error_msg));
std::cerr << "Error reading file: " << error_msg << std::endl;
return false;
}
output = contents;
free_string(contents);
return true;
}
int main() {
std::string contents;
if (read_file("test.txt", contents)) {
std::cout << "File contents: " << contents << std::endl;
} else {
std::cerr << "Failed to read file" << std::endl;
}
return 0;
}This guide covers the essential patterns and best practices for working with FFI in JUCE-RS. Key takeaways:
- Safety First: Always validate inputs and handle errors properly
- Memory Management: Track allocations and ensure proper cleanup
- Clear Ownership: Make ownership and lifetime rules explicit
- Consistent Patterns: Use consistent naming and API design
- Thorough Testing: Test FFI boundaries extensively
For more information:
- See
ffi/juce-ffi-common/src/for FFI infrastructure implementation - See
tests/property/tests/ffi_validation_properties.rsfor FFI testing examples - See individual module FFI crates (e.g.,
ffi/juce-core-ffi/) for module-specific patterns