Storing C callbacks in Rust

Although Rust is a great language for FFI, it is a very unsafe thing to do, leading very easily to UB.

When doing so, I always use the following

FFI safety belt

  • any pointer from the C world becomes Option<NonNull<_>>, (or Option<unsafe extern "C" fn (...) -> ... > for function pointers);

    • this tackles C code being able to feed NULLs at will, forcing the Rust code to handle them;
  • use ::libc::c_void to represent C's void. Thus void * becomes Option<NonNull<::libc::c_void>>;

  • even though panic = "abort" is a setting that can be added to Cargo.toml compilation profile, I prefer to have such a guard within the exported code;

  • structs and enums should be #[repr(C)] (or #[repr(transparent)] for newtypes)

  • if receiving an enum from C / FFI, it should be of integer type. If it isn't, it should be instantly transmuted into an integer and then matched against integer values to get a Rust enum back.

    • this includes booleans:
      let rust_bool: bool = mem::transmute<_, i32>(c_bool) != 0;
  • do not use static muts, not even for FFI; you should use

    • lazy_static! with RwLocks when in doubt,

    • thread_local!s with RefCells for single-threaded programs,

    • or, if you really wanna go down the unsafe path, a static UnsafeSyncCell<_>:

      #[repr(transparent)]
      pub
      struct UnsafeSyncCell<T> (pub UnsafeCell<T>);
      
      unsafe impl<T> Sync for UnsafeSyncCell<T> {}
      
      static MY_STATIC_MUT: UnsafeSyncCell< u8 > = UnsafeSyncCell(UnsafeCell::new(0));
      
      // above is still better than:
      static mut MY_STATIC_MUT: u8 = 0; // DO NOT DO THIS
      

Example: registering callbacks in a single-threaded context

Rust FFI library (crate-type = ["cdylib"] or crate-type = ["staticlib"])

use ::std::{*,
    cell::RefCell,
    ptr::NonNull,
};
use ::libc::c_void; // libc = "0.2.51"

macro_rules! ffi_panic_boundary {($($tt:tt)*) => (
    // /* or */ { ::scopeguard::defer_on_unwind(process::abort()); $($tt)* }
    match ::std::panic::catch_unwind!(|| {$($tt)*}) {//
        | Ok(ret) => ret,
        | Err(_) => {
            // /* or */ return RUST_ERR_PANICKED;
            eprintln!("Rust panicked; aborting process");
            ::std::process::abort() 
        },
    }
)}

type Arg = NonNull<c_void>;

type Callback = unsafe extern "C" fn (mb_arg: Option<Arg>);

thread_local! {
    static CALLBACKS: RefCell<
        Vec< (Callback, Option<Arg>) >
    > = RefCell::new(Vec::new());
}
// or lazy_static! with a RwLock

#[allow(non_camel_case_types)]
#[repr(C)]
pub
enum e_rust_status {
    RUST_OK = 0,
    RUST_ERR_NULL_POINTER,
    // RUST_ERR_PANICKED,
}
use self::e_rust_status::*;

macro_rules! unwrap_pointer {($pointer:expr) => (
    match $pointer {//
        | Some(non_null_pointer) => non_null_pointer,
        | None => return RUST_ERR_NULL_POINTER,
    }
)}

#[no_mangle] pub extern "C"
fn register_cb (
    cb: Option<Callback>,
    arg: Option<Arg>,
) -> e_rust_status
{
    ffi_panic_boundary! {
        let cb = unwrap_pointer!(cb);
        CALLBACKS.with(|slf| { slf
            .borrow_mut()
            .push((cb, arg))
        });
        RUST_OK
    }
}

#[no_mangle] pub unsafe extern "C"
fn call_cbs () -> e_rust_status
{
    ffi_panic_boundary! {
        CALLBACKS.with(|slf| { slf
            .borrow_mut()
            .iter_mut()
            .for_each(|&mut (cb, arg): &mut (Callback, Option<Arg>)| {
                cb(arg);
            })
        });
        RUST_OK
    }
}

#[no_mangle] pub extern "C"
fn clear_cbs () -> e_rust_status
{
    ffi_panic_boundary! {
        CALLBACKS.with(|slf| {
            let mut cbs = slf.borrow_mut();
            cbs.clear();
            cbs.shrink_to_fit();
        });
        RUST_OK
    }
}

Rust FFI library header

#ifndef __RUST_LIB_H__
#define __RUST_LIB_H__

typedef enum rust_status {
    RUST_OK = 0,
    RUST_ERR_NULL_POINTER,
} e_rust_status;

typedef void (*cb_t) (void *);

extern e_rust_status register_cb (cb_t cb, void * arg);

extern e_rust_status call_cbs (void);

extern e_rust_status clear_cbs (void);

#endif //__RUST_LIB_H__

(you can use cbindgen for this step, although it does not like Options (rightfully so: Rust non-C enums layout is undefined))

User of the library: example C code

#include <stdio.h>
#include <stdlib.h>
#include "rust_lib.h"

void rust_try (e_rust_status rust_status)
{
    switch (rust_status) {
        case RUST_OK:
            return;
        case RUST_ERR_NULL_POINTER:
            fprintf(stderr, "Rust call failed: got NULL pointer\n");
            break;
        default:
            fprintf(stderr, "Rust call returned an unknown value\n");
            break;
    }
    exit(EXIT_FAILURE);
}

void inc (int * counter)
{
    ++(*counter);
}

int main (int argc, char const * const argv[])
{
    int counter = 0;
    {
        rust_try(register_cb((cb_t) inc, &counter));
        rust_try(register_cb((cb_t) inc, &counter));
        rust_try(register_cb((cb_t) inc, &counter));
        printf("%d\n", counter);
        rust_try(call_cbs());
        printf("%d\n", counter);
        rust_try(clear_cbs());
    }
    return EXIT_SUCCESS;
}
10 Likes