Closed Bug 1842433 Opened 2 years ago Closed 6 months ago

Support synchronous functions

Categories

(Toolkit :: UniFFI Bindings, enhancement)

enhancement

Tracking

()

RESOLVED WORKSFORME

People

(Reporter: lina, Assigned: bdk)

Details

Attachments

(1 obsolete file)

Today, the Gecko JS backend automatically makes all functions and interface methods async, and calls them in a background task. This is important for functions that do I/O, but a little clunky for ones that don't.

For example, this makes it challenging to have a builder-style API (compared to passing a dictionary or multiple arguments), or an interface with convenience methods that check or return in-memory fields.

Bug 1766045, comment 7 points out that we'll probably want most functions to be async, but haven't decided on how to configure that. This bug is a follow-up to agree on the approach, and implement it.

What does "async" even mean?

I believe we are conflating 2 different issues in UniFFI and need to tease them apart.
I think it boils down to a distinction between "async" and "threading model", but I'm not sure what that means exactly. This wall of text is an attempt to try and understand it.

First let's consider how we are using UniFFI on mobile:

  • As far as our UDL/Interface/etc is concerned, we have no async functions at all, including in code exposed to desktop.
  • As far as our consumers are concerned, (almost) every function is async, both on Desktop and on mobile.

The second exists because of hand-written Swift and Kotlin wrappers, which use an explicit threading model to ensure that most UniFFI functions happen on a background thread. But on that thread they are blocking. Our Rust code is blissfully unaware of any of this.
Because they are hand-written, decisions can be made on a per-function basis - most calls get dispatched to a background thread, but some "important" ones can call into Rust from the originating thread. Rust doesn't care either way.

Desktop has the same basic requirement - but this requirement should probably not be described as "async".
Further, we have a vague goal of removing as many hand-written wrappers as we can - if UniFFI can express what we need, it seems ideal to just let it do what it does and not force consumers to hand-write threading code/dispatch queues/whatever.
So this really means that the problem described for desktop is roughly the same problem as described for the mobile platforms - but on mobile we migitate this via hand-written wrappers.
We can't really use the same solution on Desktop, because there are no threading APIs available to the author of the hand-written code - but we want to remove the hand-written code anyway, so can we come up with something that works everywhere?

How do we annotate? And what annotations should be possible?

Somehow we want to annotate these functions - but what are we actually saying about them?
Possibilities include:

  • This must never run on the main thread.
    ** This seems very specific though - do all use-cases even have a main thread?
  • This does blocking IO.
  • This uses significant resources (be it IO or CPU)
  • Something else?

With an implication being that anything not so marked is safe to call from any thread?

Or should we be inverting this annotation - explicitly marking those functions which are safe to call from any thread, and anything not so marked is assumed to be async.

Regardless, can we assume that any such annotation applies to all bindings? In other words, is there a possibility we will want JS bindings to treat a function differently from how Kotlin treats it? Or to put it another way, we ideally would annotate these things in a way that is universal.

What does this mean for actual async functions?

In this context, an "actual" async function is a function which returns a Rust future.
While in most cases we would expect actual async functions to not block on IO, it doesn't seem like that is an assumption we should bake in. Further, an async function might not block on IO but require lots of CPU to complete - such a function is probably not really suitable for running on the main thread if possible.

Therefore, it seems like whatever mechanisms we come up with for the above should be applied to async functions too?

This implies some complexity in some implementations. For example, it should be possible that an async function marked as being OK for the main thread probably can run entirely on the main thread, using the JS event loop to drive the futures to completion - UniFFI's async capabilities already assume the foreign bindings have the executor. But an async function that's not safe to run on the main thread probably implies Rust starting a new thread with a tokio executor for that thread. If we can convince ourselves that this can be made to work, we can cross that bridge when we come to it - we have literally zero async functions at the current time. But is this really sane? And what does it mean to Kotlin and Swift?

It's actually pretty easy to make JS functions synchronous. I thought
that this code hadn't been merged yet, but it has been. I don't know if
this solves the entire issue, but I think it should work for the
interrupt case.

This commit changes the arithmetic::add() function to be sync and also
adds some comments to config.toml

Assignee: nobody → bdeankawamura
Status: NEW → ASSIGNED

What do you think about closing this one? I think the config.toml system is working well enough.

Yes, let's close this out—this has super valuable insights that we can link to in the future, but the Gecko JS backend does support generating synchronous bindings for functions, so I'm not sure there's anything immediately actionable here. Thanks!

Status: ASSIGNED → RESOLVED
Closed: 6 months ago
Resolution: --- → WORKSFORME
Attachment #9348294 - Attachment is obsolete: true
You need to log in before you can comment on or make changes to this bug.

Attachment

General

Created:
Updated:
Size: