8 releases (4 breaking)

0.5.0 Aug 11, 2023
0.4.1 Feb 17, 2023
0.4.0 Feb 5, 2022
0.3.1 Nov 14, 2021
0.1.0 Mar 30, 2021

#232 in Asynchronous

Download history 1212/week @ 2023-12-13 585/week @ 2023-12-20 674/week @ 2023-12-27 2852/week @ 2024-01-03 2164/week @ 2024-01-10 3048/week @ 2024-01-17 2562/week @ 2024-01-24 2247/week @ 2024-01-31 2381/week @ 2024-02-07 2807/week @ 2024-02-14 2771/week @ 2024-02-21 3882/week @ 2024-02-28 2517/week @ 2024-03-06 2564/week @ 2024-03-13 3016/week @ 2024-03-20 2124/week @ 2024-03-27

11,408 downloads per month
Used in 5 crates (2 directly)

MIT license

26KB
328 lines

async-ffi: FFI-compatible Futures

crates.io docs.rs CI

Convert your Rust Futures into a FFI-compatible struct without relying unstable Rust ABI and struct layout. Easily provide async functions in dynamic library maybe compiled with different Rust than the invoker.

See documentation for more details.

See link_tests directory for cross-linking examples.

License

MIT Licensed.


lib.rs:

FFI-compatible Futures

Rust currently doesn't provide stable ABI nor stable layout of related structs like dyn Future or Waker. With this crate, we can wrap async blocks or async functions to make a Future FFI-safe.

FfiFuture provides the same functionality as Box<dyn Future<Output = T> + Send> but it's FFI-compatible, aka. repr(C). Any Future<Output = T> + Send + 'static can be converted into FfiFuture by calling into_ffi on it, after useing the trait FutureExt.

FfiFuture implements Future<Output = T> + Send. You can await a FfiFuture just like a normal Future to wait and get the output.

For non-Send or non-'static futures, see the section Variants of FfiFuture below.

Examples

Provide some async functions in library: (plugin side)

// Compile with `crate-type = ["cdylib"]`.
use async_ffi::{FfiFuture, FutureExt};

#[no_mangle]
pub extern "C" fn work(arg: u32) -> FfiFuture<u32> {
    async move {
        let ret = do_some_io(arg).await;
        do_some_sleep(42).await;
        ret
    }
    .into_ffi()
}

Execute async functions from external library: (host or executor side)

use async_ffi::{FfiFuture, FutureExt};

// #[link(name = "myplugin...")]
extern "C" {
    #[no_mangle]
    fn work(arg: u32) -> FfiFuture<u32>;
}

async fn run_work(arg: u32) -> u32 {
    unsafe { work(arg).await }
}

Proc-macro helpers

If you enable the feature macros (disabled by default), an attribute-like procedural macro is available at top-level. See its own documentation for details.

With the macro, the example above can be simplified to:

use async_ffi::async_ffi;

#[no_mangle]
#[async_ffi]
pub async extern "C" fn work(arg: u32) -> u32 {
    let ret = do_some_io(arg).await;
    do_some_sleep(42).await;
    ret
}

Panics

You should know that unwinding across an FFI boundary is Undefined Behaviour.

Panic in Future::poll

Since the body of async fn is translated to Future::poll by the compiler, the poll method is likely to panic. If this happen, the wrapped FfiFuture will catch unwinding with std::panic::catch_unwind, returning FfiPoll::Panicked to cross the FFI boundary. And the other side (usually the plugin host) will get this value in the implementation of <FfiFuture<T> as std::future::Future>::poll, and explicit propagate the panic, just like std::sync::Mutex's poisoning mechanism.

Panic in Future::drop or any waker vtable functions Waker::*

Unfortunately, this is very difficult to handle since drop cleanup and Waker functions are expected to be infallible. If these functions panic, we would just call std::process::abort to terminate the whole program.

Variants of FfiFuture

There are a few variants of FfiFuture. The table below shows their corresponding std type.

Type The corresponding std type
FfiFuture<T> Box<dyn Future<Output = T> + Send + 'static>
LocalFfiFuture<T> Box<dyn Future<Output = T> + 'static>
BorrowingFfiFuture<'a, T> Box<dyn Future<Output = T> + Send + 'a>
LocalBorrowingFfiFuture<'a, T> Box<dyn Future<Output = T> + 'a>

All of these variants are ABI-compatible to each other, since lifetimes and Send cannot be represented by the C ABI. These bounds are only checked in the Rust side. It's your duty to guarantee that the Send and lifetime bounds are respected in the foreign code of your external fns.

Performance and cost

The conversion between FfiFuture and orinary Future is not cost-free. Currently FfiFuture::new and its alias FutureExt::into_ffi does one extra allocation. When polling an FfiFuture, the Waker supplied does one extra allocation when cloned.

It's recommended to only wrap you async code once right at the FFI boundary, and use ordinary Future everywhere else. It's usually not a good idea to use FfiFuture in methods, trait methods, or generic codes.

abi-stable support

If you want to use this crate with abi-stable interfaces. You can enable the feature flag abi_stable (disabled by default), then the struct FfiFuture and friends would derive abi_stable::StableAbi.

Dependencies

~0–7.5MB
~17K SLoC