📦

Swift Exportはどこから来たのか、Swift Exportは何者か、Swift Exportはどこへ行くのか

2024/12/13に公開

こんにちは。Androidエンジニアの豊川です。

この記事は、kubell Advent Calendar 2024(シリーズ 1)の13日目の記事です。


先日、Kotlin Multiplatform (KMP)のロードマップが更新されました。

魅力的な内容がたくさんありましたが、自分は特にSwift Exportがロードマップに加わることへ注目しています。

この機能は以前から要望が多く、気になっていた方も多いのではないでしょうか。

加えてKotlin 2.1.0にて早期アクセスが始まり、今後のアップデートがますます楽しみです。

一方で、Swift Exportは公開されて間もないため、情報が分散しており、調べる中で理解するのに手間がかかると感じました。
そこで本記事では、Swift Exportについて調査し、どこから来たのか(なぜ作られたか)、何者か(何ができるか)、どこへ行くのか(どうなるのか)、の観点でまとめていきたいと思います。

要約

  • Swift Exportはまだアルファリリースにも至っていない
  • Objective-Cを介して実行していたものがSwiftを介して実行するようになる
    • KMPのiOS開発が快適になる
  • 実装を理解するにはKotlinを読む必要がある(全てをSwiftに変換するわけではない)
  • 現時点(Kotlin 2.1.0)でできることは限られている
    • final classのExport
    • マルチモジュールのExport
    • Gradle Moduleに対してカスタムのSwift モジュール名を設定する
    • flattenPackage プロパティを使用し、パッケージ構造を簡略化するルールの設定
  • First Releaseで既存のObjective-C Exportと同等の機能を提供する予定
  • 順調にいけば Enum, data class/object, Sealed class/interface, Flowなどがサポートされる可能性もある

Swift Exportはどこから来たのか(なぜ作られたか)

Swift Exportの話が登場したのは、私の観測範囲では2024年のロードマップでの言及が初めてだったと記憶しています。

どのような動機で作られたかに関しては、Swift Export のアーキテクチャに関するドキュメントのA bit of history: Objective-C exportで言及されています。

従来の Objective-C Exportでは

  • Kotlin宣言記述子のツリーからObjective-C宣言を生成する
  • これらのObjective-C宣言を対応するバックエンドIRノードに関連付ける
  • Objective-C <-> Kotlin ブリッジの LLVM IR(LLVMコンパイラにおける中間表現) を生成する
  • すべてをAppleのフレームワークにまとめる(.framework)

というアプローチで実装されていました。これらはうまく動作していたのですが、下記のような課題がありました。

  • IDEとの統合が難しい
  • Kotlin/NativeにおけるLLVM IRジェネレーターが2つ存在する
  • K1のレガシーであるディスクリプターに依存している
  • フレームワーク以外の成果物との統合が困難

例えば、『IDEとの統合が難しい』『K1のレガシーであるディスクリプターに依存している』という点は、普段Kotlin、特にKMPを利用している開発者なら直感的に理解しやすいのではないでしょうか。

これらの課題を解決する、という目的に加え、Analysis APIの登場、klib(Kotlin/Nativeで使用される、Kotlinコードとメタデータを内包するライブラリ形式), LLVM IRの周辺の成熟という背景も相まって、Swift Exportの開発につながりました。

Swift Exportは何者か

何者か

端的に言うと、Kotlin で書いたコードを、Objective-Cを介さずにSwiftから利用できるようにする機能です。

従来のKMPでは概ね下記のような流れでKotlinのコードをSwiftから利用していました。

Kotlin -> Kotlin/Native -> ネイティブバイナリ -> **Objective-C(.h)** <- Swift 

Kotlinで実装されたコードをネイティブバイナリにし、そのインターフェースとしてObjective-Cのヘッダーファイルを介してSwiftからアクセスする、というイメージです(これをドキュメント内ではObjective-C Exportと呼んでいます)

Swift Exportを使うことで、下記のように変わります

Kotlin -> Kotlin/Native -> ネイティブバイナリ -> **Swift(.swift)** <- Swift

インターフェースであるObjective-C(.h) が Swift(.swift) に変わるため、Objective-Cを介することなくKotlinのコードをSwiftから利用することができるようになり、KMPにおけるiOS開発のストレスを軽減することが期待できます。

何者ではないか

SwiftをKotlinにExportする機能ではない

こちらのスレッドで言及されているように、SwiftをKotlinにImportする機能ではありません(名前的に紛らわしいのですが)

上記の機能は便宜上Swift Importと呼ばれており、Swift Exportよりも大きく、難しいプロジェクトのため、Swift Exportよりも先にリリースされることはない、とも言われています。

そのため厳密には“Kotlin to Swift Export”という名称のほうがわかりやすいかもしれません。(実際、2024年のロードマップでは"direct Kotlin-to-Swift export"と呼ばれています)

詳細な実装をSwiftから読み解けるわけではない

Swift Exportという名前から、Kotlinで書かれたコードをSwiftに変換する機能のように思えるかもしれません(自分も最初その印象を抱いていました)がこれは誤りです。

