LoginSignup
9
4

Deep Dive Into SKIE Part 1

Posted at

SKIEとは

SKIEは2023年9月6日にTouchlabがOSSとして公開したKMPで使用するライブラリです。
2023年3月にブログ/Open Source Updatesで開発中であることがアナウンスされてから半年後の公開でした。
Apache License V2で公開されており、誰でも使用できます。
TouchLabはSQLiterStatelyを開発しており、KMPでアプリを作ろうとしたことがある方は見かけた方も多いでしょう。

なぜSKIEが必要なのか

KMPはKotlinのみでandroid, iOSへドメインロジック処理を提供できる素晴らしいツールです。
一方でチーム開発もしくはiOSエンジニアから見た際に幾つかの課題を抱えています。

  1. 生成されたFrameworkの一部のAPIはSwiftらしく呼べない
    1. enumは網羅性が欠落する
    2. sealed interface、sealed classは単なる実装もしくは継承しているクラスとしてみなされる
    3. Swift.async/awaitを使ってsuspend functionを呼べるのはmain threadのみ
    4. genericsを持ったinterface(protocol)はgenericsの型情報が欠落する
    5. default argumentの欠落
  2. build時間
  3. Kotlinであること

SwiftらしくKMPのAPIを呼び出せないのは開発時にObj-CもしくはKotlin側の実装を意識せざるを得なくなるため、非常に認知負荷がかかります。
これらの課題は基本的にKMPがObj-Cを経由してSwiftへとAPIを提供するために発生します。(androidエンジニアの方はJavaからKotlinを呼び出した際の制約をイメージしていただけるとわかりやすいです)
せっかく1つのコードで共通した処理の実装を実現できたとしても使用にハードルがあるとすればiOSエンジニアがKMPの導入に諸手を挙げて賛成する、という状況にはなりにくいでしょう。
またSKIE以前にも同様のサポートをするライブラリもありましたがCocoaPodsによる依存解決が必要になるなど現在のiOS開発の主流に追いついていない面が否定できませんでした。
そのような状況を打破する可能性があるため、SKIEは非常にセクシーなプロジェクトというわけです。

SKIEはなにを改善するのか

具体的にSKIEは以下を改善します。

  1. Kotlin.Enum、sealed interface、sealed classの網羅性担保
  2. Kotlin Coroutineのwrap
  3. Default Argumentのサポート

Kotlin.Enum, sealed interface、sealed classの網羅性担保

Kotlin.Enum without value

以下のenumを例にしてみていきます。

AnimalType.kt
enum class AnimalType {
    Dog,
    Cat,
    Human;
}
before

このようなenumはSKIEを使わずにビルドするとObj-Cのヘッダーでは以下のように表現されます。

shared.h
__attribute__((swift_name("KotlinEnum")))
@interface SharedKotlinEnum<E> : SharedBase <SharedKotlinComparable>
- (instancetype)initWithName:(NSString *)name ordinal:(int32_t)ordinal __attribute__((swift_name("init(name:ordinal:)"))) __attribute__((objc_designated_initializer));
@property (class, readonly, getter=companion) SharedKotlinEnumCompanion *companion __attribute__((swift_name("companion")));
- (int32_t)compareToOther:(E)other __attribute__((swift_name("compareTo(other:)")));
- (BOOL)isEqual:(id _Nullable)other __attribute__((swift_name("isEqual(_:)")));
- (NSUInteger)hash __attribute__((swift_name("hash()")));
- (NSString *)description __attribute__((swift_name("description()")));
@property (readonly) NSString *name __attribute__((swift_name("name")));
@property (readonly) int32_t ordinal __attribute__((swift_name("ordinal")));
@end

__attribute__((objc_subclassing_restricted))
__attribute__((swift_name("AnimalType")))
@interface SharedAnimalType : SharedKotlinEnum<SharedAnimalType *>
+ (instancetype)alloc __attribute__((unavailable));
+ (instancetype)allocWithZone:(struct _NSZone *)zone __attribute__((unavailable));
- (instancetype)initWithName:(NSString *)name ordinal:(int32_t)ordinal __attribute__((swift_name("init(name:ordinal:)"))) __attribute__((objc_designated_initializer)) __attribute__((unavailable));
@property (class, readonly) SharedAnimalType *dog __attribute__((swift_name("dog")));
@property (class, readonly) SharedAnimalType *cat __attribute__((swift_name("cat")));
@property (class, readonly) SharedAnimalType *human __attribute__((swift_name("human")));
+ (SharedKotlinArray<SharedAnimalType *> *)values __attribute__((swift_name("values()")));
@property (class, readonly) NSArray<SharedAnimalType *> *entries __attribute__((swift_name("entries")));
@end

