Skip to content

Commit 62284b8

Browse files
committed
Async blocking task support
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.
1 parent 6a25f90 commit 62284b8

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

62 files changed

+1011
-1315
lines changed

Cargo.lock

Lines changed: 75 additions & 13 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,6 @@ members = [
3131
"fixtures/ext-types/lib",
3232
"fixtures/ext-types/proc-macro-lib",
3333

34-
"fixtures/foreign-executor",
3534
"fixtures/keywords/kotlin",
3635
"fixtures/keywords/rust",
3736
"fixtures/keywords/swift",

docs/manual/src/futures.md

Lines changed: 59 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,4 +46,62 @@ In Rust `Future` terminology this means the foreign bindings supply the "executo
4646

4747
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.
4848

49-
See the [foreign-executor fixture](https://github.com/mozilla/uniffi-rs/tree/main/fixtures/foreign-executor) for more implementation details.
49+
## Blocking tasks
50+
51+
Rust executors are designed around an assumption that the `Future::poll` function will return quickly.
52+
This assumption, combined with cooperative scheduling, allows for a large number of futures to be handled by a small number of threads.
53+
Foreign executors make similar assumptions and sometimes more extreme ones.
54+
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.
55+
56+
This raises the question of how async code can interact with blocking code.
57+
"blocking" here means code that preforms blocking IO, long-running computations without `await` breaks, etc.
58+
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.
59+
60+
On Rust, `BlockingTaskQueue` is a UniFFI type that can safely run blocking code.
61+
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.
62+
It inputs a closure and runs it in the `BlockingTaskQueue`.
63+
This closure can reference the outside scope (it does not need to be `'static`).
64+
For example:
65+
66+
```rust
67+
#[derive(uniffi::Object)]
68+
struct DataStore {
69+
// Used to run blocking tasks
70+
queue: uniffi::BlockingTaskQueue,
71+
// Low-level DB object with blocking methods
72+
db: Mutex<Database>,
73+
}
74+
75+
#[uniffi::export]
76+
impl DataStore {
77+
#[uniffi::constructor]
78+
fn new(queue: uniffi::BlockingTaskQueue) -> Self {
79+
Self {
80+
queue,
81+
db: Mutex::new(Database::new())
82+
}
83+
}
84+
85+
fn fetch_all_items(&self) -> Vec<DbItem> {
86+
queue.run_blocking(|| db.lock().fetch_all_items())
87+
}
88+
}
89+
```
90+
91+
On the foreign side `BlockingTaskQueue` corresponds to a language-dependent class.
92+
93+
### Kotlin
94+
Kotlin uses `CoroutineContext` for its `BlockingTaskQueue`.
95+
Any `CoroutineContext` will work, but `Dispatchers.IO` is usually a good choice.
96+
A DataStore from the example above can be created with `DataStore(Dispatchers.IO)`.
97+
98+
### Swift
99+
Swift uses `DispatchQueue` for its `BlockingTaskQueue`.
100+
The `DispatchQueue` should be concurrent for all in almost all circumstances -- the user-initiated global queue is normally a good choice.
101+
A DataStore from the example above can be created with `DataStore(queue: DispatchQueue.global(qos: .userInitiated)`.
102+
103+
### Python
104+
105+
Python uses a `futures.Executor` for its `BlockingTaskQueue`.
106+
`ThreadPoolExecutor` is typically a good choice.
107+
A DataStore from the example above can be created with `DataStore(ThreadPoolExecutor())`.

fixtures/foreign-executor/Cargo.toml

Lines changed: 0 additions & 19 deletions
This file was deleted.

fixtures/foreign-executor/build.rs

Lines changed: 0 additions & 7 deletions
This file was deleted.

fixtures/foreign-executor/src/foreign_executor.udl

Lines changed: 0 additions & 7 deletions
This file was deleted.

fixtures/foreign-executor/src/lib.rs

Lines changed: 0 additions & 71 deletions
This file was deleted.

fixtures/foreign-executor/tests/bindings/test_foreign_executor.kts

Lines changed: 0 additions & 39 deletions
This file was deleted.

fixtures/foreign-executor/tests/bindings/test_foreign_executor.py

Lines changed: 0 additions & 50 deletions
This file was deleted.

0 commit comments

Comments
 (0)