Skip to content

Async/Future support

UniFFI supports exposing async Rust functions over the FFI. It can convert a Rust Future/async fn to and from foreign native futures (async/await in Python/Swift, suspend fun in Kotlin etc.)

Check out the examples or the more terse and thorough fixtures.

Example

This is a short "async sleep()" example:

use std::time::Duration;
use async_std::future::{timeout, pending};

/// Async function that says something after a certain time.
#[uniffi::export]
pub async fn say_after(ms: u64, who: String) -> String {
    let never = pending::<()>();
    timeout(Duration::from_millis(ms), never).await.unwrap_err();
    format!("Hello, {who}!")
}

This can be called by the following Python code:

import asyncio
from uniffi_example_futures import *

async def main():
    print(await say_after(20, 'Alice'))

if __name__ == '__main__':
    asyncio.run(main())

Async functions can also be defined in UDL:

namespace example {
    [Async]
    string say_after(u64 ms, string who);
}

This code uses asyncio to drive the future to completion, while our exposed function is used with await.

In Rust Future terminology this means the foreign bindings supply the "executor" - think event-loop, or async runtime. In this example it's asyncio. There's no requirement for a Rust event loop.

There are some great API docs on the implementation that are well worth a read.

Exporting async trait methods

UniFFI is compatible with the async-trait crate and this can be used to export trait interfaces over the FFI.

When using UDL, wrap your trait with the #[async_trait] attribute. In the UDL, annotate all async methods with [Async]:

[Trait]
interface SayAfterTrait {
    [Async]
    string say_after(u16 ms, string who);
};

When using proc-macros, make sure to put #[uniffi::export] outside the #[async_trait] attribute:

#[uniffi::export]
#[async_trait::async_trait]
pub trait SayAfterTrait: Send + Sync {
    async fn say_after(&self, ms: u16, who: String) -> String;
}

Combining Rust and foreign async code

Traits with callback interface support that export async methods can be combined with async Rust code. See the async-api-client example for an example of this.

Python: uniffi_set_event_loop()

Python bindings export a function named uniffi_set_event_loop() which handles a corner case when integrating async Rust and Python code. uniffi_set_event_loop() is needed when Python async functions run outside of the eventloop, for example:

- Rust code is executing outside of the eventloop.  Some examples:
    - Rust code spawned its own thread
    - Python scheduled the Rust code using `EventLoop.run_in_executor`
- The Rust code calls a Python async callback method, using something like `pollster` to block
  on the async call.

In this case, we need an event loop to run the Python async function, but there's no eventloop set for the thread. Use uniffi_set_event_loop() to handle this case. It should be called before the Rust code makes the async call and passed an eventloop to use.

Note that uniffi_set_event_loop cannot be glob-imported because it's not part of the library's __all__.

Cancelling async code.

We don't directly support cancellation in UniFFI even when the underlying platforms do. You should build your cancellation in a separate, library specific channel; for example, exposing a cancel() method that sets a flag that the library checks periodically.

Cancellation can then be exposed in the API and be mapped to one of the error variants, or None/empty-vec/whatever makes sense. There's no builtin way to cancel a future, nor to cause/raise a platform native async cancellation error (eg, a swift CancellationError).

See also https://github.com/mozilla/uniffi-rs/pull/1768.