ffi_helpers

Crates.ioffi_helpers
lib.rsffi_helpers
version0.3.0
sourcesrc
created_at2018-04-21 17:41:56.167116
updated_at2021-10-25 17:02:33.307709
descriptionA crate to help make working with FFI easier.
homepage
repositoryhttps://github.com/Michael-F-Bryan/ffi_helpers
max_upload_size
id61734
size48,516
rust-devs (github:intility:rust-devs)

documentation

README

FFI Helpers

License Crates.io Documentation

A crate to make working with FFI code easier.

This is the open-source version of a utility crate we use at work. The original purpose was to make it easier for Rust modules (DLLs) to integrate with our main GUI application. We found it to be particularly elegant and robust to use, so thought it'd be a nice thing to share with the world.

Features

This tries to give you a set of abstractions upon which safe APIs can be built. It tries to deal with several issues commonly encountered when writing FFI code.

Error Handling

Error handling is done via a private thread-local LAST_ERROR variable which lets you indicate a error using a similar mechanism to errno.

The idea is if a Rust function returns a Result::Err(_), it'll pass that error to LAST_ERROR and then return an obviously wrong value (e.g. null or 0). The caller then checks for this return and can inspect LAST_ERROR for more information.

A macro is provided to let you inspect LAST_ERROR from C.

Null Pointers

The null_pointer_check!() macro will check whether some nullable thing is null, if so it'll bail with an erroneous return value (null for functions returning pointers or 0 for integers) and set the LAST_ERROR to indicate a null pointer was encountered.

We use a Nullable trait to represent anything which has some sort of "obviously invalid" value (e.g. null pointers, 0).

pub trait Nullable {
    const NULL: Self;

    fn is_null(&self) -> bool;
}

The null_pointer_check!() then lets you check whether a particular thing is invalid, setting the LAST_ERROR, and returning early from the current function with Nullable::NULL.

In practice, this turns out to make handling the possibility of invalid input quite ergonomic.

struct Foo {
  data: Vec<u8>,
}

#[no_mangle]
unsafe extern "C" fn foo_get_data(foo: *const Foo) -> *const u8 {
    null_pointer_check!(foo);

    let foo = &*foo;
    foo.data.as_ptr()
}

Exception Safety

Exception safety becomes a concern when a bit of Rust code panics and tries to unwind across the FFI barrier. At the moment this will abort the program and, while no longer straight up Undefined Behaviour, this is still a massive pain to work around.

There is a catch_panic() function that lets you execute some code and will catch any unwinding, updating the LAST_ERROR appropriately. The catch_panic!() macro makes this a little easier and works with the Nullable trait so you can bail out of a function, returning an error (Nullable::NULL).

Splitting a Closure Into Data and Code

It's quite common for FFI functions that work with callbacks to accept an additional void *user_data argument pointing to any extra state the user may want to use. It doesn't allow the programmer to use

The split_closure() function can be used to "split" a pointer to a closure into a pointer to its data and an unsafe extern "C" fn() which can be used as a callback.

Rust closures are implemented by generating a custom type to contain any captured state, and an impl for FnMut() (or Fn(), or FnOnce()). This function works by casting the closure pointer to void * (this is our data) and defining a trampoline function which will cast the data back and invoke the closure.

It's essentially a generalisation of this:

fn split<C>(closure: &mut C) -> (*mut c_void, unsafe extern "C" fn(*mut c_void))
where C: FnMut()
{
    unsafe extern "C" fn trampoline<T>(user_data: *mut c_void) {
        let closure: &mut T = &mut *(user_data as *mut T);

        closure();
    }

    (closure as *mut C as *mut c_void, trampoline::<T>)
}

Asynchronous Tasks

The Task API helps handle the tricky concurrency issues you encounter when running a job on a background thread and then trying to expose this to C, while maintaining memory- and thread-safety.

The Task trait itself is quite simple:

pub trait Task: Send + Sync + Clone {
    type Output: Send + Sync;
    fn run(&self, cancel_tok: &CancellationToken) -> Result<Self::Output, Error>;
}

You then generate the bindings via the export_task!() macro. This will declare various extern "C" functions for spawning the Task on a background thread, periodically checking whether it's done, allowing you to cancel the task, then retrieve the result and clean everything up properly afterwards.

This is probably the crate's killer feature as it lets you to painlessly run Rust tasks in the background, allowing you to integrate it into a larger application/GUI.

It is highly recommended to visit the task module's docs for a more detailed explanation.

Commit count: 18

cargo fmt