There are some good old completion based APIs in UIKit like this:
func open(
_ url: URL,
options: [UIApplication.OpenExternalURLOptionsKey : Any] = [:],
completionHandler completion: (@MainActor @Sendable (Bool) -> Void)? = nil
)
But also, with the introduction of Swift Structured Concurrency, they have added async
versions of these same methods:
func open(
_ url: URL,
options: [UIApplication.OpenExternalURLOptionsKey : Any] = [:]
) async -> Bool
There is an entire documentation about this conversion, and it says that the async/await bindings for such methods are generated automatically on a fixed defined algorithm.
But what I'm wondering about is the following - how exactly is the conversion performed?
I would imagine it uses one of 2 possible approaches:
So which one do they use, how do they do it?
It pretty much just uses a continuation, in the same way that you would bridge completion handler APIs to Swift Concurrency using withXXXContinuation { ... }
. The SIL generated isn't much different.
I wrote two files like this:
// foo.h
#import <Foundation/Foundation.h>
@interface SimpleClass : NSObject
- (void)presentWithCompletion:(void (^)(BOOL success))completion;
@end
// main.swift
let foo = SimpleClass()
print(await foo.present())
and compiled to SIL using swiftc -emit-sil -import-objc-header foo.h main.swift
.
The result is
sil_stage canonical
import Builtin
import Swift
import SwiftShims
@MainActor @_hasStorage @_hasInitialValue let foo: SimpleClass { get }
// foo
sil_global hidden [let] @$s4main3fooSo11SimpleClassCvp : $SimpleClass
// main
sil @main : $@convention(c) (Int32, UnsafeMutablePointer<Optional<UnsafeMutablePointer<Int8>>>) -> Int32 {
bb0(%0 : $Int32, %1 : $UnsafeMutablePointer<Optional<UnsafeMutablePointer<Int8>>>):
// function_ref async_Main
%2 = function_ref @async_Main : $@convention(thin) @async () -> () // user: %8
%3 = integer_literal $Builtin.Int64, 2048 // user: %4
%4 = struct $Int (%3 : $Builtin.Int64) // user: %10
%5 = metatype $@thick ().Type // user: %6
%6 = init_existential_metatype %5 : $@thick ().Type, $@thick any Any.Type // user: %10
// function_ref thunk for @escaping @convention(thin) @async () -> ()
%7 = function_ref @$sIetH_yts5Error_pIegHrzo_TR : $@convention(thin) @async (@convention(thin) @async () -> ()) -> (@out (), @error any Error) // user: %8
%8 = partial_apply [callee_guaranteed] %7(%2) : $@convention(thin) @async (@convention(thin) @async () -> ()) -> (@out (), @error any Error) // user: %9
%9 = convert_function %8 : $@async @callee_guaranteed () -> (@out (), @error any Error) to $@async @callee_guaranteed @substituted <τ_0_0> () -> (@out τ_0_0, @error any Error) for <()> // user: %10
%10 = builtin "createAsyncTask"<()>(%4 : $Int, %6 : $@thick any Any.Type, %9 : $@async @callee_guaranteed @substituted <τ_0_0> () -> (@out τ_0_0, @error any Error) for <()>) : $(Builtin.NativeObject, Builtin.RawPointer) // user: %11
%11 = tuple_extract %10 : $(Builtin.NativeObject, Builtin.RawPointer), 0 // user: %13
// function_ref swift_job_run
%12 = function_ref @swift_job_run : $@convention(thin) (UnownedJob, UnownedSerialExecutor) -> () // user: %17
%13 = builtin "convertTaskToJob"(%11 : $Builtin.NativeObject) : $Builtin.Job // user: %14
%14 = struct $UnownedJob (%13 : $Builtin.Job) // user: %17
%15 = builtin "buildMainActorExecutorRef"() : $Builtin.Executor // user: %16
%16 = struct $UnownedSerialExecutor (%15 : $Builtin.Executor) // user: %17
%17 = apply %12(%14, %16) : $@convention(thin) (UnownedJob, UnownedSerialExecutor) -> ()
// function_ref swift_task_asyncMainDrainQueue
%18 = function_ref @swift_task_asyncMainDrainQueue : $@convention(thin) () -> Never // user: %19
%19 = apply %18() : $@convention(thin) () -> Never
unreachable // id: %20
} // end sil function 'main'
// async_Main
sil private @async_Main : $@convention(thin) @async () -> () {
bb0:
%0 = builtin "buildMainActorExecutorRef"() : $Builtin.Executor // user: %1
%1 = enum $Optional<Builtin.Executor>, #Optional.some!enumelt, %0 : $Builtin.Executor // user: %28
alloc_global @$s4main3fooSo11SimpleClassCvp // id: %2
%3 = global_addr @$s4main3fooSo11SimpleClassCvp : $*SimpleClass // users: %15, %7
%4 = metatype $@thick SimpleClass.Type // user: %6
// function_ref SimpleClass.__allocating_init()
%5 = function_ref @$sSo11SimpleClassCABycfC : $@convention(method) (@thick SimpleClass.Type) -> @owned SimpleClass // user: %6
%6 = apply %5(%4) : $@convention(method) (@thick SimpleClass.Type) -> @owned SimpleClass // user: %7
store %6 to %3 : $*SimpleClass // id: %7
%8 = integer_literal $Builtin.Word, 1 // user: %10
// function_ref _allocateUninitializedArray<A>(_:)
%9 = function_ref @$ss27_allocateUninitializedArrayySayxG_BptBwlF : $@convention(thin) <τ_0_0> (Builtin.Word) -> (@owned Array<τ_0_0>, Builtin.RawPointer) // user: %10
%10 = apply %9<Any>(%8) : $@convention(thin) <τ_0_0> (Builtin.Word) -> (@owned Array<τ_0_0>, Builtin.RawPointer) // users: %12, %11
%11 = tuple_extract %10 : $(Array<Any>, Builtin.RawPointer), 0 // user: %33
%12 = tuple_extract %10 : $(Array<Any>, Builtin.RawPointer), 1 // user: %13
%13 = pointer_to_address %12 : $Builtin.RawPointer to [strict] $*Any // user: %30
%14 = alloc_stack $Bool // users: %29, %34, %17
%15 = load %3 : $*SimpleClass // users: %16, %25
%16 = objc_method %15 : $SimpleClass, #SimpleClass.present!foreign : (SimpleClass) -> () async -> Bool, $@convention(objc_method) (Optional<@convention(block) (Bool) -> ()>, SimpleClass) -> () // user: %25
%17 = get_async_continuation_addr Bool, %14 : $*Bool // users: %27, %18
%18 = struct $UnsafeContinuation<Bool, Never> (%17 : $Builtin.RawUnsafeContinuation) // user: %21
%19 = alloc_stack $@block_storage UnsafeContinuation<Bool, Never> // users: %26, %23, %20
%20 = project_block_storage %19 : $*@block_storage UnsafeContinuation<Bool, Never> // user: %21
store %18 to %20 : $*UnsafeContinuation<Bool, Never> // id: %21
// function_ref @objc completion handler block implementation for @escaping @callee_unowned @convention(block) (@unowned Bool) -> () with result type Bool
%22 = function_ref @$sSbIeyBy_SbTz_ : $@convention(c) (@inout_aliasable @block_storage UnsafeContinuation<Bool, Never>, Bool) -> () // user: %23
%23 = init_block_storage_header %19 : $*@block_storage UnsafeContinuation<Bool, Never>, invoke %22 : $@convention(c) (@inout_aliasable @block_storage UnsafeContinuation<Bool, Never>, Bool) -> (), type $@convention(block) (Bool) -> () // user: %24
%24 = enum $Optional<@convention(block) (Bool) -> ()>, #Optional.some!enumelt, %23 : $@convention(block) (Bool) -> () // user: %25
%25 = apply %16(%24, %15) : $@convention(objc_method) (Optional<@convention(block) (Bool) -> ()>, SimpleClass) -> ()
dealloc_stack %19 : $*@block_storage UnsafeContinuation<Bool, Never> // id: %26
await_async_continuation %17 : $Builtin.RawUnsafeContinuation, resume bb1 // id: %27
bb1: // Preds: bb0
hop_to_executor %1 : $Optional<Builtin.Executor> // id: %28
%29 = load %14 : $*Bool // user: %31
%30 = init_existential_addr %13 : $*Any, $Bool // user: %31
store %29 to %30 : $*Bool // id: %31
// function_ref _finalizeUninitializedArray<A>(_:)
%32 = function_ref @$ss27_finalizeUninitializedArrayySayxGABnlF : $@convention(thin) <τ_0_0> (@owned Array<τ_0_0>) -> @owned Array<τ_0_0> // user: %33
%33 = apply %32<Any>(%11) : $@convention(thin) <τ_0_0> (@owned Array<τ_0_0>) -> @owned Array<τ_0_0> // users: %43, %40
dealloc_stack %14 : $*Bool // id: %34
// function_ref default argument 1 of print(_:separator:terminator:)
%35 = function_ref @$ss5print_9separator10terminatoryypd_S2StFfA0_ : $@convention(thin) () -> @owned String // user: %36
%36 = apply %35() : $@convention(thin) () -> @owned String // users: %42, %40
// function_ref default argument 2 of print(_:separator:terminator:)
%37 = function_ref @$ss5print_9separator10terminatoryypd_S2StFfA1_ : $@convention(thin) () -> @owned String // user: %38
%38 = apply %37() : $@convention(thin) () -> @owned String // users: %41, %40
// function_ref print(_:separator:terminator:)
%39 = function_ref @$ss5print_9separator10terminatoryypd_S2StF : $@convention(thin) (@guaranteed Array<Any>, @guaranteed String, @guaranteed String) -> () // user: %40
%40 = apply %39(%33, %36, %38) : $@convention(thin) (@guaranteed Array<Any>, @guaranteed String, @guaranteed String) -> ()
release_value %38 : $String // id: %41
release_value %36 : $String // id: %42
release_value %33 : $Array<Any> // id: %43
%44 = integer_literal $Builtin.Int32, 0 // user: %45
%45 = struct $Int32 (%44 : $Builtin.Int32) // user: %47
// function_ref exit
%46 = function_ref @exit : $@convention(c) (Int32) -> Never // user: %47
%47 = apply %46(%45) : $@convention(c) (Int32) -> Never
unreachable // id: %48
} // end sil function 'async_Main'
// thunk for @escaping @convention(thin) @async () -> ()
sil shared [transparent] [reabstraction_thunk] @$sIetH_yts5Error_pIegHrzo_TR : $@convention(thin) @async (@convention(thin) @async () -> ()) -> (@out (), @error any Error) {
// %1 // user: %2
bb0(%0 : $*(), %1 : $@convention(thin) @async () -> ()):
%2 = apply %1() : $@convention(thin) @async () -> ()
%3 = tuple () // user: %4
return %3 : $() // id: %4
} // end sil function '$sIetH_yts5Error_pIegHrzo_TR'
// swift_job_run
sil [available 12.0.0] @swift_job_run : $@convention(thin) (UnownedJob, UnownedSerialExecutor) -> ()
// swift_task_asyncMainDrainQueue
sil [available 12.0.0] @swift_task_asyncMainDrainQueue : $@convention(thin) () -> Never
// SimpleClass.__allocating_init()
sil shared @$sSo11SimpleClassCABycfC : $@convention(method) (@thick SimpleClass.Type) -> @owned SimpleClass {
// %0 "$metatype" // user: %1
bb0(%0 : $@thick SimpleClass.Type):
%1 = thick_to_objc_metatype %0 : $@thick SimpleClass.Type to $@objc_metatype SimpleClass.Type // user: %2
%2 = alloc_ref_dynamic [objc] %1 : $@objc_metatype SimpleClass.Type, $SimpleClass // user: %4
// function_ref @nonobjc SimpleClass.init()
%3 = function_ref @$sSo11SimpleClassCABycfcTO : $@convention(method) (@owned SimpleClass) -> @owned SimpleClass // user: %4
%4 = apply %3(%2) : $@convention(method) (@owned SimpleClass) -> @owned SimpleClass // user: %5
return %4 : $SimpleClass // id: %5
} // end sil function '$sSo11SimpleClassCABycfC'
// _allocateUninitializedArray<A>(_:)
sil [always_inline] [_semantics "array.uninitialized_intrinsic"] @$ss27_allocateUninitializedArrayySayxG_BptBwlF : $@convention(thin) <τ_0_0> (Builtin.Word) -> (@owned Array<τ_0_0>, Builtin.RawPointer)
// @objc completion handler block implementation for @escaping @callee_unowned @convention(block) (@unowned Bool) -> () with result type Bool
sil shared [transparent] [thunk] @$sSbIeyBy_SbTz_ : $@convention(c) (@inout_aliasable @block_storage UnsafeContinuation<Bool, Never>, Bool) -> () {
// %0 // user: %2
// %1 // user: %5
bb0(%0 : $*@block_storage UnsafeContinuation<Bool, Never>, %1 : $Bool):
%2 = project_block_storage %0 : $*@block_storage UnsafeContinuation<Bool, Never> // user: %3
%3 = load %2 : $*UnsafeContinuation<Bool, Never> // user: %7
%4 = alloc_stack $Bool // users: %5, %8, %7
store %1 to %4 : $*Bool // id: %5
// function_ref _resumeUnsafeContinuation<A>(_:_:)
%6 = function_ref @$ss25_resumeUnsafeContinuationyySccyxs5NeverOG_xntlF : $@convention(thin) <τ_0_0> (UnsafeContinuation<τ_0_0, Never>, @in τ_0_0) -> () // user: %7
%7 = apply %6<Bool>(%3, %4) : $@convention(thin) <τ_0_0> (UnsafeContinuation<τ_0_0, Never>, @in τ_0_0) -> ()
dealloc_stack %4 : $*Bool // id: %8
return undef : $() // id: %9
} // end sil function '$sSbIeyBy_SbTz_'
// _resumeUnsafeContinuation<A>(_:_:)
sil shared [available 12.0.0] @$ss25_resumeUnsafeContinuationyySccyxs5NeverOG_xntlF : $@convention(thin) <T> (UnsafeContinuation<T, Never>, @in T) -> () {
// %0 // user: %3
// %1 // user: %3
bb0(%0 : $UnsafeContinuation<T, Never>, %1 : $*T):
// function_ref UnsafeContinuation.resume<>(returning:)
%2 = function_ref @$sScc6resume9returningyxn_ts5NeverORs_rlF : $@convention(method) <τ_0_0, τ_0_1 where τ_0_1 == Never> (@in τ_0_0, UnsafeContinuation<τ_0_0, Never>) -> () // user: %3
%3 = apply %2<T, Never>(%1, %0) : $@convention(method) <τ_0_0, τ_0_1 where τ_0_1 == Never> (@in τ_0_0, UnsafeContinuation<τ_0_0, Never>) -> ()
%4 = tuple () // user: %5
return %4 : $() // id: %5
} // end sil function '$ss25_resumeUnsafeContinuationyySccyxs5NeverOG_xntlF'
// _finalizeUninitializedArray<A>(_:)
sil shared [readnone] [_semantics "array.finalize_intrinsic"] @$ss27_finalizeUninitializedArrayySayxGABnlF : $@convention(thin) <Element> (@owned Array<Element>) -> @owned Array<Element> {
[%0: escape! v** => %r.v**, escape! v**.c*.v** => %r.v**.c*.v**]
// %0 // user: %2
bb0(%0 : $Array<Element>):
%1 = alloc_stack $Array<Element> // users: %6, %5, %4, %2
store %0 to %1 : $*Array<Element> // id: %2
// function_ref Array._endMutation()
%3 = function_ref @$sSa12_endMutationyyF : $@convention(method) <τ_0_0> (@inout Array<τ_0_0>) -> () // user: %4
%4 = apply %3<Element>(%1) : $@convention(method) <τ_0_0> (@inout Array<τ_0_0>) -> ()
%5 = load %1 : $*Array<Element> // user: %7
dealloc_stack %1 : $*Array<Element> // id: %6
return %5 : $Array<Element> // id: %7
} // end sil function '$ss27_finalizeUninitializedArrayySayxGABnlF'
// default argument 1 of print(_:separator:terminator:)
sil shared @$ss5print_9separator10terminatoryypd_S2StFfA0_ : $@convention(thin) () -> @owned String {
bb0:
%0 = string_literal utf8 " " // user: %5
%1 = integer_literal $Builtin.Word, 1 // user: %5
%2 = integer_literal $Builtin.Int1, -1 // user: %5
%3 = metatype $@thin String.Type // user: %5
// function_ref String.init(_builtinStringLiteral:utf8CodeUnitCount:isASCII:)
%4 = function_ref @$sSS21_builtinStringLiteral17utf8CodeUnitCount7isASCIISSBp_BwBi1_tcfC : $@convention(method) (Builtin.RawPointer, Builtin.Word, Builtin.Int1, @thin String.Type) -> @owned String // user: %5
%5 = apply %4(%0, %1, %2, %3) : $@convention(method) (Builtin.RawPointer, Builtin.Word, Builtin.Int1, @thin String.Type) -> @owned String // user: %6
return %5 : $String // id: %6
} // end sil function '$ss5print_9separator10terminatoryypd_S2StFfA0_'
// default argument 2 of print(_:separator:terminator:)
sil shared @$ss5print_9separator10terminatoryypd_S2StFfA1_ : $@convention(thin) () -> @owned String {
bb0:
%0 = string_literal utf8 "\n" // user: %5
%1 = integer_literal $Builtin.Word, 1 // user: %5
%2 = integer_literal $Builtin.Int1, -1 // user: %5
%3 = metatype $@thin String.Type // user: %5
// function_ref String.init(_builtinStringLiteral:utf8CodeUnitCount:isASCII:)
%4 = function_ref @$sSS21_builtinStringLiteral17utf8CodeUnitCount7isASCIISSBp_BwBi1_tcfC : $@convention(method) (Builtin.RawPointer, Builtin.Word, Builtin.Int1, @thin String.Type) -> @owned String // user: %5
%5 = apply %4(%0, %1, %2, %3) : $@convention(method) (Builtin.RawPointer, Builtin.Word, Builtin.Int1, @thin String.Type) -> @owned String // user: %6
return %5 : $String // id: %6
} // end sil function '$ss5print_9separator10terminatoryypd_S2StFfA1_'
// print(_:separator:terminator:)
sil @$ss5print_9separator10terminatoryypd_S2StF : $@convention(thin) (@guaranteed Array<Any>, @guaranteed String, @guaranteed String) -> ()
// exit
// clang name: exit
sil [clang exit] @exit : $@convention(c) (Int32) -> Never
// @nonobjc SimpleClass.init()
sil shared [thunk] @$sSo11SimpleClassCABycfcTO : $@convention(method) (@owned SimpleClass) -> @owned SimpleClass {
// %0 "self" // users: %2, %1
bb0(%0 : $SimpleClass):
%1 = objc_method %0 : $SimpleClass, #SimpleClass.init!initializer.foreign : (SimpleClass.Type) -> () -> SimpleClass, $@convention(objc_method) (@owned SimpleClass) -> @owned SimpleClass // user: %2
%2 = apply %1(%0) : $@convention(objc_method) (@owned SimpleClass) -> @owned SimpleClass // user: %3
return %2 : $SimpleClass // id: %3
} // end sil function '$sSo11SimpleClassCABycfcTO'
// Array._endMutation()
sil shared [_semantics "array.end_mutation"] @$sSa12_endMutationyyF : $@convention(method) <Element> (@inout Array<Element>) -> () {
[%0: noescape! **]
// %0 // users: %9, %1
bb0(%0 : $*Array<Element>):
%1 = struct_element_addr %0 : $*Array<Element>, #Array._buffer // user: %2
%2 = struct_element_addr %1 : $*_ArrayBuffer<Element>, #_ArrayBuffer._storage // user: %3
%3 = struct_element_addr %2 : $*_BridgeStorage<__ContiguousArrayStorageBase>, #_BridgeStorage.rawValue // user: %4
%4 = load %3 : $*Builtin.BridgeObject // user: %5
%5 = end_cow_mutation %4 : $Builtin.BridgeObject // user: %6
%6 = struct $_BridgeStorage<__ContiguousArrayStorageBase> (%5 : $Builtin.BridgeObject) // user: %7
%7 = struct $_ArrayBuffer<Element> (%6 : $_BridgeStorage<__ContiguousArrayStorageBase>) // user: %8
%8 = struct $Array<Element> (%7 : $_ArrayBuffer<Element>) // user: %9
store %8 to %0 : $*Array<Element> // id: %9
%10 = tuple () // user: %11
return %10 : $() // id: %11
} // end sil function '$sSa12_endMutationyyF'
// String.init(_builtinStringLiteral:utf8CodeUnitCount:isASCII:)
sil [always_inline] [readonly] [_semantics "string.makeUTF8"] @$sSS21_builtinStringLiteral17utf8CodeUnitCount7isASCIISSBp_BwBi1_tcfC : $@convention(method) (Builtin.RawPointer, Builtin.Word, Builtin.Int1, @thin String.Type) -> @owned String
// UnsafeContinuation.resume<>(returning:)
sil shared [available 12.0.0] @$sScc6resume9returningyxn_ts5NeverORs_rlF : $@convention(method) <T, E where E == Never> (@in T, UnsafeContinuation<T, Never>) -> () {
// %0 // user: %3
// %1 // user: %2
bb0(%0 : $*T, %1 : $UnsafeContinuation<T, Never>):
%2 = struct_extract %1 : $UnsafeContinuation<T, Never>, #UnsafeContinuation.context // user: %3
%3 = builtin "resumeNonThrowingContinuationReturning"<T>(%2 : $Builtin.RawUnsafeContinuation, %0 : $*T) : $()
%4 = tuple () // user: %5
return %4 : $() // id: %5
} // end sil function '$sScc6resume9returningyxn_ts5NeverORs_rlF'
// Mappings from '#fileID' to '#filePath':
// 'main/main.swift' => 'main.swift'
The call to present
is roughly transformed to
UnsafeContinuation
, wrapping a RawUnsafeContinuation
, which seems to be just a raw pointer. This pointer is produced by the get_async_continuation_addr
instruction.present
(see the section marked "@objc completion handler block implementation for...")UnsafeContinuation.resume
await_async_continuation
instruction)Note that this is exactly what withUnsafeContinuation
does (source).
Compare the output with compiling this Swift file:
// main.swift
class SimpleClass {
func present(completion: (Bool) -> Void) {}
}
let foo = SimpleClass()
print(await withUnsafeContinuation { continuation in
foo.present { continuation.resume(returning: $0) }
})
The SIL code is roughly the same. There is just an explicit call to the withUnsafeContinuation
function, reflecting what you did in the Swift code. You can still see all the steps I mentioned above, because withUnsafeContinuation
is marked @_alwaysEmitIntoClient
. You can find the resume
call in the section marked "closure #1 in closure #1 in".