Async¶
Rust async fn → Kotlin suspend fun¶
Exported async functions, methods and constructors become suspend functions:
#[uniffi::export]
pub async fn say_after(ms: u16, who: String) -> String {
sleep(Duration::from_millis(ms.into())).await;
format!("Hello, {who}!")
}
suspend fun sayAfter(ms: UShort, who: String): String
runBlocking {
sayAfter(200u, "Alice") // "Hello, Alice!"
}
Errors work exactly as for synchronous functions: a Result turns into a thrown exception.
- No executor needed on the Rust side. The Kotlin side drives the future: it polls, and Rust
wakes it up when there is progress. Plain
asyncRust code works as is. - Tokio. Code that needs a Tokio runtime (for example
tokio::time::sleep) must say so with#[uniffi::export(async_runtime = "tokio")], and the crate must enable UniFFI'stokiofeature. - Threading. The poll loop runs on
Dispatchers.IO, so calling asuspendbinding from the main thread is safe. - Cancellation. Cancelling the coroutine cancels the Rust future, which is then dropped. Any
Rust code after the current
.awaitdoesn't run.
Async callbacks¶
Methods of callback and trait interfaces can be async too. The Kotlin
implementation is a suspend fun, and Rust .awaits it:
#[uniffi::export(with_foreign)]
#[async_trait::async_trait]
pub trait AsyncParser: Send + Sync {
async fn parse(&self, input: String) -> Result<i32, ParserError>;
}
class SlowParser : AsyncParser {
override suspend fun parse(input: String): Int {
delay(100)
return input.toInt()
}
}
The Kotlin coroutine is started in GlobalScope, because its real parent is a Rust future that
Kotlin's structured concurrency doesn't know about. If Rust drops the future before the coroutine
finishes, the coroutine is cancelled.
Restrictions¶
- An
asyncprimary constructor (new) produces no Kotlin constructor. Name it differently so it becomes asuspend funon the companion object. - Borrowed byte slices (
&[u8]) can't be arguments ofasyncfunctions, because the borrow would end before the future does. The generator rejects them. UseVec<u8>instead.