先述でも少し触れていますが、Kotlinで実装したコードの詳細はネイティブバイナリに内包されます。そのため実装を理解するにはネイティブバイナリかKotlinを読む必要があります。
そのため基本的にはKotlinを読むことが必要になるでしょう。

Swift Export は何ができるか

現時点(Kotlin 2.1.0)でできること

Basic support for Swift export を見てわかるように現在 Swift Exportでできることは下記の4つです

  • final class(継承することができないクラス)のExport
  • マルチモジュールのExport
  • Gradle Moduleに対してカスタムのSwift モジュール名をつけることができる
  • flattenPackage プロパティを使用し、パッケージ構造を簡略化するルールの設定

これ以外、例えば open/abstract classなどの基本的な要素もまだサポートされていません。
まだアルファ版にも達していないので当然ですが、できることは限られています。

First Releaseでできること

上記に加えて First Releaseでは下記の機能がサポートされる予定です

  • interfaceのExport
  • open/abstract classeのExport(これに関しては現在進行中のようです。)
  • Swift ObjectをKotlinの関数に渡すことができる
  • Swiftのclass, structに Kotlinのinterface,class継承する

Swift Exportは何ができないか

First Releaseでは下記はサポートされない予定です

  • Enum
  • Sealed interface/class
  • data class/object
  • ライブラリ型のカスタム変換(kotlinx.coroutines.Flow を AsyncSequence に変換)

First ReleaseではObjective-C Exportと同等の機能をサポートすることを目指しているため、意図的に優先度を落としていることがこちらのIssueから読み取れます。

反面、順調に進んでいけばこれらの機能もサポートされていくとも捉えられるので、期待してもよいでしょう。

:::message info
上記の機能(Sealed interface, Flowなど)はKMPでもうまくサポートされておらず、SKIEなどサードパーティのライブラリを使うことで対応している場合があります。

Swift Exportが上記を対応すれば、これらのライブラリを使わずに済む可能性があります。
:::

Swift Exportを使うことでコードはどう変わるか

目を引く変化で言うと、下記のように変わります。

  • ヘッダーファイル(.h)からSwiftファイル(.swift)になる
  • モジュール毎にファイルが定義される
  • トップレベル関数が使いやすくなる
  • typealiasが使えるようになる

次の項目から実際のコードを交え、Swift Exportを使うことでどのようにコードが変わるのかを見ていきましょう。

参考コード

下記の公式のサンプルコードを元に説明していきます。

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

ヘッダーファイル(.h)からSwiftファイル(.swift)になる

先述している通り、ヘッダーファイルがSwiftファイルに変わります。
例えばサンプルのMyClassで比較して見ましょう

MyClass

class MyClass(val property: Int) {

    class Nested(val nestedProperty: Int)
}

typealias MyNested = MyClass.Nested

fun sum(a: MyClass, b: MyNested): Int =
    a.property + b.nestedProperty

fun sharedFunction(): Int = 15

ヘッダーファイル(.h)の場合

__attribute__((swift_name("MyClass")))
@interface SharedMyClass : SharedBase
- (instancetype)initWithProperty:(int32_t)property __attribute__((swift_name("init(property:)"))) __attribute__((objc_designated_initializer));
@property (readonly) int32_t property __attribute__((swift_name("property")));
@end

__attribute__((objc_subclassing_restricted))
__attribute__((swift_name("CommonKt")))
@interface SharedCommonKt : SharedBase
+ (int32_t)sharedFunction __attribute__((swift_name("sharedFunction()")));
+ (int32_t)sumA:(SharedMyClass *)a b:(SharedMyClassNested *)b __attribute__((swift_name("sum(a:b:)")));
@end

Swiftファイル(.swift)の場合

public final class MyClass : KotlinRuntime.KotlinBase {
    public var property: Swift.Int32 {
        get {
            return com_github_jetbrains_swiftexport_MyClass_property_get(self.__externalRCRef())
        }
    }
    public init(
        property: Swift.Int32
    ) {
        let __kt = com_github_jetbrains_swiftexport_MyClass_init_allocate()
        super.init(__externalRCRef: __kt)
        com_github_jetbrains_swiftexport_MyClass_init_initialize__TypesOfArguments__Swift_UInt_Swift_Int32__(__kt, property)
    }
}

public static func sharedFunction() -> Swift.Int32 {
    return com_github_jetbrains_swiftexport_sharedFunction()
}

public static func sum(
    a: MyClass,
    b: MyNested
) -> Swift.Int32 {
    return com_github_jetbrains_swiftexport_sum__TypesOfArguments__ExportedKotlinPackages_com_github_jetbrains_swiftexport_MyClass_ExportedKotlinPackages_com_github_jetbrains_swiftexport_MyClass_Nested__(a.__externalRCRef(), b.__externalRCRef())
}

まだpackageなどの冗長な記述はあるものの、大分人間が読める形になったのではないでしょうか。

モジュール毎にファイルが定義される

Objective-C Exportの場合、下記のように一つのヘッダーファイルに定義されていました。

