What’s next for Foundation

Best alternative to CoreData is using SQLite, as the vast majority of people don't need anything more than that, and most CoreData users just treat it like a SQLite store anyway.

10 Likes

@Tony_Parker you mention that removing RunLoop is fine because structured concurrency offers a better alternative. I mostly agree, but it’s currently not 100% true today.

Without it, there is currently no good way to drain the main queue on non-Darwin platforms, breaking essential concurrency ideas like Task { @MainActor. There is a workaround via dispatch_main(), but that function never returns and therefore cannot simply only process the front of the queue and be done. The latter functionality is needed to interact with someone else’s idea of the main queue. On Android for example, we need code to run on the JVM‘s main thread, and also in Swift on the same thread. I realise this is a bit of a niche and that maybe devs of Linux apps can get away with calling dispatch_main() and be done, but there is a core functionality missing currently without the CFRunLoopRun… APIs.

To be honest we have already removed Foundation from our stack and are calling into a private API in Dispatch marked as …4CF (for CoreFoundation). But I don’t feel like this is a good or sustainable solution, and it’s not one I’d expect a dev to be able to use without months of reading source code and fully understanding the problem.

In short: please go ahead and remove RunLoop, but please also let us drain the main queue incrementally.

4 Likes

@etcwilde has been thinking about some of these issues, and in particular how to provide hooks to allow a program to take over the main runloop.

Doug

6 Likes

That’s awesome news @Douglas_Gregor @etcwilde! This will be a big help.

It’s a fine detail but it pays to be precise here: we don’t want to take over the main queue but rather share it explicitly. I think that’s what you mean but wanted to mention that the nuance is important.

Exactly. And even then there is no reason to remove it since many developers may still find the functionality necessary for their projects.

@Geordie_J and @Douglas_Gregor

Hmm, removing the runloops is going to have some interesting impacts on Swift concurrency. We're using CFRunloopRun in the underlying async-main to drain the main queues and drive the whole concurrency machine if it's linked into the process. We fall back on dispatch_main() if CFRunloopRun isn't available, but it's not really the ideal runloop.

Now to the point of swapping in different run loops, I have been taking notes for the past couple years on various requirements as the pop up.

Thus far, the MainRunLoop protocol that I've come up with looks something like;

protocol MainRunLoop: AnyObject {
  init()
  deinit()
  func shutdownSignal() -> Void
  func run() -> Void
}

(names are very much still in the air.)

init allows you to set up the run loop state if necessary.
deinit allows you to tear down any state running after the runloop has stopped (if necessary).
shutdownSignal signals to the runloop, from within a task running on said runloop, that it's time to stop. This would get called after the MainType.main() returns so that programs don't hang. Right now, we call exit, which isn't great, but that's to ensure that dispatch_main stops running.
run actually kicks the runloop off.

Depending on where my Exit Codes From Main pitch goes, we may need to adjust the shutdown signal to accept an int return code to feed through to the runtimes to return.

If we go with the example from the async main semantics proposal;

func foo() async {}
func bar() async {}

@main struct Main {
  static func main() async {
    print("Hello1")
    await foo()
    await bar()
  }
}

This gets lowered in SIL/IR, but I'll keep it looking like Swift and do a bit of hand-waving for the purpose of discussion, into something that looks like the following;

@main struct Main {
  static func _main3() {
    bar()
    exit(0) // Implicit -- kills the program because `drainQueue` doesn't return otherwise
  }
  static func _main2() {
    foo()
    enqueue(_main3) // Implicit
  }
  static func _main1() {
    print("Hello1")
    enqueue(_main2) // Implicit
  }
}

// Generated main entry point:
func _main(_ argc: Int32, _ argv: UnsafeMutablePointer<Optional<UnsafeMutablePointer<Int8>>>) -> Int32 {
  _main1()
  drainQueues() // This currently never returns.
}

With the protocol and new setup, there would be an optional main runloop variable to set that conforms to the MainRunLoop protocol. Devs would then set the run loop in the first synchronous portion of the main function. e.g.

// ...
@main struct Main {
  static func main() {
    mainRunLoop = MyCustomRunloop()
    print("Hello1")
    await foo()
    await bar()
  }
}

My current thoughts are that the new lowering would look something more like (still lots of hand-waving);

@main struct Main {
  static func _main3() {
    bar()
    mainRunLoop.shutdownSignal() // Implicit, replaces the forced exit call returning from `mainRunLoop.run()`
  }
  static func _main2() {
    foo()
    enqueue(_main3) // Implicit
  }
  static func _main1() {
    mainRunLoop = MyCustomRunloop()
    print("Hello1")
    enqueue(_main2) // Implicit
  }
}

// Generated main entry point:
func _main(_ argc: Int32, _ argv: UnsafeMutablePointer<Optional<UnsafeMutablePointer<Int8>>>) -> Int32 {
  _main1()
  if mainRunLoop {
    mainRunLoop.run()
    mainRunLoop.deinit()
  } else {
    // Default runloop implementation, which is CFRunLoopRun or dispatch_main at the moment
  }
  return 0
}

If the mainRunLoop var is set, we use that implementation. Otherwise, fall back on some default. At the moment, we use CFRunLoopRun as a default if it's linked into the process, otherwise we fall back on dispatch_main. dispatch_main presents some issues, most notably, it never returns so there's no way to really get a value back out of it. The program has to call exit with the return code, which as discussed in my exit-code pitch, has some issues.

Alright, so now that I've written what looks like an entire feature pitch as a response to a thread, why haven't I pitched it yet?

  1. I don't know where the runloop override would live. I'm bouncing between a free-floating global and something we dig out of the struct attributed with @main, like we do with the MainType.main function itself. Or does it make sense to put it somewhere else, like on the MainActor actor? In any case, we certainly want to block folks from overriding it after the first suspension point. Replacing a running runloop implementation while one is already running would be a bad day waiting to happen.

  2. Custom executors
    The runloop and executor are coupled. The executor puts jobs/tasks on the queue while the runloop takes them back off again. Who owns the queue? Who is responsible for managing the queue ordering?

If we look at the MainActor MainExecutor DispatchGlobalExecutor.inc implementation, which is sort of the original OG custom executor, it sheds some insight on where things stand today;

SWIFT_CC(swift)
static void swift_task_enqueueMainExecutorImpl(Job *job) {
  assert(job && "no job provided");

  JobPriority priority = job->getPriority();

  // This is an inline function that compiles down to a pointer to a global.
  auto mainQueue = dispatch_get_main_queue();

  dispatchEnqueue(mainQueue, job, (dispatch_qos_class_t)priority, mainQueue);
}

It uses dispatch_get_main_queue, which drags a reference to a queue out from somewhere. CFRunLoop and dispatch_main happen to know how to call dispatch_get_main_queue to get the main queue and pull tasks off to execute, and both function happen to be getting called from the right thread, but is this global function really the API we want? I don't think so. This feels more like a "happens-to-work" relationship than an "it actually works" relationship. Note the documentation comment on the dispatch_get_main_queue

In order to invoke workitems submitted to the main queue, the application
must call dispatch_main(), NSApplicationMain(), or use a CFRunLoop on the
main thread.

The corresponding UnownedSerialExecutor getter implementation for the MainActor for dispatch also does something similar;

ExecutorRef swift::swift_task_getMainExecutor() {
  return ExecutorRef::forOrdinary(
           reinterpret_cast<HeapObject*>(&_dispatch_main_q),
           _swift_task_getDispatchQueueSerialExecutorWitnessTable());
}

(note that dispatch_get_main_queue() effectively lowers to &_dispatch_main_q;

)

If the executor owns its queue, I would imagine that the runloop should call something like executor.next() (like an iterator) or executor.dequeue() to mirror the enqueue() API that exists today, and just keep pumping that to get the next task to execute. Once the main queue is empty, the program can terminate. The prioritization behavior and queue management is then left to the executor to decide.

Alternatively, if the runloop mechanism owns the actual queue, we would need to pass a reference to the queue up to the executor to ensure that executors are sending tasks to the expected queue and the runloop queue is responsible for determining the order.

Anyway, there are still plenty of conversations to be had on who owns/manages what and a fair bit more runtime spelunking that I have to do to have meaningful opinions.

This is where my thoughts are with regard to an overridable main runloop. It's still a work in progress. :slightly_smiling_face:

16 Likes

I'm very happy about the fact that Foundation is given a lot of love these days!
There are couple of points I'd like to share from the top of my head:

  • There's no replacement for RunLoop for anything that requires semi-blocking processing of data. These include render loops and event handling loops. Anything that requires to immediately block on specific event is not going to be covered by structured concurrency. If it could it would be defeating its purpose. I love structured concurrency as a thing and I love GCD even more, but watching event sources via "dispatch source" is always going to place you in the end of the callback queue and I can't imagine a single person in the world that would be happy to write something like this
    Task { 
      repeat {
        render()
        gpuBarrier()
      } while true
    }
    
  • Regarding deprecation of Locks, I think till actors support all the features of classes (like key paths, for example) there's no way they can replace usage of locks.

I've closed that pull request today after seeing this post. It's been open for 14 months and it's quite a burden to rebase-test-update it to keep it alive while not getting any feedback :/

Very sad, but I completely understand your frustration. This is exactly what I feared would happen, when the new Foundation was announced. RunLoop and assorted tools are very essential to a whole class of programs, it's pretty shortsighted if they were to really drop it.

Perhaps we can now think again about forking swift-coreutils-foundation into something that's more useful for people who actually work on cross-platform programs?

It looks like the original has been created more or less just to cross a checkmark ("yeah, we have it").

2 Likes

Swift NIO is the cross-platform run loop (or event loop). I would prefer Foundation drop it or roll it in to a platform specific subset since runloops in GUIs are very platform specific and NIO handles the other cases.

Can I attach an NSStream to a NIO runloop?

I hope that the Swift rewrite will fix the fact that while Swift itself is fully Unicode-safe, the String extension methods imported through Foundation (through NSString bridging) operate on code points instead, causing grapheme clusters to be broken unexpectedly.

There are NSStream equivalents in NIO. NSStream is not a modern API that I think should only stay around for Obj-C compatibility with older Apple APIs. That is the reason why Apple never took the NS prefix off of it. There is also swift-system that can help with cross platform file I/O. If these things are included, they should be modernized versions in their own module I think.

EDIT: I should clarify that there might be a place for a lighter weight runloop or lighter weight stream API that doesn't require platform knowledge (like swift-system), but I don't think that is required to be included in Foundation. NIO can be overkill for simple single-threaded network clients or file reading since it was really designed for servers. Since this is an area where there should be a lot of options, if it is in Foundation it should be in a separate module so it can be ignored if another I/O system makes more sense.

It might also make sense as an I/O layer for Swift compiled to WASM (that can't support complex I/O) if that is a goal for Foundation. At this point, we can't even target WASM well since Swift really requires 64-bit support in its targets from what I understand. However, it is probably worth keeping in mind for when browsers eventually support WASM-64.

EDIT: Max_Desiatov clarified some on WASM

EDIT: Tony_Parker mentioned the possibility of async/await gaining some of the features that currently require a cross-platform runloop in the future, so I think more of a reason to leave this for other packages.

I think this will be fixed by moving away from NSString as older APIs are modernized over time. I don't see how a rewrite would fix it. I don't think NS-classes are going to be part of the rewrite outside the Obj-C compatibility module which will need to keep Obj-C compatible behavior.

1 Like

Considering that the standard library's string manipulation functions are quite anemic, a larger set of string manipulation functions are needed in many (most?) scenarios involving strings. If the NSString extension methods are to be removed from Foundation, something else should come in its place.

2 Likes

I didn't expect to see so much love for RunLoop here. :grinning:

I'm aware of the differences in capabilities between what async/await is capable of now and RunLoop. There are three reasons I put it on the exclusion list:

  1. It seems like, in the future, async/await is likely to gain more functionality like RunLoop has then RunLoop becoming async/await friendly. That said, a common ER for RunLoop is some kind of integration with custom actors, especially for the main run loop.
  2. The difficulty of porting it to Swift; it has a very C-centric interface (e.g. run loop sources). Also, the follow-up part of getting the related types like NSStream, NSTimer, etc. to have more modern Swift API.
  3. Questionable value on non-Darwin platforms. To be 100% clear - RunLoop is not going anywhere on Darwin platforms. It's the fundamental concurrency primitive for all of the UI frameworks main thread interactions.

If we can address those points, I'm willing to consider putting it back on the list.

5 Likes

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