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
NULL
s 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;
-
struct
s and enum
s 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
bool
eans:
let rust_bool: bool = mem::transmute<_, i32>(c_bool) != 0;
-
do not use static mut
s, not even for FFI; you should use
-
lazy_static!
with RwLock
s when in doubt,
-
thread_local!
s with RefCell
s 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 Option
s (rightfully so: Rust non-C enum
s 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;
}