├── .github └── workflows │ ├── ci.yaml │ └── future_proof.yaml ├── .gitignore ├── CHANGELOG.md ├── Cargo.toml ├── LICENSE-MIT ├── README.md ├── link_tests ├── Makefile ├── executor-c │ └── main.c ├── executor-rust │ ├── Cargo.toml │ └── src │ │ └── main.rs ├── plugin-c │ └── plugin.c └── plugin-rust │ ├── Cargo.toml │ └── src │ └── lib.rs ├── macros ├── Cargo.toml ├── src │ └── lib.rs └── tests │ └── test.rs ├── src └── lib.rs └── tests └── basic.rs /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | pull_request: 4 | push: 5 | schedule: 6 | - cron: '53 1 * * *' # *-*-* 01:53:00 UTC 7 | 8 | permissions: 9 | contents: read 10 | 11 | env: 12 | RUST_BACKTRACE: full 13 | # Workaround: https://github.com/rust-lang/rust/issues/113436 14 | RUSTFLAGS: -Dwarnings -Aimproper-ctypes-definitions 15 | 16 | jobs: 17 | test: 18 | name: Rust ${{matrix.rust}} 19 | runs-on: ubuntu-latest 20 | strategy: 21 | fail-fast: false 22 | matrix: 23 | rust: [nightly, beta, stable] 24 | timeout-minutes: 15 25 | steps: 26 | - name: Checkout 27 | uses: actions/checkout@v4 28 | 29 | - name: Install Rust 30 | run: | 31 | rustup update --no-self-update ${{ matrix.rust }} 32 | rustup default ${{ matrix.rust }} 33 | 34 | - name: Enable type layout randomization 35 | run: echo RUSTFLAGS=${RUSTFLAGS}\ -Zrandomize-layout >> $GITHUB_ENV 36 | if: matrix.rust == 'nightly' 37 | 38 | - run: cargo test --workspace 39 | 40 | clippy: 41 | name: Clippy 42 | runs-on: ubuntu-latest 43 | timeout-minutes: 15 44 | steps: 45 | - name: Checkout 46 | uses: actions/checkout@v4 47 | 48 | - name: Install Rust stable 49 | run: | 50 | rustup update --no-self-update stable 51 | rustup default stable 52 | 53 | - run: | 54 | cargo clippy --workspace --tests -- -Dclippy::all 55 | 56 | miri: 57 | name: Miri 58 | runs-on: ubuntu-latest 59 | timeout-minutes: 15 60 | steps: 61 | - name: Checkout 62 | uses: actions/checkout@v4 63 | 64 | - name: Install Rust nightly 65 | run: | 66 | rustup update --no-self-update nightly 67 | rustup default nightly 68 | rustup component add miri 69 | 70 | # Miri is only run on the main crate. 71 | # Workaround: `abi_stable` is disabled. It breaks rustc now. 72 | # See https://github.com/rust-lang/rust/issues/113900 73 | - run: cargo miri test 74 | env: 75 | MIRIFLAGS: -Zmiri-strict-provenance 76 | 77 | minimal: 78 | name: MSRV 1.56 79 | runs-on: ubuntu-latest 80 | timeout-minutes: 15 81 | steps: 82 | - name: Checkout 83 | uses: actions/checkout@v4 84 | 85 | - name: Install Rust 86 | run: | 87 | rustup update --no-self-update nightly 88 | rustup default 1.56 89 | 90 | - run: cargo +nightly generate-lockfile -Z minimal-versions 91 | 92 | - run: cargo check --locked 93 | -------------------------------------------------------------------------------- /.github/workflows/future_proof.yaml: -------------------------------------------------------------------------------- 1 | name: Future proof tests 2 | on: 3 | schedule: 4 | - cron: '3 10 * * 0' # Sun *-*-* 10:03:00 UTC 5 | 6 | permissions: 7 | contents: read 8 | 9 | env: 10 | RUST_BACKTRACE: full 11 | 12 | jobs: 13 | outdated: 14 | name: Outdated 15 | runs-on: ubuntu-latest 16 | timeout-minutes: 15 17 | steps: 18 | - name: Checkout 19 | uses: actions/checkout@v4 20 | 21 | - name: Install cargo-outdated 22 | uses: dtolnay/install@cargo-outdated 23 | 24 | - name: cargo-outdated 25 | # See `Cargo.toml` for why ignoring `tokio`. 26 | run: cargo outdated --workspace --exit-code 1 --ignore tokio 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | Cargo.lock 3 | /.vscode 4 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # 0.5.0 2 | - [major] Tweak parameter drop-order of proc-macro generated code to align with 3 | the ordinary `async fn`. 4 | - [minor] Bump MSRV to 1.56, due to syn 2 dependency of proc-macro. 5 | - [fix] Fix `ref mut` pattern handling in proc-macro. 6 | - [fix] Suppress warnings on Clippy nightly. 7 | - [fix] Fix various typos. 8 | 9 | # 0.4.1 10 | 11 | - [minor] Bump dependency `abi_stable` to 0.11. 12 | - [minor] Proc-macro helper `#[async_ffi]`. 13 | - [fix] Unignore tests for `miri` since they work now. 14 | - [fix] Reorganize and clean up documentations. 15 | 16 | # 0.4.0 17 | 18 | - [minor] Add an optional feature `abi_stable` to derive `StableAbi`. 19 | Some internal structs are tweaked to fit the requirement of `StableAbi`, 20 | but the interface C ABI is unchanged. 21 | - [fix] Tweak crate descriptions. 22 | - [fix] Ignore tests using `tokio` on `miri` interpreter. 23 | 24 | # 0.3.1 25 | 26 | - [fix] Abort when panicking across the FFI boundary in corner cases. (#8) 27 | 28 | `Future::drop`, panic payload from `Future::poll`, all `Waker` vtable functions `Waker::*` are 29 | are now wrapped in `std::panic::catch_unwind`. An `abort` will be emitted when panic occurs, 30 | since these functions are infallible and doesn't have sane value to return when panicking. 31 | A short message would be printed to stderr before `abort`. 32 | 33 | - [fix] `FfiContext::with_context` is actually safe. 34 | 35 | # 0.3.0 36 | 37 | - [major] Introduce a Poll variant `Panicked` which returns when `Future::poll` panicked in order to 38 | propagate panic to the host. 39 | - [minor] Public `FfiPoll` and `FfiContext`. 40 | 41 | # 0.2.1 42 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "async-ffi" 3 | version = "0.5.0" 4 | edition = "2018" 5 | description = "FFI-compatible `Future`s" 6 | keywords = ["ffi", "async", "futures"] 7 | categories = ["asynchronous", "network-programming", "development-tools::ffi"] 8 | license = "MIT" 9 | repository = "https://github.com/oxalica/async-ffi" 10 | readme = "README.md" 11 | exclude = ["/link_tests", "/.github"] 12 | rust-version = "1.56" # syn 2 requires edition 2021 13 | 14 | [dependencies] 15 | abi_stable = { version = "0.11", default-features = false, optional = true } 16 | macros = { version = "0.5", package = "async-ffi-macros", path = "./macros", optional = true } 17 | 18 | [dev-dependencies] 19 | tokio = { version = "1", features = ["macros", "rt-multi-thread", "sync", "time"] } 20 | 21 | [workspace] 22 | 23 | [package.metadata.docs.rs] 24 | all-features = true 25 | rustdoc-args = ["--cfg", "docsrs"] 26 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Permission is hereby granted, free of charge, to any 2 | person obtaining a copy of this software and associated 3 | documentation files (the "Software"), to deal in the 4 | Software without restriction, including without 5 | limitation the rights to use, copy, modify, merge, 6 | publish, distribute, sublicense, and/or sell copies of 7 | the Software, and to permit persons to whom the Software 8 | is furnished to do so, subject to the following 9 | conditions: 10 | 11 | The above copyright notice and this permission notice 12 | shall be included in all copies or substantial portions 13 | of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF 16 | ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 17 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 18 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT 19 | SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 20 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 22 | IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 23 | DEALINGS IN THE SOFTWARE. 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # async-ffi: FFI-compatible `Future`s 2 | 3 | [![crates.io](https://img.shields.io/crates/v/async-ffi)](https://crates.io/crates/async-ffi) 4 | [![docs.rs](https://img.shields.io/docsrs/async-ffi)][docs] 5 | [![CI](https://github.com/oxalica/async-ffi/actions/workflows/ci.yaml/badge.svg)](https://github.com/oxalica/async-ffi/actions/workflows/ci.yaml) 6 | 7 | Convert your Rust `Future`s into a FFI-compatible struct without relying unstable Rust ABI and struct layout. 8 | Easily provide async functions in dynamic library maybe compiled with different Rust than the invoker. 9 | 10 | See [documentation][docs] for more details. 11 | 12 | See [`link_tests`](link_tests) directory for cross-linking examples. 13 | 14 | [docs]: https://docs.rs/async-ffi 15 | 16 | #### License 17 | 18 | MIT Licensed. 19 | -------------------------------------------------------------------------------- /link_tests/Makefile: -------------------------------------------------------------------------------- 1 | PWD = $(shell pwd) 2 | TARGET_DIR = $(PWD)/target 3 | CRATE = $(PWD)/../Cargo.lock $(PWD)/../Cargo.toml $(PWD)/../src/lib.rs 4 | 5 | .PHONY: all 6 | all: $(TARGET_DIR)/executor_rust $(TARGET_DIR)/executor_c $(TARGET_DIR)/libplugin_rust.so $(TARGET_DIR)/libplugin_c.so 7 | 8 | .PHONY: clean 9 | clean: 10 | -rm -rf $(TARGET_DIR)/ 11 | 12 | $(TARGET_DIR)/executor_rust: executor-rust/src/main.rs executor-rust/Cargo.toml $(CRATE) 13 | cd executor-rust && cargo build --target-dir=$(TARGET_DIR) 14 | cp -ft $(TARGET_DIR)/ $(TARGET_DIR)/debug/executor_rust 15 | 16 | $(TARGET_DIR)/libplugin_rust.so: plugin-rust/src/lib.rs plugin-rust/Cargo.toml $(CRATE) 17 | cd plugin-rust && cargo build --target-dir=$(TARGET_DIR) 18 | cp -ft $(TARGET_DIR)/ $(TARGET_DIR)/debug/libplugin_rust.so 19 | 20 | $(TARGET_DIR)/libplugin_c.so: plugin-c/plugin.c 21 | gcc $< -o $@ -fPIC -shared -pthread 22 | 23 | $(TARGET_DIR)/executor_c: executor-c/main.c 24 | gcc $< -o $@ -pthread -ldl -std=c11 -Wall -Wextra 25 | 26 | .PHONY: test 27 | test: test-rust-rust test-rust-c test-c-rust 28 | 29 | .PHONY: test-rust-rust 30 | test-rust-rust: $(TARGET_DIR)/executor_rust $(TARGET_DIR)/libplugin_rust.so 31 | time $? 32 | 33 | .PHONY: test-rust-c 34 | test-rust-c: $(TARGET_DIR)/executor_rust $(TARGET_DIR)/libplugin_c.so 35 | time $? 36 | 37 | .PHONY: test-c-rust 38 | test-c-rust: $(TARGET_DIR)/executor_c $(TARGET_DIR)/libplugin_rust.so 39 | time $? 40 | 41 | # Why do you ever have this case? 42 | # .PHONY: test-c-c 43 | # test-c-c: $(TARGET_DIR)/executor_c $(TARGET_DIR)/libplugin_c.so 44 | # time $? 45 | -------------------------------------------------------------------------------- /link_tests/executor-c/main.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | // Data structures. 10 | 11 | struct FfiWakerVTable { 12 | struct FfiWaker const *(*clone)(struct FfiWaker const *); 13 | void (*wake)(struct FfiWaker *); 14 | void (*wake_by_ref)(struct FfiWaker const *); 15 | void (*drop)(struct FfiWaker *); 16 | }; 17 | 18 | struct FfiWaker { 19 | struct FfiWakerVTable const *vtable; 20 | 21 | // Store some extra trailing data to wake up the executor loop. 22 | mtx_t *mutex; 23 | cnd_t *condvar; 24 | int *awake; 25 | }; 26 | 27 | struct FfiContext { 28 | struct FfiWaker const *waker_ref; 29 | }; 30 | 31 | struct PollU32 { 32 | uint8_t is_pending; 33 | union { uint32_t value; }; 34 | }; 35 | 36 | struct FfiFutureU32 { 37 | void *fut; 38 | struct PollU32 (*poll)(void *fut, struct FfiContext *context); 39 | void (*drop)(void *fut); 40 | }; 41 | 42 | // Waker virtual functions. 43 | 44 | struct FfiWaker const *waker_clone(struct FfiWaker const *w) { 45 | struct FfiWaker *p = malloc(sizeof(struct FfiWaker)); 46 | assert(p); 47 | *p = *w; 48 | return p; 49 | } 50 | 51 | void waker_wake_by_ref(struct FfiWaker const *w) { 52 | puts("Wake"); 53 | mtx_lock(w->mutex); 54 | *w->awake = 1; 55 | cnd_signal(w->condvar); 56 | mtx_unlock(w->mutex); 57 | } 58 | 59 | void waker_drop(struct FfiWaker *w) { 60 | free(w); 61 | } 62 | 63 | void waker_wake(struct FfiWaker *w) { 64 | waker_wake_by_ref(w); 65 | waker_drop(w); 66 | } 67 | 68 | struct FfiWakerVTable waker_vtable = { 69 | .clone = &waker_clone, 70 | .wake = &waker_wake, 71 | .wake_by_ref = &waker_wake_by_ref, 72 | .drop = &waker_drop, 73 | }; 74 | 75 | // Executor. 76 | 77 | typedef struct FfiFutureU32 (*plugin_run_fn_t)(uint32_t, uint32_t); 78 | 79 | void execute(plugin_run_fn_t fn) { 80 | // Waker may outlive the executor itself, so data referenced by waker need to be reference-counted. 81 | // Here we simply use global one since the executor is run only once. 82 | static mtx_t mutex; 83 | static cnd_t condvar; 84 | static int awake = 0; 85 | mtx_init(&mutex, mtx_plain); 86 | cnd_init(&condvar); 87 | 88 | struct FfiWaker waker = { 89 | .vtable = &waker_vtable, 90 | .mutex = &mutex, 91 | .condvar = &condvar, 92 | .awake = &awake, 93 | }; 94 | struct FfiContext ctx = { .waker_ref = &waker }; 95 | 96 | puts("Calling future"); 97 | struct FfiFutureU32 fut = fn(42, 1); 98 | 99 | struct PollU32 ret; 100 | while (1) { 101 | puts("Polling future"); 102 | ret = (fut.poll)(fut.fut, &ctx); 103 | printf("-> is_pending: %d\n", ret.is_pending); 104 | if (!ret.is_pending) 105 | break; 106 | 107 | // When `poll` returns `Pending`, it automatically hook the waker to some reactor, which will 108 | // be called `wake` or `wake_by_ref` if the future is ready to be `poll`ed again. 109 | // So we wait on the condvar until someone wake us again. 110 | mtx_lock(&mutex); 111 | while (!awake) 112 | cnd_wait(&condvar, &mutex); 113 | awake = 0; 114 | mtx_unlock(&mutex); 115 | } 116 | // Drop the future when finished or canceled. 117 | (fut.drop)(fut.fut); 118 | 119 | printf("42 + 1 = %" PRIu32 "\n", ret.value); 120 | } 121 | 122 | int main (int argc, char const **argv) { 123 | assert(argc == 2); 124 | char const *lib_path = argv[1]; 125 | 126 | void *dl = dlopen(lib_path, RTLD_LAZY); 127 | if (!dl) { 128 | fprintf(stderr, "dlopen failed: %s\n", dlerror()); 129 | exit(1); 130 | } 131 | dlerror(); // Clear errno. 132 | 133 | plugin_run_fn_t fn = dlsym(dl, "plugin_run"); 134 | char *dlerr; 135 | if ((dlerr = dlerror()) != 0) { 136 | fprintf(stderr, "dlsym failed: %s\n", dlerr); 137 | dlclose(dl); 138 | exit(1); 139 | } 140 | 141 | execute(fn); 142 | 143 | dlclose(dl); 144 | return 0; 145 | } 146 | -------------------------------------------------------------------------------- /link_tests/executor-rust/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "executor_rust" 3 | version = "0.1.0" 4 | edition = "2018" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | anyhow = "1.0.40" 10 | async-ffi = { path = "../.." } 11 | libloading = "0.7.0" 12 | tokio = { version = "1.4.0", features = ["macros", "rt-multi-thread"] } 13 | 14 | [workspace] 15 | -------------------------------------------------------------------------------- /link_tests/executor-rust/src/main.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{Context as _, Result}; 2 | use async_ffi::FfiFuture; 3 | use libloading::{Library, Symbol}; 4 | use std::time::{Duration, Instant}; 5 | 6 | // Some plugin fn. 7 | type PluginRunFn = unsafe extern "C" fn(a: u32, b: u32) -> FfiFuture; 8 | 9 | #[tokio::main] 10 | async fn main() -> Result<()> { 11 | let path = std::env::args().nth(1).context("Missing argument")?; 12 | 13 | unsafe { 14 | let lib = Library::new(&path).context("Cannot load library")?; 15 | let plugin_run: Symbol = lib.get(b"plugin_run")?; 16 | 17 | let t = Instant::now(); 18 | let ret = plugin_run(42, 1).await; 19 | // We did some async sleep in plugin_run. 20 | assert!(Duration::from_millis(500) < t.elapsed()); 21 | assert_eq!(ret, 43); 22 | println!("42 + 1 = {}", ret); 23 | } 24 | 25 | Ok(()) 26 | } 27 | -------------------------------------------------------------------------------- /link_tests/plugin-c/plugin.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | struct FfiWakerVTable { 10 | struct FfiWaker const *(*clone)(struct FfiWaker const *); 11 | void (*wake)(struct FfiWaker const *); 12 | void (*wake_by_ref)(struct FfiWaker const *); 13 | void (*drop)(struct FfiWaker const *); 14 | }; 15 | 16 | struct FfiWaker { 17 | struct FfiWakerVTable const *vtable; 18 | }; 19 | 20 | struct FfiContext { 21 | struct FfiWaker const *waker_ref; 22 | }; 23 | 24 | struct PollU32 { 25 | uint8_t is_pending; 26 | union { uint32_t value; }; 27 | }; 28 | 29 | struct FfiFutureU32 { 30 | void *fut; 31 | struct PollU32 (*poll)(void *fut, struct FfiContext *context); 32 | void (*drop)(void *fut); 33 | }; 34 | 35 | struct my_data { 36 | uint32_t state; 37 | uint32_t a, b, ret; 38 | pthread_t handle; 39 | struct FfiWaker const *waker; 40 | }; 41 | 42 | static void *handler (void *data_raw) { 43 | struct my_data *data = (struct my_data *)data_raw; 44 | usleep(500000); 45 | data->ret = data->a + data->b; 46 | atomic_store(&data->state, 2); 47 | (data->waker->vtable->wake)(data->waker); 48 | } 49 | 50 | static struct PollU32 fut_poll (void *fut, struct FfiContext *context) { 51 | struct my_data *data = (struct my_data *)fut; 52 | pthread_t handle; 53 | switch (atomic_load(&data->state)) { 54 | case 0: 55 | data->waker = (context->waker_ref->vtable->clone)(context->waker_ref); 56 | data->state = 1; 57 | pthread_create(&data->handle, NULL, handler, (void *)data); 58 | case 1: 59 | return (struct PollU32) { .is_pending = 1 }; 60 | case 2: 61 | pthread_join(data->handle, NULL); 62 | data->handle = 0; 63 | return (struct PollU32) { .is_pending = 0, .value = data->ret }; 64 | } 65 | } 66 | 67 | static void fut_drop(void *fut) { 68 | struct my_data *data = (struct my_data *)fut; 69 | if (data->handle != 0) { 70 | pthread_kill(data->handle, SIGKILL); 71 | pthread_join(data->handle, NULL); 72 | } 73 | free(data); 74 | } 75 | 76 | struct FfiFutureU32 plugin_run (uint32_t a, uint32_t b) { 77 | struct my_data *data = malloc(sizeof(struct my_data)); 78 | data->handle = 0; 79 | data->state = 0; 80 | data->a = a; 81 | data->b = b; 82 | struct FfiFutureU32 fut = { 83 | .fut = (void *)data, 84 | .poll = fut_poll, 85 | .drop = fut_drop, 86 | }; 87 | return fut; 88 | } 89 | -------------------------------------------------------------------------------- /link_tests/plugin-rust/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "plugin-rust" 3 | version = "0.1.0" 4 | edition = "2018" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [lib] 9 | crate-type = ["cdylib"] 10 | 11 | [dependencies] 12 | async-ffi = { path = "../..", features = ["macros"] } 13 | async-std = "1.9.0" 14 | 15 | [workspace] 16 | -------------------------------------------------------------------------------- /link_tests/plugin-rust/src/lib.rs: -------------------------------------------------------------------------------- 1 | #[async_ffi::async_ffi] 2 | #[no_mangle] 3 | pub async unsafe extern "C" fn plugin_run(a: u32, b: u32) -> u32 { 4 | async_std::task::sleep(std::time::Duration::from_millis(500)).await; 5 | a + b 6 | } 7 | -------------------------------------------------------------------------------- /macros/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "async-ffi-macros" 3 | version = "0.5.0" 4 | edition = "2018" # Follows async-ffi 5 | description = "Macros for async-ffi" 6 | keywords = ["ffi", "async", "futures"] 7 | categories = ["asynchronous", "network-programming", "development-tools::ffi"] 8 | license = "MIT" 9 | repository = "https://github.com/oxalica/async-ffi" 10 | readme = "../README.md" 11 | rust-version = "1.56" # syn 2 requires edition 2021 12 | 13 | [lib] 14 | proc-macro = true 15 | 16 | [dependencies] 17 | proc-macro2 = "1" 18 | quote = "1" 19 | syn = { version = "2", features = ["full"] } 20 | 21 | [dev-dependencies] 22 | async-ffi.path = ".." 23 | futures-task = "0.3" 24 | -------------------------------------------------------------------------------- /macros/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! Helper macros for `async_ffi::FfiFuture`. 2 | use std::mem; 3 | 4 | use proc_macro::TokenStream as RawTokenStream; 5 | use proc_macro2::{Ident, Span, TokenStream, TokenTree}; 6 | use quote::{quote, quote_spanned, ToTokens}; 7 | use syn::parse::{Parse, ParseStream, Result}; 8 | use syn::spanned::Spanned; 9 | use syn::{ 10 | parse_quote_spanned, Attribute, Block, Error, FnArg, ForeignItemFn, GenericParam, ItemFn, 11 | Lifetime, LifetimeParam, Pat, PatIdent, Signature, Token, 12 | }; 13 | 14 | /// A helper macro attribute to converts an `async fn` into a ordinary `fn` returning `FfiFuture`. 15 | /// 16 | /// Note that this crate doesn't automatically pulls in `async_ffi` dependency. You must manually 17 | /// add `async_ffi` dependency to your `Cargo.toml`. Or alternatively, using `macros` feature of 18 | /// `async_ffi` which re-export the macro from this crate, instead of pulling in this crate 19 | /// explicitly. 20 | /// 21 | /// # Usages 22 | /// 23 | /// The typical usage is to apply this macro to an `async fn`. 24 | /// ``` 25 | /// # async fn do_work(_: i32) {} 26 | /// use async_ffi_macros::async_ffi; 27 | /// // Or if you have `macros` feature of `async_ffi` enabled, 28 | /// // use async_ffi::async_ffi; 29 | /// 30 | /// #[async_ffi] 31 | /// #[no_mangle] 32 | /// async fn func(x: i32) -> i32 { 33 | /// do_work(x).await; 34 | /// x + 42 35 | /// } 36 | /// ``` 37 | /// 38 | /// It would be converted into roughly: 39 | /// ``` 40 | /// # async fn do_work(_: i32) {} 41 | /// #[no_mangle] 42 | /// extern "C" fn func(x: i32) -> ::async_ffi::FfiFuture { 43 | /// ::async_ffi::FfiFuture::new(async move { 44 | /// // NB. Arguments are always moved into the result Future, no matter if it's used. 45 | /// // This is the same behavior as original `async fn`s. 46 | /// let x = x; 47 | /// 48 | /// do_work(x).await; 49 | /// x + 42 50 | /// }) 51 | /// } 52 | /// ``` 53 | /// 54 | /// You can also apply `#[async_ffi]` to external functions. 55 | /// ``` 56 | /// # use async_ffi_macros::async_ffi; 57 | /// extern "C" { 58 | /// #[async_ffi] 59 | /// async fn extern_fn(arg: i32) -> i32; 60 | /// // => fn extern_fn(arg: i32) -> ::async_ffi::FfiFuture; 61 | /// } 62 | /// ``` 63 | /// 64 | /// ## Non-`Send` futures 65 | /// Call the macro with arguments `?Send` to wrap the result into `LocalFfiFuture` instead of 66 | /// `FfiFuture`. 67 | /// 68 | /// ``` 69 | /// # use async_ffi_macros::async_ffi; 70 | /// #[async_ffi(?Send)] 71 | /// async fn func() {} 72 | /// // => fn func() -> ::async_ffi::LocalFfiFuture<()> { ... } 73 | /// ``` 74 | /// 75 | /// ## References in parameters 76 | /// When parameters of your `async fn` contain references, you need to capture their lifetimes in 77 | /// the result `FfiFuture`. Currently, we don't expand lifetime elisions. You must explicitly give 78 | /// the result lifetime a name in macro arguments and specify all bounds if necessary. 79 | /// 80 | /// ``` 81 | /// # use async_ffi_macros::async_ffi; 82 | /// #[async_ffi('fut)] 83 | /// async fn borrow(x: &'fut i32, y: &'fut i32) -> i32 { *x + *y } 84 | /// // => fn borrow<'fut>(x: &'fut i32) -> ::async_ffi::BorrowingFfiFuture<'fut, i32> { ... } 85 | /// 86 | /// // In complex cases, explicit bounds are necessary. 87 | /// #[async_ffi('fut)] 88 | /// async fn complex<'a: 'fut, 'b: 'fut>(x: &'a mut i32, y: &'b mut i32) -> i32 { *x + *y } 89 | /// // => fn complex<'a: 'fut, 'b: 'fut, 'fut>(x: &'a mut i32, y: &'b mut i32) -> BorrowingFfiFuture<'fut, i32> { ... } 90 | /// 91 | /// // Non Send async fn can also work together. 92 | /// #[async_ffi('fut, ?Send)] 93 | /// async fn non_send(x: &'fut i32, y: &'fut i32) -> i32 { *x } 94 | /// // => fn non_send<'fut>(x: &'fut i32) -> ::async_ffi::LocalBorrowingFfiFuture<'fut, i32> { ... } 95 | /// ``` 96 | #[proc_macro_attribute] 97 | pub fn async_ffi(args: RawTokenStream, input: RawTokenStream) -> RawTokenStream { 98 | async_ffi_inner(args.into(), input.into()).into() 99 | } 100 | 101 | fn async_ffi_inner(args: TokenStream, mut input: TokenStream) -> TokenStream { 102 | let mut errors = Vec::new(); 103 | let args = syn::parse2::(args) 104 | .map_err(|err| errors.push(err)) 105 | .unwrap_or_default(); 106 | if matches!(input.clone().into_iter().last(), Some(TokenTree::Punct(p)) if p.as_char() == ';') { 107 | match syn::parse2::(input.clone()) { 108 | Ok(mut item) => { 109 | expand(&mut item.attrs, &mut item.sig, None, args, &mut errors); 110 | input = item.to_token_stream(); 111 | } 112 | Err(err) => errors.push(err), 113 | } 114 | } else { 115 | match syn::parse2::(input.clone()) { 116 | Ok(mut item) => { 117 | expand( 118 | &mut item.attrs, 119 | &mut item.sig, 120 | Some(&mut item.block), 121 | args, 122 | &mut errors, 123 | ); 124 | input = item.to_token_stream(); 125 | } 126 | Err(err) => errors.push(err), 127 | } 128 | } 129 | for err in errors { 130 | input.extend(err.into_compile_error()); 131 | } 132 | input 133 | } 134 | 135 | mod kw { 136 | syn::custom_keyword!(Send); 137 | } 138 | 139 | #[derive(Default)] 140 | struct Args { 141 | pub lifetime: Option, 142 | pub local: bool, 143 | } 144 | 145 | impl Parse for Args { 146 | fn parse(input: ParseStream) -> Result { 147 | let mut this = Self::default(); 148 | if input.peek(Lifetime) { 149 | this.lifetime = Some(input.parse::()?); 150 | if input.peek(Token![,]) { 151 | input.parse::()?; 152 | } 153 | } 154 | if input.peek(Token![?]) { 155 | input.parse::()?; 156 | input.parse::()?; 157 | this.local = true; 158 | } 159 | if !input.is_empty() { 160 | return Err(Error::new( 161 | Span::call_site(), 162 | "invalid arguments for #[async_ffi]", 163 | )); 164 | } 165 | Ok(this) 166 | } 167 | } 168 | 169 | fn expand( 170 | attrs: &mut Vec, 171 | sig: &mut Signature, 172 | body: Option<&mut Block>, 173 | args: Args, 174 | errors: &mut Vec, 175 | ) { 176 | let mut emit_err = 177 | |span: Span, msg: &str| errors.push(Error::new(span, format!("#[async_ffi] {}", msg))); 178 | 179 | let async_span = if let Some(tok) = sig.asyncness.take() { 180 | tok.span 181 | } else { 182 | if body.is_some() { 183 | emit_err(sig.fn_token.span, "expects an `async fn`"); 184 | } 185 | Span::call_site() 186 | }; 187 | 188 | attrs.push(parse_quote_spanned!(async_span=> #[allow(clippy::needless_lifetimes)])); 189 | attrs.push(parse_quote_spanned!(async_span=> #[must_use])); 190 | 191 | let lifetime = match args.lifetime { 192 | None => Lifetime::new("'static", Span::call_site()), 193 | Some(lifetime) => { 194 | // Add the lifetime into generic parameters, at the end of existing lifetimes. 195 | sig.generics.lt_token.get_or_insert(Token![<](async_span)); 196 | sig.generics.gt_token.get_or_insert(Token![>](async_span)); 197 | let lifetime_cnt = sig.generics.lifetimes_mut().count(); 198 | sig.generics.params.insert( 199 | lifetime_cnt, 200 | GenericParam::Lifetime(LifetimeParam::new(lifetime.clone())), 201 | ); 202 | 203 | lifetime 204 | } 205 | }; 206 | 207 | let ffi_future = if args.local { 208 | quote_spanned!(async_span=> ::async_ffi::LocalBorrowingFfiFuture) 209 | } else { 210 | quote_spanned!(async_span=> ::async_ffi::BorrowingFfiFuture) 211 | }; 212 | 213 | match &mut sig.output { 214 | syn::ReturnType::Default => { 215 | sig.output = parse_quote_spanned!(async_span=> -> #ffi_future<#lifetime, ()>); 216 | } 217 | syn::ReturnType::Type(_r_arrow, ret_ty) => { 218 | *ret_ty = parse_quote_spanned!(async_span=> #ffi_future<#lifetime, #ret_ty>); 219 | } 220 | } 221 | 222 | if let Some(va) = &sig.variadic { 223 | emit_err(va.span(), "does not support variadic parameters"); 224 | } 225 | 226 | // Force capturing all arguments in the returned Future. 227 | // This is the behavior of `async fn`. 228 | let mut param_bindings = TokenStream::new(); 229 | for (param, i) in sig.inputs.iter_mut().zip(1..) { 230 | let pat_ty = match param { 231 | FnArg::Receiver(receiver) => { 232 | emit_err(receiver.span(), "does not support `self` parameter"); 233 | continue; 234 | } 235 | FnArg::Typed(pat_ty) => pat_ty, 236 | }; 237 | 238 | let attributes = &pat_ty.attrs; 239 | let param_ident = match &*pat_ty.pat { 240 | Pat::Ident(pat_ident) => { 241 | if pat_ident.ident == "self" { 242 | emit_err(pat_ident.span(), "does not support `self` parameter"); 243 | continue; 244 | } 245 | pat_ident.ident.clone() 246 | } 247 | _ => Ident::new(&format!("__param{}", i), pat_ty.span()), 248 | }; 249 | 250 | // If this is a declaration, only check but not transform. 251 | if body.is_none() { 252 | continue; 253 | } 254 | 255 | let old_pat = mem::replace( 256 | &mut *pat_ty.pat, 257 | Pat::Ident(PatIdent { 258 | attrs: Vec::new(), 259 | by_ref: None, 260 | mutability: None, 261 | ident: param_ident.clone(), 262 | subpat: None, 263 | }), 264 | ); 265 | 266 | // NB. 267 | // - Rebind the parameter once, to ensure their drop order not broken 268 | // by non-moving patterns containing `_`. 269 | // - `mut` is required when the old pattern has `ref mut` inside. 270 | // - Use external (macro) spans, so they won't trigger lints. 271 | param_bindings.extend(quote! { 272 | #(#attributes)* 273 | #[allow(clippy::used_underscore_binding)] 274 | let mut #param_ident = #param_ident; 275 | #(#attributes)* 276 | #[allow(clippy::used_underscore_binding)] 277 | let #old_pat = #param_ident; 278 | }); 279 | } 280 | 281 | if let Some(body) = body { 282 | let stmts = mem::take(&mut body.stmts); 283 | body.stmts = parse_quote_spanned! {async_span=> 284 | #ffi_future::new(async move { 285 | #param_bindings 286 | #(#stmts)* 287 | }) 288 | }; 289 | } 290 | } 291 | 292 | #[cfg(doctest)] 293 | mod tests { 294 | /// ```compile_fail 295 | /// #[async_ffi_macros::async_ffi] 296 | /// pub fn not_async() {} 297 | /// ``` 298 | fn not_async() {} 299 | 300 | /// ```compile_fail 301 | /// pub trait Trait { 302 | /// #[async_ffi_macros::async_ffi] 303 | /// async fn method(&self); 304 | /// } 305 | /// ``` 306 | fn receiver_trait_method() {} 307 | 308 | /// ```compile_fail 309 | /// struct Struct; 310 | /// impl Struct { 311 | /// #[async_ffi_macros::async_ffi] 312 | /// async fn method(&self) {} 313 | /// } 314 | /// ``` 315 | fn receiver_impl_method() {} 316 | 317 | /// ```compile_fail 318 | /// struct Struct; 319 | /// impl Struct { 320 | /// #[async_ffi_macros::async_ffi] 321 | /// async fn method(self: &mut Self) {} 322 | /// } 323 | /// ``` 324 | fn typed_receiver() {} 325 | 326 | /// ```compile_fail 327 | /// extern "C" { 328 | /// #[async_ffi_macros::async_ffi] 329 | /// async fn foo(ref mut x: i32); 330 | /// } 331 | /// ``` 332 | fn declaration_pattern() {} 333 | } 334 | -------------------------------------------------------------------------------- /macros/tests/test.rs: -------------------------------------------------------------------------------- 1 | use async_ffi::{BorrowingFfiFuture, FfiFuture, LocalBorrowingFfiFuture, LocalFfiFuture}; 2 | use async_ffi_macros::async_ffi; 3 | 4 | #[async_ffi] 5 | #[no_mangle] 6 | async extern "C" fn empty() {} 7 | 8 | const _: extern "C" fn() -> FfiFuture<()> = empty; 9 | 10 | #[async_ffi(?Send)] 11 | #[no_mangle] 12 | async extern "C" fn local() -> i32 { 13 | 42 14 | } 15 | 16 | const _: extern "C" fn() -> LocalFfiFuture = local; 17 | 18 | #[async_ffi] 19 | #[no_mangle] 20 | async extern "C" fn has_args(x: i32) -> i32 { 21 | x 22 | } 23 | 24 | const _: extern "C" fn(i32) -> FfiFuture = has_args; 25 | 26 | extern "C" { 27 | #[async_ffi] 28 | pub async fn extern_fn(arg1: u32) -> u32; 29 | } 30 | 31 | const _: unsafe extern "C" fn(u32) -> FfiFuture = extern_fn; 32 | 33 | #[async_ffi('fut)] 34 | pub async fn borrow(a: &'fut i32) -> i32 { 35 | *a 36 | } 37 | 38 | const _: for<'a> fn(&'a i32) -> BorrowingFfiFuture<'a, i32> = borrow; 39 | 40 | #[async_ffi('fut, ?Send)] 41 | pub async fn borrow_non_send(a: &'fut i32) -> i32 { 42 | *a 43 | } 44 | 45 | const _: for<'a> fn(&'a i32) -> LocalBorrowingFfiFuture<'a, i32> = borrow_non_send; 46 | 47 | #[async_ffi] 48 | pub async fn pat_param(_: i32, (a, _, _b): (i32, i32, i32), x: i32, mut y: i32) -> i32 { 49 | y += 1; 50 | a + x + y 51 | } 52 | 53 | #[test] 54 | #[allow(clippy::toplevel_ref_arg, clippy::unused_async)] 55 | fn drop_order() { 56 | use std::cell::RefCell; 57 | use std::future::Future; 58 | use std::pin::Pin; 59 | use std::rc::Rc; 60 | use std::task::{Context, Poll}; 61 | 62 | struct Dropper(&'static str, Rc>>); 63 | 64 | impl Drop for Dropper { 65 | fn drop(&mut self) { 66 | self.1.borrow_mut().push(self.0); 67 | } 68 | } 69 | 70 | fn check_order( 71 | f: fn(Dropper, Dropper, Dropper, (Dropper, Dropper)) -> F, 72 | ) -> Vec<&'static str> 73 | where 74 | F: Future, 75 | { 76 | let order = Rc::new(RefCell::new(Vec::new())); 77 | let mut fut = f( 78 | Dropper("a", order.clone()), 79 | Dropper("b", order.clone()), 80 | Dropper("_arg", order.clone()), 81 | (Dropper("c", order.clone()), Dropper("_pat", order.clone())), 82 | ); 83 | Dropper("ret", order.clone()); 84 | let fut_pinned = unsafe { Pin::new_unchecked(&mut fut) }; 85 | let mut cx = Context::from_waker(futures_task::noop_waker_ref()); 86 | assert_eq!(fut_pinned.poll(&mut cx), Poll::Ready(())); 87 | Dropper("done", order.clone()); 88 | drop(fut); 89 | let ret = order.borrow().to_vec(); 90 | ret 91 | } 92 | 93 | #[async_ffi(?Send)] 94 | async fn func1(a: Dropper, ref mut _b: Dropper, _: Dropper, (_c, _): (Dropper, Dropper)) { 95 | a.1.borrow_mut().push("run"); 96 | } 97 | async fn func2(a: Dropper, ref mut _b: Dropper, _: Dropper, (_c, _): (Dropper, Dropper)) { 98 | a.1.borrow_mut().push("run"); 99 | } 100 | 101 | let ord1 = check_order(func1); 102 | let ord2 = check_order(func2); 103 | assert_eq!(ord1, ord2); 104 | } 105 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | //! # FFI-compatible [`Future`][`std::future::Future`]s 2 | //! 3 | //! Rust currently doesn't provide stable ABI nor stable layout of related structs like 4 | //! `dyn Future` or `Waker`. 5 | //! With this crate, we can wrap async blocks or async functions to make a `Future` FFI-safe. 6 | //! 7 | //! [`FfiFuture`] provides the same functionality as `Box + Send>` but 8 | //! it's FFI-compatible, aka. `repr(C)`. Any `Future + Send + 'static` can be converted 9 | //! into [`FfiFuture`] by calling [`into_ffi`][`FutureExt::into_ffi`] on it, after `use`ing the 10 | //! trait [`FutureExt`]. 11 | //! 12 | //! [`FfiFuture`] implements `Future + Send`. You can `await` a [`FfiFuture`] just like 13 | //! a normal `Future` to wait and get the output. 14 | //! 15 | //! For non-[`Send`] or non-`'static` futures, see the section 16 | //! [Variants of `FfiFuture`](#variants-of-ffifuture) below. 17 | //! 18 | //! ## Examples 19 | //! 20 | //! Provide some async functions in library: (plugin side) 21 | //! ``` 22 | //! # async fn do_some_io(_: u32) -> u32 { 0 } 23 | //! # async fn do_some_sleep(_: u32) {} 24 | //! // Compile with `crate-type = ["cdylib"]`. 25 | //! use async_ffi::{FfiFuture, FutureExt}; 26 | //! 27 | //! #[no_mangle] 28 | //! pub extern "C" fn work(arg: u32) -> FfiFuture { 29 | //! async move { 30 | //! let ret = do_some_io(arg).await; 31 | //! do_some_sleep(42).await; 32 | //! ret 33 | //! } 34 | //! .into_ffi() 35 | //! } 36 | //! ``` 37 | //! 38 | //! Execute async functions from external library: (host or executor side) 39 | //! ``` 40 | //! use async_ffi::{FfiFuture, FutureExt}; 41 | //! 42 | //! // #[link(name = "myplugin...")] 43 | //! extern "C" { 44 | //! #[no_mangle] 45 | //! fn work(arg: u32) -> FfiFuture; 46 | //! } 47 | //! 48 | //! async fn run_work(arg: u32) -> u32 { 49 | //! unsafe { work(arg).await } 50 | //! } 51 | //! ``` 52 | //! 53 | //! ## Proc-macro helpers 54 | //! If you enable the feature `macros` (disabled by default), an attribute-like procedural macro 55 | #![cfg_attr(not(feature = "macros"), doc = r"`async_ffi`")] 56 | #![cfg_attr(feature = "macros", doc = r"[`async_ffi`]")] 57 | //! is available at top-level. See its own documentation for details. 58 | //! 59 | //! With the macro, the example above can be simplified to: 60 | //! ``` 61 | //! # #[cfg(feature = "macros")] { 62 | //! # async fn do_some_io(_: u32) -> u32 { 0 } 63 | //! # async fn do_some_sleep(_: u32) {} 64 | //! use async_ffi::async_ffi; 65 | //! 66 | //! #[no_mangle] 67 | //! #[async_ffi] 68 | //! pub async extern "C" fn work(arg: u32) -> u32 { 69 | //! let ret = do_some_io(arg).await; 70 | //! do_some_sleep(42).await; 71 | //! ret 72 | //! } 73 | //! # } 74 | //! ``` 75 | //! 76 | //! ## Panics 77 | //! 78 | //! You should know that 79 | //! [unwinding across an FFI boundary is Undefined Behaviour](https://doc.rust-lang.org/nomicon/ffi.html#ffi-and-panics). 80 | //! 81 | //! ### Panic in `Future::poll` 82 | //! 83 | //! Since the body of `async fn` is translated to [`Future::poll`] by the compiler, the `poll` 84 | //! method is likely to panic. If this happen, the wrapped [`FfiFuture`] will catch unwinding 85 | //! with [`std::panic::catch_unwind`], returning [`FfiPoll::Panicked`] to cross the FFI boundary. 86 | //! And the other side (usually the plugin host) will get this value in the implementation of 87 | //! ` as std::future::Future>::poll`, and explicit propagate the panic, 88 | //! just like [`std::sync::Mutex`]'s poisoning mechanism. 89 | //! 90 | //! ### Panic in `Future::drop` or any waker vtable functions `Waker::*` 91 | //! 92 | //! Unfortunately, this is very difficult to handle since drop cleanup and `Waker` functions are 93 | //! expected to be infallible. If these functions panic, we would just call [`std::process::abort`] 94 | //! to terminate the whole program. 95 | //! 96 | //! ## Variants of `FfiFuture` 97 | //! 98 | //! There are a few variants of [`FfiFuture`]. The table below shows their corresponding `std` 99 | //! type. 100 | //! 101 | //! | Type | The corresponding `std` type | 102 | //! |---------------------------------------------------------------|------------------------------------------------| 103 | //! | [`FfiFuture`] | `Box + Send + 'static>` | 104 | //! | [`LocalFfiFuture`] | `Box + 'static>` | 105 | //! | [`BorrowingFfiFuture<'a, T>`][`BorrowingFfiFuture`] | `Box + Send + 'a>` | 106 | //! | [`LocalBorrowingFfiFuture<'a, T>`][`LocalBorrowingFfiFuture`] | `Box + 'a>` | 107 | //! 108 | //! All of these variants are ABI-compatible to each other, since lifetimes and [`Send`] cannot be 109 | //! represented by the C ABI. These bounds are only checked in the Rust side. It's your duty to 110 | //! guarantee that the [`Send`] and lifetime bounds are respected in the foreign code of your 111 | //! external `fn`s. 112 | //! 113 | //! ## Performance and cost 114 | //! 115 | //! The conversion between `FfiFuture` and ordinary `Future` is not cost-free. Currently 116 | //! [`FfiFuture::new`] and its alias [`FutureExt::into_ffi`] does one extra allocation. 117 | //! When `poll`ing an `FfiFuture`, the `Waker` supplied does one extra allocation when `clone`d. 118 | //! 119 | //! It's recommended to only wrap you `async` code once right at the FFI boundary, and use ordinary 120 | //! `Future` everywhere else. It's usually not a good idea to use `FfiFuture` in methods, trait 121 | //! methods, or generic codes. 122 | //! 123 | //! ## [`abi-stable`] support 124 | //! 125 | //! If you want to use this crate with [`abi-stable`] interfaces. You can enable the feature flag 126 | //! `abi_stable` (disabled by default), then the struct `FfiFuture` and friends would derive 127 | //! `abi_stable::StableAbi`. 128 | //! 129 | //! [`abi-stable`]: https://github.com/rodrimati1992/abi_stable_crates/ 130 | #![deny(missing_docs)] 131 | #![cfg_attr(docsrs, feature(doc_cfg))] 132 | use std::{ 133 | convert::{TryFrom, TryInto}, 134 | fmt, 135 | future::Future, 136 | marker::PhantomData, 137 | mem::{self, ManuallyDrop}, 138 | pin::Pin, 139 | task::{Context, Poll, RawWaker, RawWakerVTable, Waker}, 140 | }; 141 | 142 | #[cfg(feature = "macros")] 143 | #[cfg_attr(docsrs, doc(cfg(feature = "macros")))] 144 | pub use macros::async_ffi; 145 | 146 | /// The ABI version of [`FfiFuture`] and all variants. 147 | /// Every non-compatible ABI change will increase this number, as well as the crate major version. 148 | pub const ABI_VERSION: u32 = 2; 149 | 150 | /// The FFI compatible [`std::task::Poll`] 151 | /// 152 | /// [`std::task::Poll`]: std::task::Poll 153 | #[repr(C, u8)] 154 | #[cfg_attr(feature = "abi_stable", derive(abi_stable::StableAbi))] 155 | pub enum FfiPoll { 156 | /// Represents that a value is immediately ready. 157 | Ready(T), 158 | /// Represents that a value is not ready yet. 159 | Pending, 160 | /// Represents that the future panicked 161 | Panicked, 162 | } 163 | 164 | /// Abort on drop with a message. 165 | struct DropBomb(&'static str); 166 | 167 | impl DropBomb { 168 | fn with T>(message: &'static str, f: F) -> T { 169 | let bomb = DropBomb(message); 170 | let ret = f(); 171 | mem::forget(bomb); 172 | ret 173 | } 174 | } 175 | 176 | impl Drop for DropBomb { 177 | fn drop(&mut self) { 178 | use std::io::Write; 179 | // Use `Stderr::write_all` instead of `eprintln!` to avoid panicking here. 180 | let mut stderr = std::io::stderr(); 181 | let _ = stderr.write_all(b"async-ffi: abort due to panic across the FFI boundary in "); 182 | let _ = stderr.write_all(self.0.as_bytes()); 183 | let _ = stderr.write_all(b"\n"); 184 | std::process::abort(); 185 | } 186 | } 187 | 188 | /// The FFI compatible [`std::task::Context`] 189 | /// 190 | /// [`std::task::Context`]: std::task::Context 191 | #[repr(C)] 192 | #[cfg_attr(feature = "abi_stable", derive(abi_stable::StableAbi))] 193 | pub struct FfiContext<'a> { 194 | /// This waker is passed as borrow semantic. 195 | /// The external fn must not `drop` or `wake` it. 196 | waker: *const FfiWakerBase, 197 | /// Lets the compiler know that this references the `FfiWaker` and should not outlive it 198 | _marker: PhantomData<&'a FfiWakerBase>, 199 | } 200 | 201 | impl<'a> FfiContext<'a> { 202 | /// SAFETY: Vtable functions of `waker` are unsafe, the caller must ensure they have a 203 | /// sane behavior as a Waker. `with_context` relies on this to be safe. 204 | unsafe fn new(waker: &'a FfiWaker) -> Self { 205 | Self { 206 | waker: (waker as *const FfiWaker).cast::(), 207 | _marker: PhantomData, 208 | } 209 | } 210 | 211 | /// Runs a closure with the [`FfiContext`] as a normal [`std::task::Context`]. 212 | /// 213 | /// [`std::task::Context`]: std::task::Context 214 | pub fn with_context T>(&mut self, closure: F) -> T { 215 | // C vtable functions are considered from FFI and they are not expected to unwind, so we don't 216 | // need to wrap them with `DropBomb`. 217 | static RUST_WAKER_VTABLE: RawWakerVTable = { 218 | unsafe fn clone(data: *const ()) -> RawWaker { 219 | let waker = data.cast::(); 220 | let cloned = ((*(*waker).vtable).clone)(waker); 221 | RawWaker::new(cloned.cast(), &RUST_WAKER_VTABLE) 222 | } 223 | unsafe fn wake(data: *const ()) { 224 | let waker = data.cast::(); 225 | ((*(*waker).vtable).wake)(waker); 226 | } 227 | unsafe fn wake_by_ref(data: *const ()) { 228 | let waker = data.cast::(); 229 | ((*(*waker).vtable).wake_by_ref)(waker); 230 | } 231 | unsafe fn drop(data: *const ()) { 232 | let waker = data.cast::(); 233 | ((*(*waker).vtable).drop)(waker); 234 | } 235 | RawWakerVTable::new(clone, wake, wake_by_ref, drop) 236 | }; 237 | 238 | // SAFETY: `waker`'s vtable functions must have sane behaviors, this is the contract of 239 | // `FfiContext::new`. 240 | let waker = unsafe { 241 | // The waker reference is borrowed from external context. We must not call drop on it. 242 | ManuallyDrop::new(Waker::from_raw(RawWaker::new( 243 | self.waker.cast(), 244 | &RUST_WAKER_VTABLE, 245 | ))) 246 | }; 247 | let mut ctx = Context::from_waker(&waker); 248 | 249 | closure(&mut ctx) 250 | } 251 | } 252 | 253 | /// Helper trait to provide convenience methods for converting a [`std::task::Context`] to [`FfiContext`] 254 | /// 255 | /// [`std::task::Context`]: std::task::Context 256 | pub trait ContextExt { 257 | /// Runs a closure with the [`std::task::Context`] as a [`FfiContext`]. 258 | /// 259 | /// [`std::task::Context`]: std::task::Context 260 | fn with_ffi_context T>(&mut self, closure: F) -> T; 261 | } 262 | 263 | impl ContextExt for Context<'_> { 264 | fn with_ffi_context T>(&mut self, closure: F) -> T { 265 | static C_WAKER_VTABLE_OWNED: FfiWakerVTable = { 266 | unsafe extern "C" fn clone(data: *const FfiWakerBase) -> *const FfiWakerBase { 267 | DropBomb::with("Waker::clone", || { 268 | let data = data as *mut FfiWaker; 269 | let waker: Waker = (*(*data).waker.owned).clone(); 270 | Box::into_raw(Box::new(FfiWaker { 271 | base: FfiWakerBase { 272 | vtable: &C_WAKER_VTABLE_OWNED, 273 | }, 274 | waker: WakerUnion { 275 | owned: ManuallyDrop::new(waker), 276 | }, 277 | })) 278 | .cast() 279 | }) 280 | } 281 | // In this case, we must own `data`. This can only happen when the `data` pointer is returned from `clone`. 282 | // Thus the it is `Box`. 283 | unsafe extern "C" fn wake(data: *const FfiWakerBase) { 284 | DropBomb::with("Waker::wake", || { 285 | let b = Box::from_raw(data as *mut FfiWaker); 286 | ManuallyDrop::into_inner(b.waker.owned).wake(); 287 | }); 288 | } 289 | unsafe extern "C" fn wake_by_ref(data: *const FfiWakerBase) { 290 | DropBomb::with("Waker::wake_by_ref", || { 291 | let data = data as *mut FfiWaker; 292 | (*data).waker.owned.wake_by_ref(); 293 | }); 294 | } 295 | // Same as `wake`. 296 | unsafe extern "C" fn drop(data: *const FfiWakerBase) { 297 | DropBomb::with("Waker::drop", || { 298 | let mut b = Box::from_raw(data as *mut FfiWaker); 299 | ManuallyDrop::drop(&mut b.waker.owned); 300 | mem::drop(b); 301 | }); 302 | } 303 | FfiWakerVTable { 304 | clone, 305 | wake, 306 | wake_by_ref, 307 | drop, 308 | } 309 | }; 310 | 311 | static C_WAKER_VTABLE_REF: FfiWakerVTable = { 312 | unsafe extern "C" fn clone(data: *const FfiWakerBase) -> *const FfiWakerBase { 313 | DropBomb::with("Waker::clone", || { 314 | let data = data as *mut FfiWaker; 315 | let waker: Waker = (*(*data).waker.reference).clone(); 316 | Box::into_raw(Box::new(FfiWaker { 317 | base: FfiWakerBase { 318 | vtable: &C_WAKER_VTABLE_OWNED, 319 | }, 320 | waker: WakerUnion { 321 | owned: ManuallyDrop::new(waker), 322 | }, 323 | })) 324 | .cast() 325 | }) 326 | } 327 | unsafe extern "C" fn wake_by_ref(data: *const FfiWakerBase) { 328 | DropBomb::with("Waker::wake_by_ref", || { 329 | let data = data as *mut FfiWaker; 330 | (*(*data).waker.reference).wake_by_ref(); 331 | }); 332 | } 333 | unsafe extern "C" fn unreachable(_: *const FfiWakerBase) { 334 | std::process::abort(); 335 | } 336 | FfiWakerVTable { 337 | clone, 338 | wake: unreachable, 339 | wake_by_ref, 340 | drop: unreachable, 341 | } 342 | }; 343 | 344 | let waker = FfiWaker { 345 | base: FfiWakerBase { 346 | vtable: &C_WAKER_VTABLE_REF, 347 | }, 348 | waker: WakerUnion { 349 | reference: self.waker(), 350 | }, 351 | }; 352 | 353 | // SAFETY: The behavior of `waker` is sane since we forward them to another valid Waker. 354 | // That waker must be safe to use due to the contract of `RawWaker::new`. 355 | let mut ctx = unsafe { FfiContext::new(&waker) }; 356 | 357 | closure(&mut ctx) 358 | } 359 | } 360 | 361 | // Inspired by Gary Guo (github.com/nbdd0121) 362 | // 363 | // The base is what can be accessed through FFI, and the regular struct contains 364 | // internal data (the original waker). 365 | #[repr(C)] 366 | #[cfg_attr(feature = "abi_stable", derive(abi_stable::StableAbi))] 367 | struct FfiWakerBase { 368 | vtable: *const FfiWakerVTable, 369 | } 370 | #[repr(C)] 371 | struct FfiWaker { 372 | base: FfiWakerBase, 373 | waker: WakerUnion, 374 | } 375 | 376 | #[repr(C)] 377 | union WakerUnion { 378 | reference: *const Waker, 379 | owned: ManuallyDrop, 380 | unknown: (), 381 | } 382 | 383 | #[derive(PartialEq, Eq, Hash, Clone, Copy)] 384 | #[repr(C)] 385 | #[cfg_attr(feature = "abi_stable", derive(abi_stable::StableAbi))] 386 | struct FfiWakerVTable { 387 | clone: unsafe extern "C" fn(*const FfiWakerBase) -> *const FfiWakerBase, 388 | wake: unsafe extern "C" fn(*const FfiWakerBase), 389 | wake_by_ref: unsafe extern "C" fn(*const FfiWakerBase), 390 | drop: unsafe extern "C" fn(*const FfiWakerBase), 391 | } 392 | 393 | /// The FFI compatible future type with [`Send`] bound. 394 | /// 395 | /// See [module level documentation](`crate`) for more details. 396 | #[repr(transparent)] 397 | #[cfg_attr(feature = "abi_stable", derive(abi_stable::StableAbi))] 398 | pub struct BorrowingFfiFuture<'a, T>(LocalBorrowingFfiFuture<'a, T>); 399 | 400 | /// The FFI compatible future type with [`Send`] bound and `'static` lifetime, 401 | /// which is needed for most use cases. 402 | /// 403 | /// See [module level documentation](`crate`) for more details. 404 | pub type FfiFuture = BorrowingFfiFuture<'static, T>; 405 | 406 | /// Helper trait to provide conversion from `Future` to [`FfiFuture`] or [`LocalFfiFuture`]. 407 | /// 408 | /// See [module level documentation](`crate`) for more details. 409 | pub trait FutureExt: Future + Sized { 410 | /// Convert a Rust `Future` implementing [`Send`] into a FFI-compatible [`FfiFuture`]. 411 | fn into_ffi<'a>(self) -> BorrowingFfiFuture<'a, Self::Output> 412 | where 413 | Self: Send + 'a, 414 | { 415 | BorrowingFfiFuture::new(self) 416 | } 417 | 418 | /// Convert a Rust `Future` into a FFI-compatible [`LocalFfiFuture`]. 419 | fn into_local_ffi<'a>(self) -> LocalBorrowingFfiFuture<'a, Self::Output> 420 | where 421 | Self: 'a, 422 | { 423 | LocalBorrowingFfiFuture::new(self) 424 | } 425 | } 426 | 427 | impl FutureExt for F where F: Future + Sized {} 428 | 429 | /// Represents that the poll function panicked. 430 | #[derive(Debug)] 431 | pub struct PollPanicked { 432 | _private: (), 433 | } 434 | 435 | impl fmt::Display for PollPanicked { 436 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 437 | f.write_str("FFI poll function panicked") 438 | } 439 | } 440 | 441 | impl std::error::Error for PollPanicked {} 442 | 443 | impl FfiPoll { 444 | /// Converts a [`std::task::Poll`] to the [`FfiPoll`]. 445 | /// 446 | /// [`std::task::Poll`]: std::task::Poll 447 | pub fn from_poll(poll: Poll) -> Self { 448 | match poll { 449 | Poll::Ready(r) => Self::Ready(r), 450 | Poll::Pending => Self::Pending, 451 | } 452 | } 453 | 454 | /// Try to convert a [`FfiPoll`] back to the [`std::task::Poll`]. 455 | /// 456 | /// # Errors 457 | /// Returns `Err(PollPanicked)` if the result indicates the poll function panicked. 458 | /// 459 | /// [`std::task::Poll`]: std::task::Poll 460 | pub fn try_into_poll(self) -> Result, PollPanicked> { 461 | match self { 462 | Self::Ready(r) => Ok(Poll::Ready(r)), 463 | Self::Pending => Ok(Poll::Pending), 464 | Self::Panicked => Err(PollPanicked { _private: () }), 465 | } 466 | } 467 | } 468 | 469 | impl From> for FfiPoll { 470 | fn from(poll: Poll) -> Self { 471 | Self::from_poll(poll) 472 | } 473 | } 474 | 475 | impl TryFrom> for Poll { 476 | type Error = PollPanicked; 477 | 478 | fn try_from(ffi_poll: FfiPoll) -> Result { 479 | ffi_poll.try_into_poll() 480 | } 481 | } 482 | 483 | impl<'a, T> BorrowingFfiFuture<'a, T> { 484 | /// Convert an [`std::future::Future`] implementing [`Send`] into a FFI-compatible [`FfiFuture`]. 485 | /// 486 | /// Usually [`FutureExt::into_ffi`] is preferred and is identical to this method. 487 | pub fn new + Send + 'a>(fut: F) -> Self { 488 | Self(LocalBorrowingFfiFuture::new(fut)) 489 | } 490 | } 491 | 492 | // SAFETY: This is safe since we allow only `Send` Future in `FfiFuture::new`. 493 | unsafe impl Send for BorrowingFfiFuture<'_, T> {} 494 | 495 | impl Future for BorrowingFfiFuture<'_, T> { 496 | type Output = T; 497 | 498 | fn poll(mut self: Pin<&mut Self>, ctx: &mut Context<'_>) -> Poll { 499 | Pin::new(&mut self.0).poll(ctx) 500 | } 501 | } 502 | 503 | /// The FFI compatible future type without [`Send`] bound. 504 | /// 505 | /// Non-[`Send`] `Future`s can only be converted into [`LocalFfiFuture`]. It is not able to be 506 | /// `spawn`ed in a multi-threaded runtime, but is useful for thread-local futures, single-threaded 507 | /// runtimes, or single-threaded targets like `wasm32-unknown-unknown`. 508 | /// 509 | /// See [module level documentation](`crate`) for more details. 510 | #[repr(C)] 511 | #[cfg_attr(feature = "abi_stable", derive(abi_stable::StableAbi))] 512 | pub struct LocalBorrowingFfiFuture<'a, T> { 513 | fut_ptr: *mut (), 514 | poll_fn: unsafe extern "C" fn(fut_ptr: *mut (), context_ptr: *mut FfiContext) -> FfiPoll, 515 | drop_fn: unsafe extern "C" fn(*mut ()), 516 | _marker: PhantomData<&'a ()>, 517 | } 518 | 519 | /// The FFI compatible future type without `Send` bound but with `'static` lifetime. 520 | /// 521 | /// See [module level documentation](`crate`) for more details. 522 | pub type LocalFfiFuture = LocalBorrowingFfiFuture<'static, T>; 523 | 524 | impl<'a, T> LocalBorrowingFfiFuture<'a, T> { 525 | /// Convert an [`std::future::Future`] into a FFI-compatible [`LocalFfiFuture`]. 526 | /// 527 | /// Usually [`FutureExt::into_local_ffi`] is preferred and is identical to this method. 528 | pub fn new + 'a>(fut: F) -> Self { 529 | unsafe extern "C" fn poll_fn( 530 | fut_ptr: *mut (), 531 | context_ptr: *mut FfiContext, 532 | ) -> FfiPoll { 533 | // The poll fn is likely to panic since it contains most of user logic. 534 | let ret = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { 535 | let fut_pin = Pin::new_unchecked(&mut *fut_ptr.cast::()); 536 | (*context_ptr).with_context(|ctx| F::poll(fut_pin, ctx)) 537 | })); 538 | match ret { 539 | Ok(p) => p.into(), 540 | Err(payload) => { 541 | // Panic payload may panic when dropped, ensure not propagate it. 542 | // https://github.com/rust-lang/rust/issues/86027 543 | DropBomb::with("drop of panic payload from Future::poll", move || { 544 | drop(payload); 545 | }); 546 | FfiPoll::Panicked 547 | } 548 | } 549 | } 550 | 551 | unsafe extern "C" fn drop_fn(ptr: *mut ()) { 552 | DropBomb::with("Future::drop", || { 553 | drop(Box::from_raw(ptr.cast::())); 554 | }); 555 | } 556 | 557 | let ptr = Box::into_raw(Box::new(fut)); 558 | Self { 559 | fut_ptr: ptr.cast(), 560 | poll_fn: poll_fn::, 561 | drop_fn: drop_fn::, 562 | _marker: PhantomData, 563 | } 564 | } 565 | } 566 | 567 | impl Drop for LocalBorrowingFfiFuture<'_, T> { 568 | fn drop(&mut self) { 569 | // SAFETY: This is safe since `drop_fn` is construct from `LocalBorrowingFfiFuture::new` 570 | // and is a dropper 571 | // `LocalBorrowingFfiFuture::new` and they are just a Box pointer and its corresponding 572 | // dropper. 573 | unsafe { (self.drop_fn)(self.fut_ptr) }; 574 | } 575 | } 576 | 577 | impl Future for LocalBorrowingFfiFuture<'_, T> { 578 | type Output = T; 579 | 580 | fn poll(self: Pin<&mut Self>, ctx: &mut Context<'_>) -> Poll { 581 | // SAFETY: This is safe since `poll_fn` is constructed from `LocalBorrowingFfiFuture::new` 582 | // and it just forwards to the original safe `Future::poll`. 583 | ctx.with_ffi_context(|ctx| unsafe { (self.poll_fn)(self.fut_ptr, ctx) }) 584 | .try_into() 585 | // Propagate panic from FFI. 586 | .unwrap_or_else(|_| panic!("FFI future panicked")) 587 | } 588 | } 589 | -------------------------------------------------------------------------------- /tests/basic.rs: -------------------------------------------------------------------------------- 1 | #![allow(clippy::unused_async)] 2 | use async_ffi::FutureExt as _; 3 | use std::{ 4 | future::Future, 5 | pin::Pin, 6 | ptr::addr_of_mut, 7 | rc::Rc, 8 | sync::Arc, 9 | task::{Context, Poll, RawWaker, RawWakerVTable, Waker}, 10 | time::Duration, 11 | }; 12 | use tokio::task; 13 | 14 | #[tokio::test] 15 | async fn call_test() { 16 | async fn foo(x: u32) -> u32 { 17 | x + 42 18 | } 19 | 20 | let ret = foo(1).into_ffi().await; 21 | assert_eq!(ret, 43); 22 | } 23 | 24 | #[tokio::test] 25 | async fn complicate_test() { 26 | let (tx, mut rx) = tokio::sync::mpsc::channel(2); 27 | 28 | tokio::spawn( 29 | async move { 30 | tokio::time::sleep(Duration::from_millis(1)) 31 | .into_ffi() 32 | .await; 33 | for i in 0..8 { 34 | tx.send(i).await.unwrap(); 35 | } 36 | } 37 | .into_ffi(), 38 | ); 39 | 40 | let mut v = Vec::new(); 41 | while let Some(i) = rx.recv().await { 42 | v.push(i); 43 | } 44 | assert_eq!(v, [0, 1, 2, 3, 4, 5, 6, 7]); 45 | } 46 | 47 | #[test] 48 | fn future_drop_test() { 49 | struct Dropper { 50 | _ref: Arc<()>, 51 | } 52 | 53 | let rc = Arc::new(()); 54 | 55 | let d = Dropper { _ref: rc.clone() }; 56 | let fut = async move { drop(d) }.into_ffi(); 57 | assert_eq!(Arc::strong_count(&rc), 2); 58 | drop(fut); 59 | assert_eq!(Arc::strong_count(&rc), 1); 60 | } 61 | 62 | #[test] 63 | fn waker_test() { 64 | static VTABLE: RawWakerVTable = { 65 | unsafe fn log(data: *const (), s: &str) { 66 | (*(data as *mut ()).cast::>()).push(s.to_owned()); 67 | } 68 | unsafe fn clone(data: *const ()) -> RawWaker { 69 | log(data, "clone"); 70 | RawWaker::new(data, &VTABLE) 71 | } 72 | unsafe fn wake(data: *const ()) { 73 | log(data, "wake"); 74 | } 75 | unsafe fn wake_by_ref(data: *const ()) { 76 | log(data, "wake_by_ref"); 77 | } 78 | unsafe fn drop(data: *const ()) { 79 | log(data, "drop"); 80 | } 81 | RawWakerVTable::new(clone, wake, wake_by_ref, drop) 82 | }; 83 | 84 | struct Fut(usize, Option); 85 | impl Future for Fut { 86 | type Output = i32; 87 | fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { 88 | let w = cx.waker(); 89 | match self.0 { 90 | 0 => { 91 | w.wake_by_ref(); 92 | self.0 = 1; 93 | Poll::Pending 94 | } 95 | 1 => { 96 | let w2 = w.clone(); 97 | w2.wake_by_ref(); 98 | self.1 = Some(w2.clone()); 99 | drop(w2); 100 | self.0 = 2; 101 | Poll::Pending 102 | } 103 | 2 => { 104 | self.1.take().unwrap().wake(); 105 | self.0 = 3; 106 | Poll::Ready(42) 107 | } 108 | _ => unreachable!(), 109 | } 110 | } 111 | } 112 | 113 | let mut v: Vec = Vec::new(); 114 | let v = addr_of_mut!(v); 115 | let waker = unsafe { Waker::from_raw(RawWaker::new(v as *const (), &VTABLE)) }; 116 | let mut ctx = Context::from_waker(&waker); 117 | 118 | let look = || std::mem::take(unsafe { &mut *v }); 119 | 120 | let mut c_fut = Fut(0, None).into_ffi(); 121 | assert_eq!(Pin::new(&mut c_fut).poll(&mut ctx), Poll::Pending); 122 | assert_eq!(look(), &["wake_by_ref"]); 123 | assert_eq!(Pin::new(&mut c_fut).poll(&mut ctx), Poll::Pending); 124 | assert_eq!(look(), &["clone", "wake_by_ref", "clone", "drop"]); 125 | assert_eq!(Pin::new(&mut c_fut).poll(&mut ctx), Poll::Ready(42)); 126 | assert_eq!(look(), &["wake"]); 127 | } 128 | 129 | #[tokio::test] 130 | async fn non_send_future_test() { 131 | async fn foo(x: u32) -> u32 { 132 | let a = Rc::new(x); 133 | let _ = task::yield_now().await; 134 | *a + 42 135 | } 136 | 137 | let fut = foo(1).into_local_ffi(); 138 | 139 | let local = task::LocalSet::new(); 140 | let ret = local 141 | .run_until(async move { task::spawn_local(fut).await.unwrap() }) 142 | .await; 143 | 144 | assert_eq!(ret, 43); 145 | } 146 | 147 | #[tokio::test] 148 | async fn panic_inside_test() { 149 | let fut = async { 150 | let _ = std::panic::catch_unwind(|| { 151 | panic!("already caught inside"); 152 | }); 153 | 42 154 | } 155 | .into_ffi(); 156 | assert_eq!(fut.await, 42); 157 | } 158 | 159 | #[tokio::test] 160 | #[should_panic = "FFI future panicked"] 161 | async fn panic_propagate_test() { 162 | async { 163 | panic!("not caught inside"); 164 | } 165 | .into_ffi() 166 | .await; 167 | } 168 | 169 | #[cfg(feature = "macros")] 170 | #[tokio::test] 171 | async fn macros() { 172 | #[async_ffi::async_ffi] 173 | async fn foo(x: u32) -> u32 { 174 | x + 42 175 | } 176 | 177 | const _: fn(u32) -> async_ffi::FfiFuture = foo; 178 | 179 | let ret = foo(1).await; 180 | assert_eq!(ret, 43); 181 | } 182 | --------------------------------------------------------------------------------