Как API-интерфейсы завершения Objective-C становятся доступными через async-await?

В UIKit есть несколько старых добрых API-интерфейсов на основе завершения, например вот этот:

func open(
    _ url: URL,
    options: [UIApplication.OpenExternalURLOptionsKey : Any] = [:],
    completionHandler completion: (@MainActor @Sendable (Bool) -> Void)? = nil
)

Но также, с появлением Swift Structured Concurrency, они добавили async версии этих же методов:

func open(
    _ url: URL, 
    options: [UIApplication.OpenExternalURLOptionsKey : Any] = [:]
) async -> Bool

Об этом преобразовании существует полная документация, и в ней говорится, что привязки async/await для таких методов генерируются автоматически по фиксированному определенному алгоритму.

Но меня интересует следующее - как именно происходит преобразование?

Я предполагаю, что он использует один из двух возможных подходов:

  • буквально просто оберните звонок в продолжение
  • какой-то другой сложный магический способ

Итак, какой из них они используют, как они это делают?


50
1

Ответ:

Решено

По сути, он просто использует продолжение, точно так же, как вы соединяете API-интерфейсы обработчика завершения с Swift Concurrency с помощью withXXXContinuation { ... }. Сгенерированный SIL мало чем отличается.

Я написал два таких файла:

// 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())

и скомпилирован в SIL с помощью swiftc -emit-sil -import-objc-header foo.h main.swift.

Результат

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'

Вызов present примерно преобразуется в

  • создайте UnsafeContinuation, обернув RawUnsafeContinuation, который кажется просто необработанным указателем. Этот указатель создается инструкцией get_async_continuation_addr.
  • передать замыкание present (см. раздел с пометкой «Реализация блока обработчика завершения @objc для...»)
  • в завершение звоните UnsafeContinuation.resume
  • дождитесь результата (см. инструкцию await_async_continuation)

Обратите внимание, что именно это и делает withUnsafeContinuation (источник).

Сравните вывод с компиляцией этого файла Swift:

// main.swift
class SimpleClass {
    func present(completion: (Bool) -> Void) {}
}

let foo = SimpleClass()
print(await withUnsafeContinuation { continuation in
    foo.present { continuation.resume(returning: $0) }
})

Код SIL примерно такой же. Есть просто явный вызов функции withUnsafeContinuation, отражающий то, что вы сделали в коде Swift. Вы по-прежнему можете увидеть все шаги, которые я упомянул выше, потому что withUnsafeContinuation отмечен @_alwaysEmitIntoClient. Вы можете найти вызов resume в разделе с пометкой «закрытие №1 в замыкании №1».