こんにちは。Androidエンジニアの豊川です。
この記事は、kubell Advent Calendar 2024(シリーズ 1)の13日目の記事です。
先日、Kotlin Multiplatform (KMP)のロードマップが更新されました。
魅力的な内容がたくさんありましたが、自分は特にSwift Exportがロードマップに加わることへ注目しています。
この機能は以前から要望が多く、気になっていた方も多いのではないでしょうか。
加えてKotlin 2.1.0にて早期アクセスが始まり、今後のアップデートがますます楽しみです。
一方で、Swift Exportは公開されて間もないため、情報が分散しており、調べる中で理解するのに手間がかかると感じました。
そこで本記事では、Swift Exportについて調査し、どこから来たのか(なぜ作られたか)、何者か(何ができるか)、どこへ行くのか(どうなるのか)、の観点でまとめていきたいと思います。
この先に記載する内容には初期段階の情報が含まれています。
今後変更されたり、そもそも廃止される可能性がありますので予めご承知おきください。
また、筆者もKMPの知識が万全ではないため、誤った情報が含まれている可能性があります。その際はご指摘いただけると幸いです。
要約
- 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から読み取れます。
反面、順調に進んでいけばこれらの機能もサポートされていくとも捉えられるので、期待してもよいでしょう。
上記の機能(Sealed interface, Flowなど)はKMPでもうまくサポートされておらず、SKIEなどサードパーティのライブラリを使うことで対応している場合があります。
Swift Exportが上記を対応すれば、これらのライブラリを使わずに済む可能性があります。
Swift Exportを使うことでコードはどう変わるか
目を引く変化で言うと、下記のように変わります。
- ヘッダーファイル(.h)からSwiftファイル(.swift)になる
- モジュール毎にファイルが定義される
- トップレベル関数が使いやすくなる
-
typealias
が使えるようになる
次の項目から実際のコードを交え、Swift Exportを使うことでどのようにコードが変わるのかを見ていきましょう。
参考コード
下記の公式のサンプルコードを元に説明していきます。
ヘッダーファイル(.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
- Get started with Kotlin/Native in IntelliJ IDEA
- Kotlin Overview - Kotlin Native
- Kotlin/Native README.md
Swift Export
- Kotlin Multiplatform Development Roadmap for 2024#Multiplatform core
- Kotlin Multiplatform Development Roadmap for 2025#Kotlin-to-Swift export
- kotlin/docs
機能の廃止に関して言及している箇所
[!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.