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.

We've also documentation on the internals of how this works.

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 this github PR.