上記のようなObj-CヘッダーをSwiftでは以下のように解釈します。

shared.swift
open class KotlinEnum<E> : KotlinBase, KotlinComparable where E : AnyObject {

    public init(name: String, ordinal: Int32)

    open class var companion: KotlinEnumCompanion { get }

    open func compareTo(other: E) -> Int32

    open func isEqual(_ other: Any?) -> Bool

    open func hash() -> UInt

    open func description() -> String

    open var name: String { get }

    open var ordinal: Int32 { get }
}

public class AnimalType : KotlinEnum<AnimalType> {

    open class var dog: AnimalType { get }

    open class var cat: AnimalType { get }

    open class var human: AnimalType { get }

    open class func values() -> KotlinArray<AnimalType>

    open class var entries: [AnimalType] { get }
}

そうです、Kotlin.Enumはenumではないのです。
そのためKMPで定義したenumを呼び出す際にはdefault句が必要となり網羅性が欠如します。
以下のようなラッパーをextension等で手で作成することも可能ですが列挙が追加された場合にコンパイルエラーにならず気付けないため運用にはハードルがあります。

CallEnum.swift
func call(value: AnimalType) {
    switch value {
    case .cat:
        print("mewo")
    case .dog:
        print("bow")
    case .human:
        print("...")
    default:
        fatalError("unknown animal")
    }
}
after

ではSKIEを導入するとどうなるでしょうか?

shared.h
__attribute__((swift_name("KotlinEnum")))
@interface SharedKotlinEnum<E> : SharedBase <SharedKotlinComparable>
@property (class, readonly, getter=companion) SharedKotlinEnumCompanion *companion __attribute__((swift_name("companion")));
@property (readonly) NSString *name __attribute__((swift_name("name")));
@property (readonly) int32_t ordinal __attribute__((swift_name("ordinal")));
- (instancetype)initWithName:(NSString *)name ordinal:(int32_t)ordinal __attribute__((swift_name("init(name:ordinal:)"))) __attribute__((objc_designated_initializer));
- (int32_t)compareToOther:(E)other __attribute__((swift_name("compareTo(other:)")));
- (BOOL)isEqual:(id _Nullable)other __attribute__((swift_name("isEqual(_:)")));
- (NSUInteger)hash __attribute__((swift_name("hash()")));
- (NSString *)description __attribute__((swift_name("description()")));
@end

__attribute__((objc_subclassing_restricted))
__attribute__((swift_name("AnimalType")))
@interface SharedAnimalType : SharedKotlinEnum<SharedAnimalType *>
@property (class, readonly) SharedAnimalType *dog __attribute__((swift_name("dog")));
@property (class, readonly) SharedAnimalType *cat __attribute__((swift_name("cat")));
@property (class, readonly) SharedAnimalType *human __attribute__((swift_name("human")));
@property (class, readonly) NSArray<SharedAnimalType *> *entries __attribute__((swift_name("entries")));
+ (instancetype)alloc __attribute__((unavailable));
+ (instancetype)allocWithZone:(struct _NSZone *)zone __attribute__((unavailable));
- (instancetype)initWithName:(NSString *)name ordinal:(int32_t)ordinal __attribute__((swift_name("init(name:ordinal:)"))) __attribute__((objc_designated_initializer)) __attribute__((unavailable));
+ (SharedKotlinArray<SharedAnimalType *> *)values __attribute__((swift_name("values()")));
@end
shared.swift
@frozen public enum AnimalType : String, Hashable, CaseIterable {

    case dog

    case cat

    case human

    public var name: String { get }

    public var ordinal: Int32 { get }

    public static func _forceBridgeFromObjectiveC(_ source: shared.__Skie.class__DeepDiveIntoSKIE_shared__io_github_ryunen344_skie_AnimalType, result: inout shared.AnimalType?)

    public static func _conditionallyBridgeFromObjectiveC(_ source: shared.__Skie.class__DeepDiveIntoSKIE_shared__io_github_ryunen344_skie_AnimalType, result: inout shared.AnimalType?) -> Bool

    public static func _unconditionallyBridgeFromObjectiveC(_ source: shared.__Skie.class__DeepDiveIntoSKIE_shared__io_github_ryunen344_skie_AnimalType?) -> shared.AnimalType

    public func _bridgeToObjectiveC() -> shared.__Skie.class__DeepDiveIntoSKIE_shared__io_github_ryunen344_skie_AnimalType

