What’s next for Foundation

I agree with this. I think the standard library should keep string functions to a minimum, but Foundation should expand on it.

alas, this is not how the NIO developers see it, at least with respect to other NIO types that have become de-factor cross-platform standards.

i don’t agree with this. the sheer size of Foundation and the amount of un-used APIs that must be brought along just to perform basic string processing tasks (like left-padding a number!) make it a non-starter for server-side use cases.

4 Likes

It's worthwhile noting as an aside that RunLoop is such a key and integral part of Foundation today and yet the actual code backing it -- to put it in the nicest possible terms -- shows signs of it having grown organically over a number of years and duplicates a lot of Dispatch functionality. As noted, RunLoop already shares a load-bearing backdoor with Dispatch to work and therefore in order to get Foundation to work on a new platform you have to get Dispatch to work on that platform, which seems to have its own problems.

I'd be quite happy were RunLoop to become a separate, nonportable library for nonportable Swift code, and so to add perhaps a contrary note to the RunLoop love: I'm quite supportive of this going away from Swift Foundation proper.

4 Likes

Sure. I think I overstated that more than I meant to. I think there is a balance. If anything is pretty esoteric it should be in Foundation. Any common string manipulation functions belong in the standard library.

EDIT: I hope moving anything that really should be part of the standard library to the standard library is part of this proposal. I'd need to look through Foundation more carefully, but there are probably some things that are in the wrong place.

2 Likes

I don't want to derail the thread, I will only briefly state that this is incorrect. There's no such requirement for target platforms. Swift still supports armv7, IIRC there are community-maintained builds for that 32-bit platform.

Additionally, Wasm (capitalized this way per the WebAssembly spec) is not comparable to other platforms in terms of bit width. It's fully stack-based, so it has no registers, but it fully supports 64-bit integers and floats right from the beginning. Thus, "Wasm64" is a misnomer, as it's not a new variant of the platform, but only an extension of the existing spec.

Wasm doesn't support I/O in the same way aarch64 or x86_64 don't support I/O. These matters are handled on a higher level by operating systems (POSIX, WASI etc), not instruction sets.

8 Likes

Yeah. I actually read up on some of the history of this after posting that. The NIO devs think NIO is too complex (requires a powerful OS behind it) to be a good portable implementation. Some targets might be impossible to implement it. I'm still of the opinion that runloops or other event handling abstractions (other than use of modern concurrency) don't belong in Foundation except maybe in an Obj-C compatibility module.

1 Like

One easy rule of thumb to apply is “does it take a Locale?”. If yes, Foundation, if no, probably stdlib. There’s cases this doesn’t cover but it’s a nice guideline.

14 Likes

perhaps this is just my lack of experience with internationalization, but is locale relevant for:

  1. left-padding a number with 0s,
  2. right-padding a number with 0s, or
  3. stringifying (not rounding!) a Double to n digits of precision?

3 yes, the other two no, unless I'm forgetting something. Note that I'm not claiming current stuff follows this rule of thumb, just that it's useful for thinking about where new things should go.

7 Likes

As long as these APIs are the only access methods for certain functionality in iOS (see also my personal use case which I have explained in What’s next for Foundation - #46 by mickeyl), Stream continues to be a great abstraction. Now with the next Foundation ditching Stream while at the same time swift-coreutils-foundation blocking contributions, I'm wary of the future.

1 Like

Aren’t there different numerical systems that could use characters other than 0? Or would that be handled by the font?

oh, yeah, maybe so. Let's not get too bogged down in this one example please :slight_smile:

1 Like

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