swiftcocoastructured-concurrency

How are Objective-C `completion` APIs made available via async-await?


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?


Solution

  • 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

    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".