    public typealias _ObjectiveCType = shared.__Skie.class__DeepDiveIntoSKIE_shared__io_github_ryunen344_skie_AnimalType
}

extension AnimalType {

    public func toKotlinEnum() -> shared.__Skie.class__DeepDiveIntoSKIE_shared__io_github_ryunen344_skie_AnimalType
}

見事にenumになっていますね、一体どんな黒魔術をしているのでしょうか。
enumとして認識されているため、Xcode上でも不要なdefault caseだよとsuggestされるようになりました。

enum-without-value.png

Kotlin.Enum with value

値なしのベーシックなKotlin.Enumを綺麗なSwift.Enumに変換してくれることはわかりました。
では値ありのKotlin.Enumはどうでしょうか?
Swiftではraw valuesと呼ばれ、宣言1つにつき1つの値を持つことができます。
整数値浮動小数点数値文字列のみを割り当てられるという制約があります。

Color.kt
enum class Color(val rgb: Int) {
    RED(0xFF0000),
    GREEN(0x00FF00),
    BLUE(0x0000FF)
}
before

Obj-C上ではrgbは単なるクラスのメンバ変数として認識されています。
そのためrgbを取得するのは普段のraw value enumと変わらない感触で実装できます。

shared.h
__attribute__((objc_subclassing_restricted))
__attribute__((swift_name("Color")))
@interface SharedColor : SharedKotlinEnum<SharedColor *>
+ (instancetype)alloc __attribute__((unavailable));
+ (instancetype)allocWithZone:(struct _NSZone *)zone __attribute__((unavailable));
- (instancetype)initWithName:(NSString *)name ordinal:(int32_t)ordinal __attribute__((swift_name("init(name:ordinal:)"))) __attribute__((objc_designated_initializer)) __attribute__((unavailable));
@property (class, readonly) SharedColor *red __attribute__((swift_name("red")));
@property (class, readonly) SharedColor *green __attribute__((swift_name("green")));
@property (class, readonly) SharedColor *blue __attribute__((swift_name("blue")));
+ (SharedKotlinArray<SharedColor *> *)values __attribute__((swift_name("values()")));
@property (class, readonly) NSArray<SharedColor *> *entries __attribute__((swift_name("entries")));
@property (readonly) int32_t rgb __attribute__((swift_name("rgb")));
@end
shared.swift
public class Color : KotlinEnum<Color> {

    open class var red: Color { get }

    open class var green: Color { get }

    open class var blue: Color { get }

    open class func values() -> KotlinArray<Color>

    open class var entries: [Color] { get }

    open var rgb: Int32 { get }
}
CallEnum.swift
func call(value: Color) {
    switch value {
    case .red:
        print(value.rgb)
    case .green:
        print(value.rgb)
    case .blue:
        print(value.rgb)
    default:
        fatalError("unknown color")
    }
}
after
shared.h
__attribute__((objc_subclassing_restricted))
__attribute__((swift_name("Color")))
@interface SharedColor : SharedKotlinEnum<SharedColor *>
@property (class, readonly) SharedColor *red __attribute__((swift_name("red")));
@property (class, readonly) SharedColor *green __attribute__((swift_name("green")));
@property (class, readonly) SharedColor *blue __attribute__((swift_name("blue")));
@property (class, readonly) NSArray<SharedColor *> *entries __attribute__((swift_name("entries")));
@property (readonly) int32_t rgb __attribute__((swift_name("rgb")));
+ (instancetype)alloc __attribute__((unavailable));
+ (instancetype)allocWithZone:(struct _NSZone *)zone __attribute__((unavailable));
- (instancetype)initWithName:(NSString *)name ordinal:(int32_t)ordinal __attribute__((swift_name("init(name:ordinal:)"))) __attribute__((objc_designated_initializer)) __attribute__((unavailable));
+ (SharedKotlinArray<SharedColor *> *)values __attribute__((swift_name("values()")));
@end
shared.swift
@frozen public enum Color : String, Hashable, CaseIterable {

    case red

    case green

    case blue

    public var name: String { get }

    public var ordinal: Int32 { get }

    public var rgb: Int32 { get }

    public static func _forceBridgeFromObjectiveC(_ source: shared.__Skie.class__DeepDiveIntoSKIE_shared__io_github_ryunen344_skie_Color, result: inout shared.Color?)

    public static func _conditionallyBridgeFromObjectiveC(_ source: shared.__Skie.class__DeepDiveIntoSKIE_shared__io_github_ryunen344_skie_Color, result: inout shared.Color?) -> Bool

