I'm writing a small command line tool that does two things, and the second thing is async
, so I'd like to write this:
@main struct Main {
static func main() async {
doThing1()
await doThing2()
}
}
Unfortunately for me, doThing1()
breaks for reasons that I don't fully understand when it's called from an async main()
, but works fine when called from a non-async main()
. I don't have the source code to doThing1()
nor can I modify it in any way. [1]
So to make these two things work nicely together, I want to do essentially this:
static func main() { // not async!
doThing1() // Now this is happy
Task { @MainActor in
await doThing2()
}
// TODO: Wait until the task finishes,
// but I can't block the main actor!
}
I've tried the various unsafe and unrecommended tricks like using a semaphore or spinning a run loop here but—as expected—they cause deadlock if doThing2()
tries to queue up work on the main actor. So looking at what the compiler codegens for async main()
, I tried this instead:
static func main() { // not async!
doThing1() // Now this is happy
Task { @MainActor in
await doThing2()
exit(0)
}
_asyncMainDrainQueue()
}
@_silgen_name("swift_task_asyncMainDrainQueue")
func _asyncMainDrainQueue() -> Never
This works in my testing, but is it unsafe in any way that I'm missing? Is there a better way to do what I need here (maybe one that isn't using a low-level ABI)?
The function is
XCTestSuite.default.run()
, which I use to manually run test suites in a standard executable rather than an.xctest
bundle. Ifmain
isasync
and XCTest tries to run anasync
test method,swift::StackAllocator::dealloc()
aborts with"freed pointer was not the last allocation"
. I suspect that XCTest internally does some shenanigans to run@MainActor async
test methods and safely wait for them to complete even though it gets called from the main actor and this is interfering withasync main()
somehow. ↩︎