What’s next for Foundation

I think this is the best possible way forward with functionality of "run loop". Making it async is not scalable just because conceptually it does not fit. Appending to what I pointed as an example: dedicated actor representing "render context" accepting render requests (i.e. consuming events) and producing notifications about rendering done (i.e. generating events) is literally a replacement of render loop.

The only concerning things for me personally are:

  • prioritization of "event sources" like CF/NS run loop source allowed. conceptually this would be "place this event to the top of the queue", tho it sounds a bit odd
  • main thread is not thrown away on non-Darwing platforms. the concept of main thread is well established practice for every person making apps for Darwin platforms. keeping this concept everywhere simplifies greatly ability to switch development targets
3 Likes

I must say I didn’t understand a lot of that but I appreciate the thought you’ve put into it.

My concern remains though: I’m not looking for a way to “hijack” the default main run loop and install my own function which also blocks for the lifecycle of the program. Instead, I need a way to manually drain jobs that have been enqueued to the main queue.

To fulfil this purpose, we currently call into CFRunLoopRun(kCFRunLoopMain, 0.001) from a callback provided to Android’s Choreographer API, which is the equivalent of Apple platforms’ DisplayLink.

The flow looks like this:

  1. Swift code enqueues work onto the main queue via Task { @MainActor … } or DispatchQueue.main.async
  2. Android Choreographer calls into our custom swift render function, which renders via OpenGL and then calls CFRunLoopRun as above, which processes the jobs.

There is no place in our Swift code where we can reasonably block, because that would block Android’s existing main thread Looper. We want Swift’s main thread to be that same thread to prevent synchronisation issues. By blocking that thread, we’d hang the entire app and be terminated by the system.

Also, on Android, there is no @main function in the first place. Our native code runs (necessarily) as a shared library, again, because we have to share the process with the JVM and all the managed code running there. We do get a callback when our Swift shared library is loaded, but we certainly can’t block in it.

So what I’m asking for is hopefully actually pretty simple. To remove RunLoop we just need something like MainActor.processTasks(maxAllowedProcessingDuration: .milliseconds(10)). I don’t think the customisation point described @etcwilde’s post will solve our issue, unfortunately. CC: @Douglas_Gregor

2 Likes

There is a hook in the Concurrency runtime called swift_task_enqueueMainExecutor_hook which should allow you to forward MainActor jobs to an existing service like Android's main-thread queue. When the queue invokes you, run the job using swift_job_run, passing swift_task_getMainExecutor() as the executor.

That would probably be a reasonable default behavior on Android. It's not a configuration that has gotten a lot of contributions, but that's not because they'd be unwelcome.

8 Likes

I am, perhaps, unreasonably happy about this.

This is a pretty important if subtle point. The UI frameworks themselves make heavy use of {,NS,CF}RunLoop, and a lot of existing code would break if its timing shifted relative to work the frameworks already schedule against the main thread’s runloop.

2 Likes

Thanks, I’ll have to look into this further to understand how to use it exactly and whether it indeed solves our problem.

If I could see an example of existing code doing this (eg. in dispatch_main, or CF’s equivalent) I would feel more confident about using it going forward. I will have another look at those functions.

To be clear, this would essentially involve calling into private APIs of the Swift runtime? I’m hoping the fact that it’s called “…_hook” means its use is somewhat sanctioned and will not disappear in a future release. You mention contributions in that direction - are additions to Swift-accessible public API in scope for that?

Finally, I assume that solution would not drain jobs enqueued via DispatchQueue.main, only Swift native concurrency, is that correct?

The fact that it's an API declared in the public ABI header Concurrency.h means it's stable and intended to be used from outside the concurrency runtime.

It does not drain jobs at all. It is a hook for taking over the enqueuing of jobs onto Swift's main executor. You would replace it with a function that enqueues those jobs somewhere else. You are correct that it doesn't change anything about DispatchQueue.main, if you have code using that directly.

4 Likes

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).

2 Likes

As a general rule, Swift Concurrency should be built on high-performance platform primitives with an eye towards clean interoperation with normal platform code. I am far from an Android expert, but I’m perfectly willing to believe that that means this Looper thing there and should be the default configuration. @compnerd might be able to verify that this is the right thing to do.

There might also be a platform thread pool that we should be using instead of Dispatch. As long as it’s limited, it should be fine.

If you can find a way to rewrite your DispatchQueue.main code in terms of MainActor, you’d be more portable and might be in a good position to cut the Dispatch dependency entirely, which I imagine would be a boon for you.

