I am fully aware that what I am asking here is a grave sin. However:
TL;DR: Is there any supported way to call an async function from a non-async one, blocking the calling thread (yea, verily!) until the call completes?
Yes, I am fully versed in Swift Concurrency and I know the downsides of this: janking up the main thread etc. etc. But hear me out.
I am working on a major upgrade of a library that was designed long before Swift had async/await, so all its methods are synchronous. In my rewrite, database access is asynchronous, so that multiple tasks can share a database-connection pool.
The issue is converting existing client code that calls this API. We have numerous developers [paid customers!] using this API, and it would be nice if they weren't forced to do a massive function-recoloring on their codebases to deal with the asynchrony we just added. I am considering a compatibility shim, basically some optional extensions that would add non-async wrappers around the methods (ideally with signatures conforming to the old API.)
The question is how to implement such adapters. The best idea I can come up with is to spawn a Task that calls the async function, then sets a condition variable. Meanwhile the calling task blocks on the condition variable. I assume this would involve importing pthreads and figuring out how to call it from Swift. Yuck. Is there perhaps some existing facility for this that I could import?
DispatchGroup's wait() or if you are on the main thread then your could also use RunLoop.main.run(until:) to avoid locking up the UI and not concerned with reentrancy issues.
Itβs whatever thread the client is calling me on; quite likely the main thread, yes.
But itβs no shadier than what the current [c.2018] implementation does, which is to call into Obj-C code that uses @synchronized to block until the db handle is available.
Task.immediate is not blocking for async work, it simply starts any sync work before the first suspension point immediately. If that's all you need, then sure, but I don't think that's what's being asked here.
There is no safe, supported way to do this. I'd suggest an intermediate release of the library that keeps the sync API and adds the async API so consumers can start the migration between the two. @Cyberbeni's suggestions are probably the best, but anything that touches a task runs the risk of deadlocks if called from another concurrency thread when the concurrency pool is saturated.
This is a rewrite β one of the main goals is to replace the old Obj-C code that implements this stuff β so that'd mean implementing the thread-blocking behavior in pure Swift using DispatchSemaphore or whatever, which seems pretty bad architecturally. Any number of concurrent tasks could end up blocked waiting for resources (db handles) to become available.
I also don't see how I could implement the core producer-consumer queue that manages the handles such that it can be accessed in both blocking and async modes at once.
I tried to flesh out your example, but things get nasty when it comes to getting the result of the closure and returning it to the caller. This is the closest I've been able to get:
package func blockingCall<T>(_ asyncFn: @escaping @Sendable () async throws -> T) throws -> T {
let sem = DispatchSemaphore(value: 0)
var result: Result<T,Error>?
Task { // ERROR: Sending value of non-Sendable type '() async -> ()' risks causing data races
do {
result = .success(try await asyncFn())
} catch {
result = .failure(error)
}
sem.signal()
}
sem.wait()
return try result!.get()
}
Note that I had to mark the closure as both escaping and Sendable, which are likely to be problematic, and it still doesn't work because Swift doesn't like result being accessed both in the Task and the main function.
IMHO it's unreasonable of Swift to completely prevent blocking on an async call this way. It is usually not a good idea to do it, but there are times when it's legitimately required for compatibility reasons.
Swift is in an awkward position here. We're well aware that a lot of API authors are in this situation of needing to upgrade from a synchronous to an asynchronous API. We also can't really provide a standard solution for it without that being immediately interpreted as an acceptable thing to use everywhere.
Waiting on a condition variable is a reasonable choice.
I don't mind waiting on a condition variable. The problem seems to be that Swift's thread-safety checks actively prevent communicating any result back from the async task to the blocked sync caller (see my previous post.)
//
// Thread.swift
// Threads
//
// Created by ibex on 18/4/2026.
import Foundation
struct Thread: ~Copyable {
private let id : String
private let arg : UnsafeMutableRawPointer
private let proc: @convention(c) (UnsafeMutableRawPointer) -> UnsafeMutableRawPointer?
private let pt : UnsafeMutablePointer <pthread_t?>
init (id: String, proc: @escaping @convention(c) (UnsafeMutableRawPointer) -> UnsafeMutableRawPointer?, arg: UnsafeMutableRawPointer) {
self.id = id
self.proc = proc
self.arg = arg
self.pt = .allocate (capacity: 1)
}
deinit {
log (#function)
pt.deallocate ()
}
}
extension Thread {
func log (_ f: String) {
print (type (of: self), "\(f): \(id)")
}
func log (_ f: String, _ s: String) {
print (type (of: self), "\(f): \(id): \(s)")
}
}
extension Thread {
func start () -> Bool {
guard pt.pointee == nil else {
log (#function, "already started")
return false
}
let r = pthread_create (pt, nil, proc, arg)
assert (pt.pointee != nil)
return r == 0
}
}
extension Thread {
func join () -> UnsafeMutableRawPointer? {
guard let t = pt.pointee else {
log (#function, "not started")
return nil
}
var p: UnsafeMutableRawPointer?
let r = pthread_join (t, &p)
if r != 0 {
return nil
}
return p
}
}
Proc.swift
//
// Proc.swift
// Threads
//
// Created by ibex on 19/4/2026.
// [Assisted by Gemini]
// This protocol "erases" the generic type T
private protocol ProcInput {
func transformed() -> UnsafeMutableRawPointer?
}
// A concrete wrapper that keeps track of the generic logic
private struct ProcInputWrapper <T: PointerConvertible>: ProcInput {
let value: T
func transformed () -> UnsafeMutableRawPointer? {
let u = value.transformed()
return u.rawPointer
}
}
// This function is truly non-generic and can be a C function pointer
private func procEntry (u: UnsafeMutableRawPointer) -> UnsafeMutableRawPointer? {
// We receive a pointer to a 'ProcInput' existential
let pointer = u.assumingMemoryBound (to: ProcInput.self)
let input = pointer.pointee
let result = input.transformed()
// Clean up the allocated task memory
pointer.deallocate()
return result
}
struct Proc <Input: PointerConvertible, Output: PointerConvertible>: ~Copyable {
private let id: String
private let thread: Thread
init (id: String, input: Input) {
self.id = id
// Wrap the generic work into a non-generic protocol existential
let pi: any ProcInput = ProcInputWrapper (value: input)
// Allocate memory for the existential itself to pass to C
let inputPtr = UnsafeMutablePointer <ProcInput>.allocate (capacity: 1)
inputPtr.initialize (to: pi)
// Thread runs `procEntry` function with `inputPtr` as argument
self.thread = Thread (id: id, proc: procEntry, arg: inputPtr)
}
}
extension Proc {
func start () -> Bool {
thread.start ()
}
}
extension Proc {
var output: Output? {
let r = thread.join ()
guard let r else { return nil }
let pr = r.assumingMemoryBound (to: Output.self)
let u = pr.pointee
pr.deallocate()
return u
}
}
PointerConvertible.swift
//
// PointerConvertible.swift
// Threads
//
// Created by ibex on 18/4/2026.
//
protocol PointerConvertible {
associatedtype TransformedValue: PointerConvertible
var pointer: UnsafeMutablePointer <Self> {get}
var rawPointer: UnsafeMutableRawPointer { get }
func convert (_ u: UnsafeMutableRawPointer) -> Self
func transformed () -> TransformedValue
}
extension PointerConvertible {
var pointer: UnsafeMutablePointer <Self> {
let u: UnsafeMutablePointer <Self> = .allocate (capacity: 1)
u.initialize (to: self)
return u
}
var rawPointer: UnsafeMutableRawPointer {
return UnsafeMutableRawPointer (self.pointer)
}
func convert (_ u: UnsafeMutableRawPointer) -> Self {
let v = u.assumingMemoryBound (to: Self.self)
return v.pointee
}
}
Usage Example
Driver.swift
//
// Driver.swift
// Threads
//
// Created by ibex on 18/4/2026.
//
import Foundation
@main
enum Driver {
static func main() {
test ()
struct Fubar {
let u: Int
}
let u = withUnsafeTemporaryAllocation (of: Fubar.self, capacity: 1) { (u: UnsafeMutableBufferPointer<Fubar>) in
u.baseAddress?.initialize (to: Fubar (u: 235711131719))
return u
}
print (u, u.baseAddress!.pointee)
}
}
private func test () {
let procv: [any ProcProxy] = [
ProcAgent <String, String> (id: 0, "Fubar"),
ProcAgent <String, String> (id: 1, "Failed unibus address register"),
ProcAgent <Int, Int> (id: 2, 16),
ProcAgent <Fubar, Ar> (id: 3, Fubar (begin: 2, end: 512))
]
print (#function, "wait for results...")
for proc in procv {
proc.start ()
if let output = proc.output {
print (#function, "id:", proc.id, "input:", proc.input, "-->", output)
}
}
print ("Hit the Return key to exit")
_ = readLine ()
}
private protocol ProcProxy {
associatedtype Input: PointerConvertible
associatedtype Output: PointerConvertible
var id: Int { get }
var input: Input { get }
var output: Output? { get }
func start ()
}
private class ProcAgent <Input: PointerConvertible, Output: PointerConvertible>: ProcProxy {
let id: Int
let input: Input
let proc: Proc <Input, Output>
init (id: Int, _ input: Input) {
self.id = id
self.input = input
self.proc = Proc (id: "\(id)", input: input)
}
var output: Output? { proc.output }
func start () {
let r = proc.start ()
assert (r)
}
}
PointerCovertibles
//
// PointerCovertibles.swift
// Threads
//
// Created by ibex on 19/4/2026.
//
extension String: PointerConvertible {
func transformed () -> String {
"\(count)" + self
}
}
extension Int: PointerConvertible {
func transformed () -> Int {
self * self
}
}
struct Fubar {
let begin: Int
let end : Int
}
struct Ar {
let code: Int
}
import Dispatch // for sleep ()
extension Fubar: PointerConvertible {
func transformed () -> Ar {
sleep (3)
return .init (code: end * end - begin * begin)
}
}
extension Ar: PointerConvertible {
func transformed () -> Self {
self
}
}