Skip to content

ADR: Async bridges in Rust - #7657

Open
bendk wants to merge 1 commit into
mozilla:mainfrom
bendk:bdk/push-kpsuykzvswpp
Open

bendk wants to merge 1 commit into
mozilla:mainfrom
bendk:bdk/push-kpsuykzvswpp

Conversation

@bendk

@bendk bendk commented Oct 14, 2026 •

Copy link
Copy Markdown
Contributor

Rendered version

Pull Request checklist

  • Breaking changes: This PR follows our breaking change policy
    • This PR follows the breaking change policy:
      • This PR has no breaking API changes, or
      • There are corresponding PRs for our consumer applications that resolve the breaking changes and have been approved
  • Quality: This PR builds and tests run cleanly
    • Note:
      • For changes that need extra cross-platform testing, consider adding [ci full] to the PR title.
      • If this pull request includes a breaking change, consider cutting a new release after merging.
  • Tests: This PR includes thorough tests or an explanation of why it does not
  • Changelog: This PR includes a changelog entry in CHANGELOG.md or an explanation of why it does not need one
    • Any breaking changes to Swift or Kotlin binding APIs are noted explicitly
  • Dependencies: This PR follows our dependency management guidelines
    • Any new dependencies are accompanied by a summary of the due diligence applied in selecting them.

// Rust code here
}.await
}
```

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this needs to block the ADR, but I'm happy to bikeshed on this. I agree with Mark that we should be keeping the Rust learning curve as gradual as possible.

@bendk
bendk force-pushed the bdk/push-kpsuykzvswpp branch 2 times, most recently from dea3bb6 to 8dd3182 Compare October 14, 2026 19:21
@bendk bendk mentioned this pull request Oct 14, 2026
5 tasks done

@jonalmeida jonalmeida left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This largely looks fine to me with a small hold out on the hard-coding of Dispatchers.IO in the KotlinWorkQueue. I originally understood it as a placeholder in the pseudo-code, so I want to make sure that it isn't a specific Dispatcher that the embedder cannot specify.

```kotlin
class KotlinWorkQueue : WorkQueue {
fun run(task: () -> Unit) {
withContext(Dispatchers.IO) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The layer above or even this one can have a default of Dispatchers.IO as long as we allow for it to be overridden by the embedder.

The current design would not let me supply my own CoroutineContext where needed (example).

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1 to this

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1 to that too.
I think we might have some tasks that are long running but not IO bound?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the discussion, I'd love to figure this out. I think there are 2 things here:

You might want to wrap the entire operation in a custom coroutine scope, like in the autopush example where you want to add the exception handler and the supervisor job. I don't the WorkQueue is the right place to do this, since only the blocking parts of the Rust function are going to be scheduled with the work queue. This will often be the entire operation, but not always -- see the potential future work section. Instead, I think you'd need to continue to wrap the Rust call in the custom scope which should be okay, you'd just be wrapping an async function instead of a sync function.

You also might want to schedule the blocking work with a different dispatcher like in the IO bound example. I think there's a couple ways we could handle that:

  • Each component inputs its own work queue
  • Tasks get a type tag that specifies what kind of work they're doing. IO-bound work gets scheduled with Dispatchers.IO, CPU-bound work gets scheduled with a separate dispatcher.


```kotlin
class KotlinWorkQueue : WorkQueue {
fun run(task: () -> Unit) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For this future ADR when we think about cancellation, a task could be a Continuation when we can describe what that means at the rust layer so that we can inform the dispatcher that this task is not cancellable, like the sqlite tasks.

I mention this here because it might be worth thinking about the API design for the task if it helps you in the future.

Comment thread docs/adr/0010-async-bridges-in-rust.md Outdated
Comment thread docs/adr/0010-async-bridges-in-rust.md Outdated
```kotlin
class KotlinWorkQueue : WorkQueue {
fun run(task: () -> Unit) {
withContext(Dispatchers.IO) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1 to this

The Kotlin and Swift implementations are straightforward:

```kotlin
class KotlinWorkQueue : WorkQueue {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ potential bikeshedding:

The name WorkQueue implies to me, that it is a queue of tasks, but I didn't see that in the implementation.

It seems to me more like a "runner" / or "executor" than a "work queue"

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point. I like TaskRunner. TaskExecutor could also work, but "executor" sometimes means "async runtime" which could be slightly confusing.

Comment thread docs/adr/0010-async-bridges-in-rust.md Outdated
Comment on lines +174 to +178
pub fn add(&self, entry: LoginEntry) -> ApiResult<Login> {
spawn_blocking(move || {
// code to do the work
})
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Question: Should add() become async add() now?


One of the goals of this document is to alleviate the sync/async ambiguities to Kotlin/Swift/JS developers.

As a Kotlin developer using these APIs, one of the things that make it unclear to me is that the APIs do not clearly look async, so I have to look at the API whether it's async or not.


With this work, it seems to me that we can now employ async constructs in the function definitions.

This add() function is not a suspend function in Kotlin, and so to me, it could be potentially blocking, and I wouldn't know for sure until I check the implementation.

If add became a suspend function, then I would know for sure that it is suspending, and I would only be able to call it in scenarios where I am prepared for handling asynchronous operations.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oops, I just forgot to add the async (and the .await at the end). I agree that seeing the async / suspend keyword is useful as a developers since you have a better idea what's happening inside the function.

@bendk
bendk force-pushed the bdk/push-kpsuykzvswpp branch from 8dd3182 to 8b01486 Compare October 15, 2026 14:01

@titooan titooan left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, I didn't intend to "Request changes". Trying to dismiss this request for changes here.

Comment on lines +96 to +101
func run(task: () -> ()) {
queue.async {
task()
}
}
}

@Cramsden Cramsden Oct 15, 2026 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems fine to me. I guess we might want to leverage a continuation here to make this interface use swift concurrency for callers. I take that back we would want to be extremely thoughtful about where and how we bridge this back to the Swift concurrency world since these calls are blocking. It should still be possible as long as they are guaranteed to complete. @ih-codes what do you think?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Apologies for the delay, I ran out of time this week but I will read through the ADR on Monday and get back to you with my thoughts.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants