From what I understand, you're suggesting we override the function that enqueues a job into the global task queue, rather than finding a way to drain Swift's internal queue. The result would be that the job is enqueued directly onto Android's main Looper instead (i.e. its main run loop), and the Swift-native queue would remain empty.
That sounds like a preferable solution to the alternative of having to pessimistically drain the main queue in our render loop, as we do now. I'm not sure what the best way of including Concurrency.h from a SwiftPM target is, but if I can figure that part out I would probably do this:
static int messagePipe[2];
void setup_main_callback(void)
{
int i = pipe2(messagePipe, O_NONBLOCK | O_CLOEXEC);
assert(i != -1);
ALooper_addFd(
ALooper_forThread(), // would be the main looper if called from the main thread
messagePipe[0],
LOOPER_ID_MAIN,
ALOOPER_EVENT_INPUT,
messagepipe_cb,
NULL // user_data (see cb)
);
}
// Called by the Android system on the main thread:
static int messagepipe_cb(int fd, int events, void* user_data) {
Job* job;
ExecutorRef executor = swift_task_getCurrentExecutor();
while (read(fd, job, sizeof(job)) == sizeof(job)) {
swift_job_run(job, executor);
}
return 1;
}
void post_job_to_main_queue(Job* job)
{
if (write(messagePipe[1], job, sizeof(job)) != sizeof(job)) {
LOGE("Callback buffer overrun!");
}
}
// in JNI_OnLoad (Swift)
// it would be great if we could run this by default on Android in libSwiftRuntime somewhere rather than in user code like this
@_cdecl("JNI_OnLoad")
public func JNI_OnLoad(...) {
setup_main_callback()
swift_task_enqueueMainExecutor_hook = { job, _ in
post_job_to_main_queue(job)
}
}
Apologies for the long post. The reason for all the detail is because I've never used ALooper
and I don't know much about Swift's concurrency internals, so any feedback would be greatly appreciated. The approach does seem reasonable to me nevertheless, assuming Android does what it should.
If there is a place the above could reasonably run within the Swift runtime itself then I'd be happy to contribute something along these lines to the Swift project. If we can do that there should be no need to import Concurrency.h from any SwiftPM target after all.
The above does have one outstanding issue with it though: the fact that this ignores Dispatch tasks means we can't use DispatchQueue.main.sync
, which is sometimes needed in deinit
of our @MainActor
isolated types. So that would still be missing if we can't use RunLoop
any more, but we could probably work around it another way until a more general solution can be found for deinit
of actor-isolated types (deinit
on an actor-isolated type is not isolated itself, so it can be difficult to clean up isolated ivars in this configuration without illegally extending the lifetime of self
).