The macro

In this chapter we'll explore where you can use the #[ezffi::export] macro and the code it generates.

Structs

C-compatible structs

If you have a struct where all the fields are C-compatible, #[ezffi::export] will rename it and mark it with #[repr(C)], and provide a type alias so you can keep using the original name in your code. E.g.:

#[ezffi::export]
pub struct Counter {
    num: u64
}

will generate

#[derive(Clone, Copy)]
pub struct LibNameCounter {
    num: u64
}

pub type Counter = LibNameCounter;

// These are the `ezffi` traits that define how to go from
// a Rust-type to a C-type and the other way around. For
// C-compatible structs they're no-op and only exist to
// simplify the wrapper generation. The compiler should
// optimize them out.
impl ::ezffi::RustRefIntoC<()> for LibNameCounter {...} // This one copies the value though
impl ::ezffi::RustOwnedIntoC<()> for LibNameCounter {...}
impl ::ezffi::CRefIntoRust<LibNameCounter> for LibNameCounter {...}
impl ::ezffi::COwnedIntoRust<LibNameCounter> for LibNameCounter {...}

With this approach ezffi can ensure max performance on the C side, avoiding the generic solution of a c_void pointer to a heap-allocated struct.

Non C-compatible structs

This will be the most common case for users that don't need that extra performance and want to use all kind of Rust types in their exported structs, like Rc. E.g.:

#[ezffi::export]
pub struct Screen {
    header: Rc<Widget>
}

will generate

pub struct Screen {
    header: Rc<Widget>
}

#[derive(Clone, Copy)]
#[repr(C)]
pub struct LibNameScreen {
    inner: *mut core::ffi::c_void
}

#[unsafe(no_mangle)]
pub unsafe extern "C" fn libname_screen_free(o: *const LibNameScreen) {
    let _ = unsafe { Box::from_raw((*o).inner as *mut Screen) };
}

// In this case the traits Box the Rust-type and
// know how to cast from c_void to the actual Rust-type.
impl ::ezffi::RustRefIntoC<()> for Screen {...}
impl ::ezffi::RustOwnedIntoC<()> for Screen {...}
impl ::ezffi::CRefIntoRust<T> for LibNameScreen {...}
impl ::ezffi::COwnedIntoRust<T> for LibNameScreen {...}

Generic structs

These are a little trickier and I personally avoid exposing them. E.g.:

pub struct Wrapper<T> {
    value: T,
}

will generate

pub struct Wrapper<T> {
    value: T,
}

#[derive(Clone, Copy)]
#[repr(C)]
pub struct LibNameWrapper {
    inner: *mut core::ffi::c_void,
    drop_fn: unsafe extern "C" fn(*mut core::ffi::c_void), // Per-monomorphization drop
}

#[unsafe(no_mangle)]
pub unsafe extern "C" fn libname_wrapper_free(o: *const LibNameWrapper) {
    unsafe { ((*o).drop_fn)((*o).inner); }
}

// Same `ezffi` conversion traits as before.

Structs with Lifetimes

Not yet implemented but should be easy to do.

Enums

C-compatible enums

C-compatible enums are those that have all their variants as unit variants, and they receive the same treatment as the C-compatible structs. Your enum will be renamed and marked as #[repr(C)], and a type alias will be declared so you can use the name you gave to it.

Non C-compatible

Non C-compatible enums are those that, well, aren't C-compatible, and they receive the same treatment as non C-compatible structs. A struct will be created containing a pointer to the heap-allocated enum.

Generic enums and enums with lifetimes

Not yet implemented. I felt lazy and honestly, I didn't have a use case for it. Feel free to do the implementation yourself if you'd like to collaborate and/or need it.

Functions

This section is a short and easy one, but I'll split it in two.

Functions as God intended them to be

All functions that receive and return C-compatible types or exported Rust-types (ergo they have a generated C-type) can be marked with #[ezffi::export]. E.g.:

#[ezffi::export]
pub fn distance(a: Point, b: Point) -> f64 {...}

generates

pub fn distance(a: Point, b: Point) -> f64 {...}

pub unsafe extern "C" fn libname_distance(a: LibNamePoint, b: LibNamePoint) -> f64 {
    use ezffi::RustRefIntoC;
    use ezffi::RustOwnedIntoC;
    use ezffi::CRefIntoRust;
    use ezffi::COwnedIntoRust;

    // Here is where the `ezffi` traits are used
    // to convert C-types into Rust-types.
    let a = &*a;
    let a = a.into_rust();
    let b = &*b;
    let b = b.into_rust();

    // Now we can call the Rust function without any problem.
    let result = distance(a, b);

    result.owned_into_c()
}

Async functions

These are pretty easy. They need the async feature to be enabled, and the macro will generate almost the same code as before — only the Rust function call will change, and look like let result = ::ezffi::dispatch_async(distance(a, b));. This dispatch_async function is defined in ezffi only if the feature is enabled, and hands the future to the configured dispatcher. By default ezffi will run pollster::block_on(fut) for every async function, but you can plug in any dispatcher you want, it's up to you.

The recommended way to change the dispatcher is the following:

static TOKIO_RT: OnceLock<tokio::runtime::Runtime> = OnceLock::new();

// We export this function to set the desired async dispatcher
// from the C side before calling any async function.
#[ezffi::export]
pub fn init_tokio() {
    TOKIO_RT
        .set(
            tokio::runtime::Builder::new_multi_thread()
                .enable_time()
                .build()
                .unwrap(),
        )
        .ok();
    ezffi::set_async_dispatcher(|fut| {
        TOKIO_RT.get().unwrap().block_on(fut);
    });
}

The C side would look something like

#include "libname/libname.h"
#include <assert.h>

int main() {
  libname_init_tokio();

  // Now we can execute our async functions using tokio.

  return 0;
}

Impl Blocks

This is also a trivial implementation: any exported Rust-type can export its methods, we saw an example of this in the Getting Started section. The only difference with regular functions is that the Self type gets resolved to the actual type, and the Rust method calls are done using the full path: Point::add(this, other);.

Async methods are also supported and will be dispatched using the same logic as regular functions.