    public static func _unconditionallyBridgeFromObjectiveC(_ source: shared.__Skie.class__DeepDiveIntoSKIE_shared__io_github_ryunen344_skie_Color?) -> shared.Color

    public func _bridgeToObjectiveC() -> shared.__Skie.class__DeepDiveIntoSKIE_shared__io_github_ryunen344_skie_Color

    public typealias _ObjectiveCType = shared.__Skie.class__DeepDiveIntoSKIE_shared__io_github_ryunen344_skie_Color
}

extension Color {

    public func toKotlinEnum() -> shared.__Skie.class__DeepDiveIntoSKIE_shared__io_github_ryunen344_skie_Color
}

同様にenumとして認識されているため、Xcode上でも不要なdefault caseだよとsuggestされるようになりました。

enum-with-value.png

2種類のKotlin.Enumを見ていてお気づきの方もいると思いますが、SKIEはKotlin.Enumをstring raw value enumとして解釈するようです。
2個目の値付きKotlin.Enumであればenum Color: Intのような定義に変換されて欲しいところですが、その点はマッピングの制約が発生しているので注意が必要です。
Kotlinはvalue部にInt, Float, String以外も割り当てることが可能なためこのような制約が発生しています。

Kotlin.Enumの変数名(先述の例のrgbにあたる部分)をrawValueにすると__rawValueというメンバ変数として変換してくれるため、予約後の衝突を心配する必要はありません。

sealed class, sealed interface

sealed class, sealed interfaceは継承を同一ファイル内に制限することでコンパイル時にパターン網羅をチェックしてくれるenumのようでもあり、union typeのようでもある言語機能です。
SwiftのAssociated Valueが言語仕様として近いです。

以下のようなsealed class, sealed interfaceを例にしてみていきます。

SealedError.kt
sealed interface SealedError

sealed class IOError : SealedError

class FileReadError(val file: String) : IOError()
class DatabaseError(val source: String) : IOError()

data object RuntimeError : SealedError

今回の定義では👇のようなSwift.Enumに見えるとSwiftらしくswitch文が書けるでしょう。

SealedError.swift
enum SealedError {
    case IOError
    case RuntimeError
}

enum IOError {
    case FileReadError(String)
    case DatabaseError(String)
}
before

SKIEを使わない場合は以下のような定義になります。

shared.h
__attribute__((swift_name("SealedError")))
@protocol SharedSealedError
@required
@end

__attribute__((swift_name("IOError")))
@interface SharedIOError : SharedBase <SharedSealedError>
@end

__attribute__((objc_subclassing_restricted))
__attribute__((swift_name("DatabaseError")))
@interface SharedDatabaseError : SharedIOError
- (instancetype)initWithSource:(NSString *)source __attribute__((swift_name("init(source:)"))) __attribute__((objc_designated_initializer));
@property (readonly) NSString *source __attribute__((swift_name("source")));
@end

__attribute__((objc_subclassing_restricted))
__attribute__((swift_name("FileReadError")))
@interface SharedFileReadError : SharedIOError
- (instancetype)initWithFile:(NSString *)file __attribute__((swift_name("init(file:)"))) __attribute__((objc_designated_initializer));
@property (readonly) NSString *file __attribute__((swift_name("file")));
@end
shared.swift
public protocol SealedError {}

open class IOError : KotlinBase, SealedError {}

public class DatabaseError : IOError {
    public init(source: String)

    open var source: String { get }
}

public class FileReadError : IOError {
    public init(file: String)

    open var file: String { get }
}

Kotlinでの言語仕様の定義通り、protocolの準拠もしくはクラスの継承で定義されることがわかります。
そのためsealed class, sealed interfaceをSwiftからType Safeに呼び出すためにはtype castが必要になります。
type castのタイミングでチェックすべき順番、クラスが漏れ落ちるため網羅性が欠落します。
実際には以下のようなコードでtype castを行い、プロパティを取得することになります。

CallSealedClass.swift
func call(error: SealedError) {
    switch error {
    case let error as IOError:
        print("io error")
        switch error {
            case let io as FileReadError:
                print("file read error \(io.file)")
            case let database as DatabaseError:
                print("database error \(database.source)")
            default:
                fatalError("unknown io error")
        }
    case _ as RuntimeError:
        print("runtime error")
    default:
        fatalError("unknown error")
    }
}

見るからに大変ですね。手運用でこの網羅性をカバーするのは非常にしんどいです。

after

ではSKIEを使用するとどうなるでしょうか?

まずはObj-Cです

shared.h
__attribute__((swift_name("SealedError")))
@protocol SharedSealedError
@required
@end