__attribute__((objc_subclassing_restricted))
__attribute__((swift_name("Module_aClassFromA")))
@interface SharedModule_aClassFromA : SharedBase
- (instancetype)initWithName:(NSString *)name __attribute__((swift_name("init(name:)"))) __attribute__((objc_designated_initializer));
- (NSString *)hello __attribute__((swift_name("hello()")));
@end

__attribute__((objc_subclassing_restricted))
__attribute__((swift_name("IOSSourceKt")))
@interface SharedIOSSourceKt : SharedBase
+ (int32_t)iosModuleABar __attribute__((swift_name("iosModuleABar()")));
@end

....


__attribute__((objc_subclassing_restricted))
__attribute__((swift_name("Module_bClassFromB")))
@interface SharedModule_bClassFromB : SharedBase
- (instancetype)initWithName:(NSString *)name __attribute__((swift_name("init(name:)"))) __attribute__((objc_designated_initializer));
- (NSString *)hello __attribute__((swift_name("hello()")));
@end

__attribute__((objc_subclassing_restricted))
__attribute__((swift_name("IOSSourceKt")))
@interface SharedIOSSourceKt : SharedBase
+ (int32_t)iosModuleBBar __attribute__((swift_name("iosModuleBBar()")));
@end

Swift Exportを使うことで次のようにモジュール毎にファイルが定義されます。

モジュールA

import ExportedKotlinPackages
import KotlinRuntime

public typealias ClassFromA = ExportedKotlinPackages.com.github.jetbrains.modulea.ClassFromA

public func iosModuleABar() -> Int32

extension com.github.jetbrains.modulea {

    final public class ClassFromA : KotlinBase {

        public init(name: String)

        public func hello() -> String
    }

    public static func iosModuleABar() -> Int32
}

モジュールB

import ExportedKotlinPackages
import KotlinRuntime

public typealias ClassFromB = ExportedKotlinPackages.com.github.jetbrains.moduleb.ClassFromB

public func iosModuleBBar() -> Int32

extension com.github.jetbrains.moduleb {

    final public class ClassFromB : KotlinBase {

        public init(name: String)

        public func hello() -> String
    }

    public static func iosModuleBBar() -> Int32
}

トップレベル関数が使いやすくなる

今まで定義したトップレベル関数はFileNameKt.functionName()という形で使う必要がありました。
例えば下記のような記述の仕方です。

let moduleA = CommonKt.useClassFromA()
Text("Module A: \(moduleA.hello())")
let moduleB = CommonKt.useClassFromB()
Text("Module B: \(moduleB.hello())")

Swift Exportを使うこと下記のようにでKotlin, Swiftと同じようにfunctionName()という形で使うことができます。

let moduleA = useClassFromA()
Text("Module A: \(moduleA.hello())")
let moduleB = useClassFromB()
Text("Module B: \(moduleB.hello())")

Typealiasが使えるようになる

今まで Objective-C Exportでは Kotlinで定義したTypealiasをSwiftで使うことはできませんでした。
そのため、今回のサンプルのコードのMyClass.Nestedのインスタンスを生成する必要がある場合は愚直に

let nestedClass = MyClass.Nested(nestedProperty: 6)

と書く必要がありました。

Swift Exportを使った場合、下記のように記述することができます。

let nestedClass = MyNested(nestedProperty: 6)

Swift Exportはどこへ行くのか

前項でも触れてはいますが、Swift Exportのissueで言及されているように、Objective-C Export と同等の機能セットがFirst Releaseで提供される予定です。

時期は未定ですが2025年のRoadmap Itemとなっているので、順調にいけば2025年中にリリースされることが予想されます。

First Releaseで無事安定すればそこに加え、現時点ではスコープから外れているEnum, data class, sealed class/interface, Flowなどがサポートされていくはずです。

上記がサポートされていけばKMPにおけるiOS開発がさらに快適になることや、Objective-C Exportと比べて機能の追従もしやすくなることが期待されます。(Swift ExportではK2を利用しているため、パフォーマンスにもいい影響を与えるかもしれません)

終わりに

今回はSwift Exportについて調べました。

調べていく中でSwift Export以外にもKotlin/Nativeに関する情報も知ることができ、思わぬ収穫でした。

KMPは毎年進化していて、Androidアプリ開発に身を置くものとしては非常に楽しいですね(キャッチアップも大変ではありますが)

まだアルファより前の段階、かつ開発中止の可能性も示唆されていますが、個人的には非常に期待しているのでうまくいってほしいなと思っています。

今後のSwift Exportの進捗が気になる方は Youtrackで確認ができますので、興味があればチェックしてみてください。

最後まで読んでいただきありがとうございました!

参考資料

Roadmap

Kotlin

Kotlin/Native

Swift Export

機能の廃止に関して言及している箇所

What's new in Kotlin 2.1.0

[!CAUTION]

This feature is currently in the early stages of development. It may be dropped or changed at any time. Opt-in is required (see the details below), and you should use it only for evaluation purposes. We would appreciate your feedback on it in YouTrack.

Discussion