6 Likes

That's about as primitive and high-performance as we can get on Android AFAIK. The alternative would be using the JNI and calling into Java, which I wouldn't be too happy about (especially as a PR on swift).

I'm just not sure about things like Task.sleep or other delayed Jobs – does Swift Concurrency deal with that transparently? Or would the implementation here need to check the desired execution time of job and delay its execution "manually"?


There is no way to do MainActor.sync AFAIK – that's the functionality we'd be missing. Other than that we've been able to remove Dispatch almost entirely (although we have had some weird cases where e.g. Task { @MainActor doesn't appear to run on the main thread, which is concerning).

1 Like

If there's some timer facilities you would prefer to use on android generally you may be able to do this by implementing a custom Clock that would be preferable then: swift/Clock.swift at main · apple/swift · GitHub

I've done so on a different C++ concurrency runtime, where we use a custom clock to schedule on an existing thing, rather than using the defaults Swift uses.

PS: This seems like a productive sub-thread but is somewhat off topic for the "what's next foundation", maybe worth splitting it out?

3 Likes

Thanks @ktoso for your reply! I’m not sure I understand the idea about using a custom clock. I don’t think I need a specific type of timing, I’d just like to ensure that Task.sleep works with the suggestion I made above (and maybe get some tips on how this works otherwise if it doesn’t). I will have to try it and see I guess.

I agree that it’d be nice to have a separate thread about this. I actually tried starting two separate threads about this recently but didn’t get any response:

Not sure if I posted those in the wrong place or something but if neither of those topics are appropriate I’d be happy to start another :slight_smile: The conversation here has certainly changed my mindset about what the correct approach would be; another difference is that Dispatch is out of the picture now too.

To add to this: unfortunately, CF/NS RunLoop was never exposing the "wake up port" to be observed like well established APIs do (libinput, wayland, gtk, qt, etc etc). Most of them are using only public API of periodically run runloop like you've explained earlier and I've never seen anyone using the _dispatch_get_main_queue_port_4CF private API as an alternative.

Because of this for my project I'm using Foundation's runloop as main loop and integrate others into it, since all of those expose the file descriptor to watch whenever events arrive on those loops. This way I don't have to use private API. Unfortunately, on linux (and probably on android too) this would not work properly either because of SR-14920. I've made a PR addressing the issue and porting CFFileDescriptor to make the usage more convenient.

With that said, I don't think what you are looking for can and will be addressed at any point in future. Android's Looper does not expose it's wake file descriptor either, just like CFRunLoop does not. Here's the thread I was asking pretty much the same thing that you need (arguably in opposite way) and reply there is basically same as here: runloop is dead

1 Like

Hi @John_McCall, thanks a lot for your ideas so far. I implemented the suggestion I posted above today and it’s all building, taking some inspiration from @kateinoigakukun’s implementation in JavaScriptKit. Basically, the integration between the Android and Swift runtimes looks like it should work.

The issue is, the swift_task_enqueueMainExecutor_hook is not being called. I set that pointer to my override, add a task to the main actor (Task.detached { @MainActor in doSomething() } and the hook is never called (nor is the task of course). Does it only work if I specifically disable Dispatch while building Swift?

1 Like

It shouldn't require that, no. You'll have to debug it; if you have more questions, let's take it to a dedicated thread in Development.

1 Like

Will conversion of decimal strings into currencies be addressed in the Internationalization API?

My understanding is that no new API is being suggested here.

But perhaps the process for including new API in the future Foundation libraries will open up, so everyone could contribute with feature proposals.

1 Like

I'm late to this party, it seems, but this is incredibly exciting. One question I have is: can we make pitches for things to be implemented in these new Foundation-successor frameworks via the standard Evolution process?

Specifically, given this particular quote from the original post:

I've got a working, performant, fully unit-tested progress implementation based on structured concurrency, which I'd love to submit for your consideration if you're interested in it:

2 Likes

Thanks @CharlesS, I will take a look. In the short term we’re really focused on the fundamental types. After we get the basics down we will certainly be discussing API like Progress (and beyond).

3 Likes

I just remember what I want for next Foundation and keep a record of it. :nerd_face:

I want Foundation to add a protocol such as StreamProtocol that could be like Ruby's IO.
I suppose its conforming types are:

  • Stream class
  • Pipe class
  • Classes related to sockets
  • FileHandle class
  • ...and so on

Backgrounds

5 Likes