__attribute__((swift_name("IOError")))
@interface SharedIOError : SharedBase <SharedSealedError>
@end

__attribute__((objc_subclassing_restricted))
__attribute__((swift_name("DatabaseError")))
@interface SharedDatabaseError : SharedIOError
@property (readonly) NSString *source __attribute__((swift_name("source")));
- (instancetype)initWithSource:(NSString *)source __attribute__((swift_name("init(source:)"))) __attribute__((objc_designated_initializer));
@end

__attribute__((objc_subclassing_restricted))
__attribute__((swift_name("FileReadError")))
@interface SharedFileReadError : SharedIOError
@property (readonly) NSString *file __attribute__((swift_name("file")));
- (instancetype)initWithFile:(NSString *)file __attribute__((swift_name("init(file:)"))) __attribute__((objc_designated_initializer));
@end

次にSwiftです。

shared.swift
public protocol SealedError {}

open class IOError : KotlinBase, SealedError {}

public class DatabaseError : IOError {
    open var source: String { get }

    public init(source: String)
}

public class FileReadError : IOError {
    open var file: String { get }

    public init(file: String)
}

extension __SwiftGen.IOError {

    @frozen public enum Enum {

        case databaseError(shared.__Skie.class__DeepDiveIntoSKIE_shared__io_github_ryunen344_skie_DatabaseError)

        case fileReadError(shared.__Skie.class__DeepDiveIntoSKIE_shared__io_github_ryunen344_skie_FileReadError)
    }
}

extension __SwiftGen.SealedError {

    @frozen public enum Enum {

        case iOError(shared.__Skie.class__DeepDiveIntoSKIE_shared__io_github_ryunen344_skie_IOError)

        case runtimeError(shared.__Skie.class__DeepDiveIntoSKIE_shared__io_github_ryunen344_skie_RuntimeError)
    }
}

public func onEnum<SEALED>(of sealed: SEALED) -> shared.__SwiftGen.IOError.Enum where SEALED : IOError

public func onEnum<SEALED>(of sealed: SEALED) -> shared.__SwiftGen.SealedError.Enum where SEALED : SealedError

SKIEは内部でsealed class, sealed interfaceに対応したAssociated Valueを定義することで網羅的にハンドルするAPIを提供します。
そのため以下のようにdefault句なしでsealed class, sealed interfaceを呼び出せます。

CallSealedClass.swift
func call(error: SealedError) {
    switch onEnum(of: error) {
    case .iOError(let ioError):
        switch onEnum(of: ioError) {
        case .databaseError(let db):
            print("database error \(db.source)")
        case .fileReadError(let file):
            print("file read error \(file.file)")
        }
    case .runtimeError(_):
        print("runtime error")
    }
}

onEnumメソッドでunwrapする点は注意ですが、コンパイル時に網羅性チェックが行えるようになりました。

Kotlin Coroutineのwrap

そもそもK/NはKotlin Coroutineのネイティブサポートを謳っています。
これはKotlinのsuspend functionをObj-CにcompletionHandlerとして提供することで非同期処理をiOS側から呼び出せるようにしている、ということです。
SwiftのcompilerがcompletionHandlerをasync/awaitとして呼べるように解釈してくれるため、特別な設定なしでSwift.async/awaitからKotlin suspend functionを呼べるようになっています。

Interoperability with Swift/Objective-C#Mappings

SKIEを使わずに👇のようなKotlinコードをビルドすると

Suspend.kt
suspend fun callSuspend() {
    println("called suspend function")
}

👇のようなObj-Cヘッダーにexportするため

shared.h
__attribute__((objc_subclassing_restricted))
__attribute__((swift_name("SuspendKt")))
@interface SharedSuspendKt : SharedBase
+ (void)callSuspendWithCompletionHandler:(void (^)(NSError * _Nullable))completionHandler __attribute__((swift_name("callSuspend(completionHandler:)")));
@end

👇のようにSwiftは解釈されます

shared.swift
public class SuspendKt : KotlinBase {

    open class func callSuspend(completionHandler: @escaping (Error?) -> Void)

    open class func callSuspend() async throws
}

結果として👇のように特別な呼び出し方をせずとも呼び出せるようになっています

Suspend.swift
func callKmpSuspend() {
    // async/awaitの場合
    Task {
        try! await SuspendKt.callSuspend()
    }
    
    // completion handlerの場合
    SuspendKt.callSuspend { _ in
        print("called completion handler")
    }
}

