Why use safer_ffi?

Traditionally, to generate FFI from Rust to C developers would use #[no_mangle] and cbindgen like so:

#[repr(C)]
pub
struct Point {
    x: i32,
    y: i32,
}

#[no_mangle] pub extern "C"
fn origin () -> Point
{
    Point { x: 0, y: 0 }
}

And this is already quite good! For simple FFI projects (e.g., exporting just one or two Rust functions to C), this pattern works wonderfully. So kudos to cbindgen authors for such a convenient, customizable and easy to use tool!

But it turns out that this can struggle with more complex scenarios. My company, Ditto, extensively uses FFI with Rust and has run into the limitations outlined below.

Learn more about Ditto's experience with FFI and Rust.

safer_ffi features that traditional FFI struggles to support

  • These have been tested with cbindgen v0.14.2.

Correctly detecting fn pointers that use an incorrect ABI

As mentioned in the callbacks chapter, functions have an associated calling convention, and getting it wrong leads to Undefined Behavior.

Example

Traditionally, if one were to write the following FFI definition:

#[repr(C)]
pub
struct MyCallback {
    cb: fn(), /* Wops, forgot to mark it `extern "C"` */
}

#[no_mangle] pub extern "C"
fn call (it: MyCallback)
{
    (it.cb)()
}

they would get (with no warnings whatsoever!):

#include <stdarg.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdlib.h>

typedef struct {
    void (*cb)(void); // Wrong, this header corresponds to an `extern "C"` ABI
} MyCallback;

void call(MyCallback it); /* => UB to call from C! */

Fix:

+ use ::safer_ffi::prelude::*;
+
+ #[derive_ReprC]
  #[repr(C)]

...

- #[no_mangle] pub extern "C"
+ #[ffi_export]

By using ::safer_ffi, such errors are caught (since only extern "C" fn pointers are ReprC):

error[E0277]: the trait bound `fn(): safer_ffi::layout::ReprC` is not satisfied
 --> src/lib.rs:3:1
  |
3 | #[derive_ReprC]
  | ^^^^^^^^^^^^^^^ the trait `safer_ffi::layout::ReprC` is not implemented for `fn()`
  |
  = help: the following implementations were found:
            <extern "C" fn() -> Ret as safer_ffi::layout::ReprC>
            <unsafe extern "C" fn() -> Ret as safer_ffi::layout::ReprC>
  = help: see issue #48214
  = note: this error originates in a macro (in Nightly builds, run with -Z macro-backtrace for more info)

💪

Support for complex types and respective layout or ABI semantics

Traditionally, if one were to write the following FFI definition:

#[no_mangle] pub extern "C"
fn my_free (ptr: Box<i32>)
{
    drop(ptr)
}

they would get:

typedef struct Box_i32 Box_i32;

void my_free(Box_i32 ptr);

which does not even compile.

This means that the moment you want to use types to express properties and invariants, you quickly stumble upon this limitation. This is why, traditional Rust→C FFI code uses "flat" raw pointers. This results in unsafe implementations which are more error-prone.

::safer_ffi solves this issue by using more evolved types:

Traditional FFIsafer_ffi
Mutable pointer or NULL*mut TOption<&mut T>
Mutable pointer*mut T&mut T
Owned pointer or NULL*mut TOption<repr_c::Box<T>>
Owned pointer*mut Trepr_c::Box<T>
Example
#[ffi_export]
fn my_free (ptr: repr_c::Box<i32>)
{
    drop(ptr)
}

correctly generates

void my_free(int32_t * ptr);

For instance, what better way to guard against NULL pointer dereferences than to express nullability (or lack thereof) with Option<_>-wrapped pointer types?

Example
#[ffi_export]
fn my_free_supports_null (ptr: Option<repr_c::Box<i32>>)
{
    drop(ptr)
}

Consistent support for macro-generated definitions

Since safer_ffi is integrated within the compiler, it supports macros expanding to #[ffi_export] function definitions or #[derive_ReprC] type definitions.

Example

To make the following code work (w.r.t. auto-generated headers):

macro_rules! adders {(
    $(
        $T:ty => $add_T:ident,
    )*
) => (
    $(
        #[no_mangle] pub extern "C"
        fn $add_T (x: $T, y: $T) -> $T
        {
            x.wrapping_add(y)
        }
    )*
)}

adders! {
    u8  => add_uint8,
    i8  => add_int8,
    u16 => add_uint16,
    i16 => add_int16,
    u32 => add_uint32,
    i32 => add_int32,
    u64 => add_uint64,
    i64 => add_int64,
}

one only has to:

-       #[no_mangle] pub extern "C"
+       #[ffi_export]

Support for shadowed paths

Since safer_ffi is integrated withing the compiler, the types the code refers to are unambiguously understood by both #[derive_ReprC] and #[ffi_export].

Example

The following examples confuses traditional FFI:

/// Let's imagine that we have a custom `Option` type with a defined C layout.
/// We are opting out of a niche layout optimization.
/// (https://rust-lang.github.io/unsafe-code-guidelines/glossary.html#niche)
#[repr(C)]
pub
struct Option<T> {
    is_some: bool,
    value: ::core::mem::MaybeUninit<T>,
}

mod ffi_functions {
    use super::*; // <- This is what `cbindgen` currently struggles with

    #[no_mangle] pub extern "C"
    fn with_my_option (my_opt: Option<&'_ i32>) -> i8
    {
        if my_opt.is_some {
            let value: &'_ i32 = unsafe { my_opt.value.assume_init() };
            println!("{}", *value);
            0
        } else {
            -1
        }
    }
}

Indeed, it generates:

void with_my_option(const int32_t *_it);

Which corresponds to the signature of a function using the standard Option type: ⚠️ an incorrect function signature, with no warnings whatsoever ⚠️

This is another instance where

-   #[no_mangle] pub extern "C"
+   #[ffi_export]

saves the day 🙂