Skip to content

Commit

Permalink
Async blocking task support
Browse files Browse the repository at this point in the history
Replaced `ForeignExecutor` with `BlockingTaskQueue`.  BlockingTaskQueue
allows a Rust closure to be scheduled on a foreign thread where blocking
operations are okay. The closure runs inside the parent future, which is
nice because it allows the closure to reference its outside scope.

Added new tests for this in the futures fixtures.  Updated the tests to
check that handles are being released properly.

TODO: implement dropping BackgroundQueue and releasing the handle.
  • Loading branch information
bendk committed Nov 9, 2023
1 parent 6a25f90 commit 5cabada
Show file tree
Hide file tree
Showing 62 changed files with 1,011 additions and 1,315 deletions.
88 changes: 75 additions & 13 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 0 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@ members = [
"fixtures/ext-types/lib",
"fixtures/ext-types/proc-macro-lib",

"fixtures/foreign-executor",
"fixtures/keywords/kotlin",
"fixtures/keywords/rust",
"fixtures/keywords/swift",
Expand Down
60 changes: 59 additions & 1 deletion docs/manual/src/futures.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,4 +46,62 @@ In Rust `Future` terminology this means the foreign bindings supply the "executo

There are [some great API docs](https://docs.rs/uniffi_core/latest/uniffi_core/ffi/rustfuture/index.html) on the implementation that are well worth a read.

See the [foreign-executor fixture](https://github.com/mozilla/uniffi-rs/tree/main/fixtures/foreign-executor) for more implementation details.
## Blocking tasks

Rust executors are designed around an assumption that the `Future::poll` function will return quickly.
This assumption, combined with cooperative scheduling, allows for a large number of futures to be handled by a small number of threads.
Foreign executors make similar assumptions and sometimes more extreme ones.
For example, the Python eventloop is single threaded -- if any task spends a long time between `await` points, then it will block all other tasks from progressing.

This raises the question of how async code can interact with blocking code.
"blocking" here means code that preforms blocking IO, long-running computations without `await` breaks, etc.
To support this, UniFFI defines the `BlockingTaskQueue` type, which is a foreign object that schedules work on a thread where it's okay to block.

On Rust, `BlockingTaskQueue` is a UniFFI type that can safely run blocking code.
It's `run_blocking` method works like tokio's [block_in_place](https://docs.rs/tokio/latest/tokio/task/fn.block_in_place.html) function.
It inputs a closure and runs it in the `BlockingTaskQueue`.
This closure can reference the outside scope (it does not need to be `'static`).
For example:

```rust
#[derive(uniffi::Object)]
struct DataStore {
// Used to run blocking tasks
queue: uniffi::BlockingTaskQueue,
// Low-level DB object with blocking methods
db: Mutex<Database>,
}

#[uniffi::export]
impl DataStore {
#[uniffi::constructor]
fn new(queue: uniffi::BlockingTaskQueue) -> Self {
Self {
queue,
db: Mutex::new(Database::new())
}
}

fn fetch_all_items(&self) -> Vec<DbItem> {
self.queue.run_blocking(|| self.db.lock().fetch_all_items())
}
}
```

On the foreign side `BlockingTaskQueue` corresponds to a language-dependent class.

### Kotlin
Kotlin uses `CoroutineContext` for its `BlockingTaskQueue`.
Any `CoroutineContext` will work, but `Dispatchers.IO` is usually a good choice.
A DataStore from the example above can be created with `DataStore(Dispatchers.IO)`.

### Swift
Swift uses `DispatchQueue` for its `BlockingTaskQueue`.
The `DispatchQueue` should be concurrent for all in almost all circumstances -- the user-initiated global queue is normally a good choice.
A DataStore from the example above can be created with `DataStore(queue: DispatchQueue.global(qos: .userInitiated)`.

### Python

Python uses a `futures.Executor` for its `BlockingTaskQueue`.
`ThreadPoolExecutor` is typically a good choice.
A DataStore from the example above can be created with `DataStore(ThreadPoolExecutor())`.
19 changes: 0 additions & 19 deletions fixtures/foreign-executor/Cargo.toml

This file was deleted.

7 changes: 0 additions & 7 deletions fixtures/foreign-executor/build.rs

This file was deleted.

7 changes: 0 additions & 7 deletions fixtures/foreign-executor/src/foreign_executor.udl

This file was deleted.

71 changes: 0 additions & 71 deletions fixtures/foreign-executor/src/lib.rs

This file was deleted.

39 changes: 0 additions & 39 deletions fixtures/foreign-executor/tests/bindings/test_foreign_executor.kts

This file was deleted.

50 changes: 0 additions & 50 deletions fixtures/foreign-executor/tests/bindings/test_foreign_executor.py

This file was deleted.

Loading

0 comments on commit 5cabada

Please sign in to comment.