このように特別な設定をせずともiOSから呼び出せるCoroutineですが、実はいくつかの課題があります。

  1. Mainスレッドからしか呼べない(experimentalなcompiler flagを使用することで制限解除できる)
  2. Coroutineのキャンセルを行うためにはCoroutineのAPIをSwiftに露出させなければならない
  3. Coroutine FlowがGenericsを持ったinterface(protocol)のため型情報が欠落する
  4. Coroutine FlowをSwift.async/awaitで使用するためにはラッパークラスが必要

ではSKIEはどのように改善してくれるのかを見ていきます。

Suspend Function

今まではCoroutineScopeをSwiftに露出させ、自前でwrapper実装を書いたり、KMP-NativeCoroutinesなどを用いることでキャンセル可能にすることができました。
SKIEでも同様に内部でキャンセル可能な実装でラップし、その実装を強制的に使用させるextensionをビルド時に生成します。

io.github.ryunen344.skie.callSuspend.swift
public extension SuspendKt {
  @available(iOS 13, macOS 10.15, watchOS 6, tvOS 13, * )
  static func callSuspend() async throws {
    return try await SwiftCoroutineDispatcher.dispatch {
      __Skie.file__shared____SkieSuspendWrappersKt.Skie_Suspend__0__callSuspend(suspendHandler:)($0)
    }
  }
}

実際にラップしている実装を深ぼっていきます。SwiftとKotlinコード両方が登場する点、ライブラリがruntimeとして含めているコードと自動生成されるコードが入り乱れる点に注意してください。
ポイントは3つです。

  1. __SkieSuspendWrappers.kt
  2. SwiftCoroutineDispatcher.swift
  3. Skie_CancellationHandler.kt, Skie_SuspendHandler.kt
__SkieSuspendWrappers.kt

__SkieSuspendWrappersskie-compiler-pluginによって生成される、susupend functionを強制的にラップして呼び出すためのクラスです。
このクラスとこのクラスのメソッドを呼び出すextensionをcompiler pluignで生成することによって後述するSKIEの仕組みを強制的に差し込んでいます。
Coroutine Flowのcollect, emitメソッドもsuspend functionなためヘッダーファイルに登場しています。

shared.h
__attribute__((objc_subclassing_restricted))
__attribute__((swift_name("__SkieSuspendWrappersKt")))
@interface Shared__SkieSuspendWrappersKt : SharedBase
+ (void)Skie_Suspend__0__callSuspendSuspendHandler:(SharedSkie_SuspendHandler *)suspendHandler __attribute__((swift_name("Skie_Suspend__0__callSuspend(suspendHandler:)")));
+ (void)Skie_Suspend__1__hasNextDispatchReceiver:(SharedSkieColdFlowIterator<id> *)dispatchReceiver suspendHandler:(SharedSkie_SuspendHandler *)suspendHandler __attribute__((swift_name("Skie_Suspend__1__hasNext(dispatchReceiver:suspendHandler:)")));
+ (void)Skie_Suspend__2__collectDispatchReceiver:(id<SharedKotlinx_coroutines_coreFlow>)dispatchReceiver collector:(id<SharedKotlinx_coroutines_coreFlowCollector>)collector suspendHandler:(SharedSkie_SuspendHandler *)suspendHandler __attribute__((swift_name("Skie_Suspend__2__collect(dispatchReceiver:collector:suspendHandler:)")));
+ (void)Skie_Suspend__3__emitDispatchReceiver:(id<SharedKotlinx_coroutines_coreFlowCollector>)dispatchReceiver value:(id _Nullable)value suspendHandler:(SharedSkie_SuspendHandler *)suspendHandler __attribute__((swift_name("Skie_Suspend__3__emit(dispatchReceiver:value:suspendHandler:)")));
@end
SwiftCoroutineDispatcher.swift

次に見ていくのは生成されたextensionの内部で使用されているCoroutineDispatcherと名付けられているstructです。
これはライブラリがruntime用に提供しているためGitHub上でコードを確認することができます。
SwiftCoroutineDispatcher.swift

CoroutineDispatcherと名付けられていますがkotlinx.coroutines.CoroutineDispatcherを継承していません。
内部で後述するSkie_CancellationHandlerとSkie_SuspendHandlerを使用し、Swift.async/awaitによるキャンセルを実現しています。

SwiftCoroutineDispatcher#dispatchのメソッドの中身を大まかに説明すると以下のような内容になっています。

