Skip to main content

Module macros

Module macros 

Source
Expand description

FFI Helper Macros

This module provides a set of macros for building safe, ergonomic C FFI bindings. The macros handle common FFI patterns like:

  • Null pointer checking
  • C string conversion
  • Result/error handling with early returns
  • Option handling for validation
  • Handle-based object management

All macros that perform early returns include _or_return_ in their names to make control flow explicit and obvious.

§🔍 Test Mode Debugging & Leak Detection

The FFI layer includes comprehensive memory tracking and debugging features to catch memory management bugs during development and testing.

§Automatic Leak Detection (All Builds)

The pointer registry automatically detects memory leaks at program shutdown. When the registry is dropped, it checks for any pointers that were never freed:

⚠️  WARNING: 3 pointer(s) were not freed at shutdown!
This indicates C code did not properly free all allocated pointers.
Each pointer should be freed exactly once with cimpl_free().

This runs in ALL builds (debug, release, test) and helps identify:

  • Forgotten cimpl_free() calls in C code
  • Pointers returned to C that were never cleaned up
  • Resource leaks that could accumulate over time

§Test-Mode Error Reporting

In test builds (#[cfg(test)]), additional error reporting is enabled for immediate feedback:

§cimpl_free Error Output

When cimpl_free fails in test mode, it prints detailed diagnostic information to stderr:

⚠️  ERROR: cimpl_free failed for pointer 0x12345678: pointer not tracked
This usually means:
1. The pointer was not allocated with box_tracked!/track_box
2. The pointer was already freed (double-free)
3. The pointer is invalid/corrupted

§Why These Errors Matter

Memory management errors caught by these mechanisms indicate serious bugs:

  • Untracked allocations: Using Box::into_raw without track_box means cimpl_free cannot safely deallocate the memory, causing memory leaks
  • Double-free: Calling cimpl_free twice on the same pointer is undefined behavior
  • Invalid pointers: Corrupted or uninitialized pointers will be caught
  • Memory leaks: Pointers never freed waste resources and accumulate over program lifetime

§Production Behavior

In production builds (without #[cfg(test)]):

  • Leak detection at shutdown still runs (helps catch bugs in integration testing)
  • cimpl_free errors are still returned (as -1) and set via crate::Error::set_last
  • No stderr output for individual cimpl_free failures (quieter for library usage)
  • C code should always check return values and handle errors appropriately

§⚠️ CRITICAL: Anti-Pattern Detection Guide ⚠️

Before writing ANY FFI code, scan for these patterns and replace:

❌ if ptr.is_null() { Error::...; return -1; }
✅ deref_mut_or_return_int!(ptr, Type)

❌ match result { Ok(v) => ..., Err(e) => { Error::...; return null } }
✅ ok_or_return_null!(result.map_err(InternalError::from))

❌ unsafe { if ptr.is_null() { ... } &mut *ptr }
✅ deref_mut_or_return_int!(ptr, Type)

❌ unsafe { &*ptr } or unsafe { &mut *ptr }
✅ deref_or_return_int!(ptr, Type) or deref_mut_or_return_int!(ptr, Type)

❌ Manual string length checks and conversion
✅ cstr_or_return!(ptr, -1)

Literal strings to search for in your code:

  • if ptr.is_null() or if ctx.is_null() → Use a macro
  • match result { Ok → Use ok_or_return!
  • unsafe { &* → Use deref_or_return!
  • unsafe { &mut * → Use deref_mut_or_return!

If you see ANY of these patterns, STOP and use the appropriate macro below.

§Quick Reference: Which Macro to Use?

§Input Validation (from C)

  • Pointer from C: deref_or_return_null!(ptr, Type) → validates & dereferences to &Type
  • String from C: cstr_or_return_null!(c_str) → converts C string to Rust String
  • Byte array from C: bytes_or_return_null!(ptr, len, "name") → validates & converts to &[u8]
  • Check not null: ptr_or_return_null!(ptr) → just null check, no deref (for output params)

§Output Creation (to C)

  • Box a value: box_tracked!(value) → heap allocate and return pointer
  • Return string: to_c_string(rust_string) → convert to C string
  • Optional string: option_to_c_string!(opt)None becomes NULL

§Error Handling

  • External crate Result: ok_or_return_null!(result) → uses From trait automatically
  • cimpl::Error Result: ok_or_return_null!(result) → used directly

§Naming Pattern

All macros follow: action_or_return_<what>

  • _null: Returns NULL pointer
  • _int: Returns -1
  • _zero: Returns 0
  • _false: Returns false

§Type Mapping Guide

Rust TypeC receivesMacro to useExample
*mut T (from C)-deref_or_return_null!(ptr, T)Getting object from C
*const c_char (from C)-cstr_or_return_null!(s)Getting string from C
*const c_uchar + len-bytes_or_return_null!(p, len, "name")Getting byte array from C
Result<T, ExtErr>pointer/intok_or_return_null!(r)External crate errors (From trait)
Result<T, cimpl::Err>pointer/intok_or_return_null!(r)Internal validation
Option<T> custompointer/intsome_or_return_null!(o, err)Specific error needed
T (owned)*mut Tbox_tracked!(value)Returning new object
String*mut c_charto_c_string(s)Returning string
Option<String>*mut c_charoption_to_c_string!(opt)Optional string

§Common FFI Function Patterns

§Pattern 1: Constructor (returns new object)

#[no_mangle]
pub extern "C" fn thing_new(value: i32) -> *mut Thing {
    let thing = some_or_return_other_null!(
        Thing::try_new(value),
        "Invalid value"
    );
    box_tracked!(thing)
}

§Pattern 2: Parser (external crate Result with centralized mapping)

// 1. Define error code enum
#[repr(i32)]
pub enum UuidError {
    ParseError = 100,
}

// 2. Implement From trait (centralized mapping!)
impl From<uuid::Error> for cimpl::Error {
    fn from(e: uuid::Error) -> Self {
        cimpl::Error::new(
            UuidError::ParseError as i32,
            format!("ParseError: {}", e)
        )
    }
}

// 3. Use in FFI (automatic conversion via From!)
#[no_mangle]
pub extern "C" fn uuid_parse(s: *const c_char) -> *mut Uuid {
    let s_str = cstr_or_return_null!(s);
    let uuid = ok_or_return_null!(Uuid::from_str(&s_str));
    box_tracked!(uuid)
}

§Pattern 3: Method (operates on object)

#[no_mangle]
pub extern "C" fn thing_add(thing: *mut Thing, value: i32) -> i32 {
    let obj = deref_or_return_int!(thing, Thing);
    obj.add(value)
}

§Pattern 4: Method with validation (Option)

#[no_mangle]
pub extern "C" fn date_add_days(date: *mut Date, days: i64) -> *mut Date {
    let obj = deref_or_return_null!(date, Date);
    let new_date = some_or_return_other_null!(
        obj.checked_add_days(days),
        "Date overflow"
    );
    box_tracked!(new_date)
}

Constants§

MAX_CSTRING_LEN
Maximum length for C strings when using bounded conversion (1MB)