SKIEが一体何をやっているのか調べたメモ

こんにちは、こんばんは、kitakkunです。

Kotlin Conf 2024の以下のセッションで何度かSKIEについて言及されていたので、気になって調べた雑なメモです。

<iframe width="560" height="315" src="https://www.youtube.com/embed/P_5ZEtK05kc?si=yDQVQN_pYdPPibx9" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe>

ほぼ公式ドキュメントの概略をまとめた内容で、詳細な内部実装までは本記事では言及しません。元が英文なのと、筆者のiOS周辺知識が不足しているので、一部間違ってたらコメントなどでご指摘ください。

# SKIEとは

SKIE = Swift Kotlin Interface Enhancer

発音はスカイ(sky)。

KMPで書き出されたObjective-CライブラリはSwiftからアクセスする際、様々な言語機能の制限を受ける。

Kotlin Native の Compiler Plugin を使って Xcode Framework を改変することで上記問題を回避しているらしい。

https://skie.touchlab.co/intro#overview

# SKIEがサポートする機能

ざっくり以下をサポートしているらしい([https://skie.touchlab.co/features](https://skie.touchlab.co/features))。

- 列挙(enum、sealed class/interface)
- 関数(デフォルト引数、グローバル関数、インタフェースの拡張関数、オーバーロード)
- コルーチン(suspend関数、Flow)
- Swift Code Bundling

## 列挙

そのままだと網羅的な条件分岐が使えず、毎回 switch 文に default case を書かなければいけない。

SKIEはKotlinのenum classやsealed classをラップするSwiftのenumを作って対応している。

enum classはSKIEが生成したSwiftのラップ版に置き換えてくれるので特別な作業はいらないが、sealed classに関してはインスタンスが別クラスになってしまうので、Swift側ではonEnum(of:) を使ってKotlinのenumをSwiftのenumに変換して使う必要がある。

## 関数

### デフォルト引数

デフォルト引数はObjective-Cがサポートしてない。SKIEはオーバーロードの関数宣言を全部生成することで実質的にデフォルト引数がある挙動を実現している。

ただし、やりすぎると目に見えてオーバーヘッドがでかくなるので、デフォルトでは無効になっているみたい。

### グローバル関数

通常Kotlin側に定義されたグローバル関数(トップレベル関数)は、ファイル名に続けて呼ぶ必要がある。

```kotlin
FileKt.globalFunction(i: 0)
```

SKIEを導入すると`FileKt.`の部分を省いてKotlinと同じように呼べるラッパーを作ってくれる。

### インタフェースの拡張関数

グローバル関数と同じくファイル名書いて呼ばないといけなくてめんどいし、通常の関数みたいに引数でレシーバーを渡さないといけない。

```kotlin
FileKt.interfaceExtension(C(), i: 1)
```

SKIEはグローバル関数のときと同じくラッパーを作って(？)、通常の拡張関数と同じ感じで呼び出せるようにしてくれる。

### オーバーロード

KotlinもSwiftも関数の引数型によるオーバーロード宣言が可能だが、Objective-Cはそれができない。

そのためKotlin Compilerは自動的に変数名を変えることによって衝突を防いでいる。

例えば以下のようなKotlinの関数があったとして、
```kotlin
fun foo(i: Int) {
}

fun foo(i: String) {
}
```

Swift側では通常以下のように呼び出すことになる(2つ目のfooの引数名がリネームされる)。

```swift
foo(i: 1)

foo(i_: "A")
```

このようなリネームは本来Swiftには不要。SKIEはこれらの関数をそのまま使えるようにしてくれる。

```swift
foo(i: 1)

foo(i: "A")
```

## コルーチン

### suspend関数

Kotlin Compilerはsuspend関数をObjective-Cのコールバック関数としてコンパイルする。この関数は、Swiftからasync/await構文で呼び出すことができる。

ただし、これには以下の問題がある。
- Swiftではこれらのコールバック関数をキャンセルできない
- suspend関数を必ずメインスレッドから呼び出さなければならない

キャンセルができない問題に関しては、SKIEがKotlinとSwiftの両方のプラットフォームの非同期処理ランタイムを相互接続する機構を備えたasync関数を生成することで回避する。

また、実行スレッドの制限はSwiftのコンパイラーオプションを変更することで回避できるが、SKIEが勝手にいい感じにやってくれるらしい。

ドキュメントから拝借したものだが、例えば以下のKotlinのsuspend関数は
```kotlin
class ChatRoom {
    suspend fun send(message: String) {
        // Send a message
    }
}
```

Swiftからは以下のように呼び出せるらしい(筆者はAndroidエンジニアなので詳しいことは知らないが、通常の非同期処理と同様の感覚で呼び出せる的なニュアンスの説明があったので、iOSエンジニア目線での違和感はそんなにないと思われる)
```swift
let chatRoom = ChatRoom()

let task = Task.detached { // Runs on a background thread
    try? await chatRoom.send(message: "some message")
}

// Cancellation of the task also cancels the Kotlin coroutine
task.cancel()
```

### Flow

Flowは通常のKotlinのインタフェースであり、Swiftの独自実装のAsyncSequenceと互換性がない。

直接的な互換APIもなく、キャストして使うこともできない。

SKIEはFlowをSwiftのAsyncSequenceを実装したカスタムクラスへとコンパイルする。
通常インタフェースの型引数(Flow<T>のTの部分)はコンパイルで失われてしまうが、カスタムクラスにコンパイルする際にその情報は引き継がれるので、Swift側で特別に型のキャストが必要になることはないらしい。

## Swift Code Bundling

SKIEを使うと、自分で書いたSwiftコードの一部をKotlinで作られたフレームワークの中にダイレクトに埋め込むことができる。

# Kotlin Swift Export について

ここまで説明してきた内容は、Kotlinのコードが直接SwiftではなくObjective-Cとしてエクスポートされていたことに起因しているものが多い。

現在JetBrainsはKotlin Swift Exportの開発をアクティブに進めていて、これが実現するともう少しKMPがiOS開発者にとって身近な存在になっていくかもなので、期待したいところ。

https://github.com/Kotlin/swift-export-sample

# まとめ

本記事では公式ドキュメントで紹介されているSKIEの機能と役割について簡単に整理してみました。Kotlin NativeのCompiler Pluginということで、詳細な内部実装も気になるところです。

仮に今後Kotlin Swift Exportが安定化した際に、SKIEがどうなっていくのかは未知数ですが、Objective-Cとして書き出される現在の状態でも、割とSwiftから使いやすい状態にはできそうな印象です。

KMPの開発に取り組む際は、他の言語から見た使い勝手も無視できない要素だなとあらためて思いました。