SwiftCoroutineDispatcher.swift
struct SwiftCoroutineDispatcher {
    static func dispatch<T>(
        coroutine: (Skie_SuspendHandler) -> Swift.Void
    ) async throws -> T {
        // cancelをハンドリングするためのクラス、後述で説明
        let cancellationHandler = Skie_CancellationHandler()
        // Swift.async/awaitのwithTaskCancellationHandlerを用いてキャンセル時にCoroutineのキャンセルを行う
        return try await _Concurrency.withTaskCancellationHandler(operation: {
            var result: Swift.Result<T, Swift.Error>? = nil
            let dispatcher = _Concurrency.AsyncStream<Kotlinx_coroutines_coreRunnable> { continuation in
                // Swift.AsyncSequenceでCoroutineのRunnableを保持するためのdelegate
                let dispatcherDelegate = AsyncStreamDispatcherDelegate(continuation: continuation)
                
                // 内部でcoroutine dispatcherを保持し、dispatchDelegate内のRunnableを逐次処理していく
                let suspendHandler = Skie_SuspendHandler(
                    cancellationHandler: cancellationHandler,
                    dispatcherDelegate: dispatcherDelegate,
                    onResult: { suspendResult in
                        let convertedResult: Swift.Result<T, Swift.Error> = convertToResult(suspendResult: suspendResult)
                        result = convertedResult
                        dispatcherDelegate.stop()
                    }
                )

                coroutine(suspendHandler)
            }

            // cancelなしのTaskとしてAsyncStreamのRunnableを実行していく
            await _Concurrency.Task {
                for await block in dispatcher { block.run() }
            }.value
            
            // cast類を行いSwiftのResult
            return try unwrap(result: result)
        }, onCancel: {
            // 呼び出し元からキャンセルが実行された場合はcoroutineもキャンセルする
            cancellationHandler.cancel()
        })
    }
}
Skie_CancellationHandler.kt, Skie_SuspendHandler.kt

Skie_CancellationHandler, Skie_SuspendHandler共にライブラリがruntime用に提供しているクラスです。
Skie_CancellationHandler.kt
Skie_SuspendHandler.kt

Skie_CancellationHandlerは内部でキャンセル処理が実行されたかをenumで管理しています。CoroutineContext, CoroutineScopeを用いずにcancelされたかを管理するためのhandlerクラスです。
Skie_SuspendHandlerはCoroutineScopeを完全に隠蔽しきるため、Swift側にCoroutineScopeを露出させずに非同期処理をSwiftから扱える様にします。

Skie_SuspendHandler.kt
class Skie_SuspendHandler(
    private val cancellationHandler: Skie_CancellationHandler,
    dispatcherDelegate: Skie_DispatcherDelegate,
    private val onResult: (Skie_SuspendResult) -> Unit,
) {
    // DispacheQueue, Swift.async/awaitなどをCoroutine Dispatcher以外の実行制御機構をCoroutine上で認識させるためのラッパー
    private val dispatcher = CoroutineDispatcherWithDelegate(dispatcherDelegate)

    // __SkieSuspendWrappersでラップされたsuspend functionが呼び出すentry point
    internal fun launch(checkedExceptions: Array<KClass<out Throwable>>, coroutine: suspend () -> Any?) {
        CoroutineScope(dispatcher).launch {
            cancellationHandler.setCancellationCallback {
                // cancellationHandlerでcancelされた場合はCoroutineをcancelする
                cancel()
            }

            try {
                val result = coroutine.invoke()

                onResult(Skie_SuspendResult.Success(result))
            } catch (_: CancellationException) {
                onResult(Skie_SuspendResult.Canceled)
            } catch (e: Throwable) {
                if (e.isCheckedException(checkedExceptions)) {
                    // 
                    throwSwiftException(e)
                } else {
                    throw e
                }
            }
        }
    }

    // @Throwsで事前に定義されているExceptionかチェックする
    private fun Throwable.isCheckedException(checkedExceptions: Array<out KClass<out Throwable>>): Boolean =
        checkedExceptions.any { it.isInstance(this) }

    // Kotlin.ThrowableをNSErrorに変換してSwift.async/awaitのResultに伝える
    private fun throwSwiftException(e: Throwable) {
        val error = e.toNSError()

        onResult(Skie_SuspendResult.Error(error))
    }
}
Suspend Functionまとめ

SKIEは自動生成とkotlin側でCoroutineをcallback形式で管理する仕組みを提供することでSwift.async/awaitを用いつつCoroutine APIをSwift側から叩くことなくキャンセルさせる仕組みを提供します。

Coroutine Flow

Kotlin.Flow#collectKotlin.Flow#emitはsuspend functionのため先述の仕組みで使用できるようになっていますが、Coroutine Flowが抱えている残りの問題はどのように対応しているのでしょうか?

