|
| 1 | +# Using Async Rust |
| 2 | + |
| 3 | +* Status: draft |
| 4 | +* Deciders: |
| 5 | +* Date: ??? |
| 6 | + |
| 7 | +## Context and Problem Statement |
| 8 | + |
| 9 | +Our Rust components are currently written as using synchronous Rust. |
| 10 | +The components are then wrapped in Kotlin to present an async interface. |
| 11 | +Swift also wraps them to present an async-style interface, although it currently uses `DispatchQueue` and completion handlers rather than `async` functions. |
| 12 | + |
| 13 | +UniFFI has been adding async capabilities in the last year and it seems possible to switch to using async Rust and not having an async wrapper. |
| 14 | + |
| 15 | +So... should we? |
| 16 | + |
| 17 | +### Scope |
| 18 | + |
| 19 | +This ADR discusses what our general policy on wrapper code should be. |
| 20 | +It does not cover how we should plan our work. |
| 21 | +If we decide to embrace async Rust, we do not need to commit to any particular timeline for actually switching to it. |
| 22 | + |
| 23 | +### Desktop and gecko-js |
| 24 | + |
| 25 | +On desktop, we can't write async wrappers because it's not possible in Javascript. |
| 26 | +Instead we use a strategy where every function is automatically wrapped as async in the C++ layer. |
| 27 | +Using a config file, it's possible to opt-out of this strategy for particular functions/methods. |
| 28 | + |
| 29 | +This seems to be working okay although it feels slightly weird. |
| 30 | +It's not completely clear that it would scale with more complex components. |
| 31 | + |
| 32 | +### Android-components |
| 33 | + |
| 34 | +In Kotlin, the async wrapper layer currently lives in `android-components`. |
| 35 | +For the purposes of this ADR, it doesn't really matter, and this ADR will not make a distinction between wrapper code in our repo and `android-components`. |
| 36 | + |
| 37 | +## How it would work |
| 38 | + |
| 39 | +### SQLite queries |
| 40 | + |
| 41 | +One of the reasons our code currently blocks is to run SQLite queries. |
| 42 | +https://github.com/mozilla/uniffi-rs/pull/1837 has a system to run blocking code inside an async function. |
| 43 | +It would basically mean replacing code like this: |
| 44 | + |
| 45 | +```kotlin |
| 46 | + override suspend fun wipeLocal() = withContext(coroutineContext) { |
| 47 | + conn.getStorage().wipeLocal() |
| 48 | + } |
| 49 | +``` |
| 50 | + |
| 51 | +with code like this: |
| 52 | +```rust |
| 53 | + |
| 54 | + async fn wipe_local() { |
| 55 | + self.queue.run_blocking(|| self.db.wipe_local()).await |
| 56 | + } |
| 57 | +``` |
| 58 | + |
| 59 | +We would need to merge this code, which is currently planed for the end of 2023. |
| 60 | + |
| 61 | +### Locks |
| 62 | + |
| 63 | +Another reason our code blocks is to wait on a `Mutex` or `RWLock`. |
| 64 | +To handle this, we could switch from using `parking_lot` to `async_lock`. |
| 65 | +This could be achieved with the current UniFFI, no additional changes are needed. |
| 66 | + |
| 67 | +### Network requests |
| 68 | + |
| 69 | +The last reason we block is for network requests. |
| 70 | +To support that we would probably need some sort of "async viaduct" that would allow consumer applications to choose either: |
| 71 | +- Use async functions from the `reqwest` library. |
| 72 | + This matches what we currently do for `firefox-ios`. |
| 73 | +- Use the foreign language's network stack via an async callback interface. |
| 74 | + This matches what we currently do for `firefox-android`. |
| 75 | + This would require implemnenting https://github.com/mozilla/uniffi-rs/issues/1729, which is currently planed for the end of 2023. |
| 76 | + |
| 77 | +## Decision Drivers |
| 78 | + |
| 79 | +## Considered Options |
| 80 | + |
| 81 | +* **(A) Embrace Async Rust** |
| 82 | +* **(B) Avoid Async Rust** |
| 83 | + |
| 84 | +## Decision Outcome |
| 85 | + |
| 86 | +## Pros and Cons of the Options |
| 87 | + |
| 88 | +### (A) Embrace Async Rust |
| 89 | + |
| 90 | +* Good, if we decide to avoid wrappers in `ADR-0008` because it allows us to remove the async wrappers. |
| 91 | +* Bad, because there's a risk that the UniFFI async code will cause issues and our current async strategy is working okay. |
| 92 | +* Good because it allows us to be more effecient with our thread usage. |
| 93 | + When an async task is waiting on a lock or network request, it can suspend itself and release the thread for other async work. |
| 94 | + Currently, we need to block a thread while we are waiting for this. |
| 95 | + However, it's not clear this would meaningfully affect our consumers since we don't run that many blocking operations. |
| 96 | + We would be saving maybe 1-2 threads at certain points. |
| 97 | +* Good, because it makes it easier to integrate with new languages that expect async. |
| 98 | + For example, WASM integration libraries usually returns `Future` objects to Rust which can only be evaluated in an async context. |
| 99 | + Note: this is a separate issue from UniFFI adding WASM support. |
| 100 | + If we switched our component code to using async Rust, it's possible that we could use `wasm-bindgen` instead. |
| 101 | +* Bad, because it makes it harder to provide bindings on new languages that don't support async, like C and C++. |
| 102 | + Maybe we could bridge the gap with some sort of callback-based async system, but it's not clear how that would work. |
| 103 | + |
| 104 | +### (B) Avoid Async Rust |
| 105 | + |
| 106 | +This is basically just the inverse of the last section. |
0 commit comments