Coroutine FlowがGenericsを持ったinterface(protocol)のため型情報が欠落する
Coroutine FlowをSwift.async/awaitで使用するためにはラッパークラスが必要

SKIEのFlowラッパーを見てみるとFlow, SharedFlow, StateFlowそれぞれのラッパーがkotlinとswiftの両方で定義されています。
listed wrapper

SkieKotlinFlow, SkieSwiftFlowを例にしてそれぞれを見ていきましょう。

SkieKotlinFlow.kt
@OptIn(ExperimentalObjCName::class)
class SkieKotlinFlow<out T : Any>(@ObjCName(swiftName = "_") private val delegate: Flow<T>) : Flow<T> by delegate

SkieKotlinFlowはFlowをそのままSwiftから参照するとprotocolのgenericsが欠落してしまうためkotlin側で一度ラップした上で型情報が欠落させないためのクラスです。
ObjCNameはObj-Cヘッダー上で変数名がぶつかった際にビルドができなくならないようにヘッダーにexportする変数名を変更するアノテーションです。

SkieSwiftFlow.swift
public final class SkieSwiftFlow<T>: _Concurrency.AsyncSequence, Swift._ObjectiveCBridgeable {

    public typealias AsyncIterator = SkieSwiftFlowIterator<T>

    public typealias Element = T

    public typealias _ObjectiveCType = SkieKotlinFlow<Swift.AnyObject>

    internal let delegate: Skie.org_jetbrains_kotlinx__kotlinx_coroutines_core.Flow.__Kotlin

    internal init(internal flow: Skie.org_jetbrains_kotlinx__kotlinx_coroutines_core.Flow.__Kotlin) {
        delegate = flow
    }

    public func makeAsyncIterator() -> SkieSwiftFlowIterator<T> {
        return SkieSwiftFlowIterator(flow: delegate)
    }

    public func _bridgeToObjectiveC() -> _ObjectiveCType {
        return SkieKotlinFlow(delegate)
    }

    public static func _forceBridgeFromObjectiveC(_ source: _ObjectiveCType, result: inout SkieSwiftFlow<T>?) {
        result = fromObjectiveC(source)
    }

    public static func _conditionallyBridgeFromObjectiveC(_ source: _ObjectiveCType, result: inout SkieSwiftFlow<T>?) -> Bool {
        result = fromObjectiveC(source)
        return true
    }

    public static func _unconditionallyBridgeFromObjectiveC(_ source: _ObjectiveCType?) -> SkieSwiftFlow<T> {
        return fromObjectiveC(source)
    }

    private static func fromObjectiveC(_ source: _ObjectiveCType?) -> SkieSwiftFlow<T> {
        return SkieSwiftFlow(internal: source!)
    }
}

delegateとしてkotlin.Flowを保持しつつ_Concurrency.AsyncSequence, Swift._ObjectiveCBridgeableを準拠することでswiftから見るとシンプルなAsyncSequenceとして見える様にラップしています。

以下の様なStateFlowが公開されているクラスをビルドすると

ObservableHolder.kt
class ObservableHolder {
    private val _state: MutableStateFlow<Int> = MutableStateFlow(0)
    val state = _state.asStateFlow()
}
shared.h
__attribute__((objc_subclassing_restricted))
__attribute__((swift_name("ObservableHolder")))
@interface SharedObservableHolder : SharedBase
@property (readonly) id<SharedKotlinx_coroutines_coreStateFlow> state __attribute__((swift_name("state")));
- (instancetype)init __attribute__((swift_name("init()"))) __attribute__((objc_designated_initializer));
+ (instancetype)new __attribute__((availability(swift, unavailable, message="use object initializers instead")));
@end

ObservableHolder.stateはObj-Cのヘッダー上ではkotlin.StateFlowとして公開されていますがXcode上で確認するとSkieSwiftStateFlowとなっています。

flow holder state

SkieSwiftStateFlowはAsyncSequenceを準拠しているため素のAsyncSequenceとしてSwift側で扱うことができます。

SampleFlow.swift
func hoge() async {
    let holder = ObservableHolder()
    Task {
        for await num in holder.state {
            print(num)
        }
    }
}

つまりSKIEは👇のような構成でgenericsの欠落を防ぐと同時にSwift.async/awaitで扱える様に変換しています。

Obj-Cヘッダー上ではKotlinx_coroutines_coreStateFlowとされているのにXcode上では直接SkieSwiftStateFlowとして参照できる理由はPart2で解説します。

9
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
9
4