Swift SDKsを使ってAndroidでSwiftコードを動かす
Swift SDKsとは?
Swift 6.0から、swift sdk
コマンドが追加されていることはご存知でしょうか?
Swift SDKsはSE-0387で提唱されたクロスコンパイルの仕組みで、異なるランタイム向けのビルドを簡単に行うことができます。
公式サイトではmacOSからLinux用の実行ファイルをビルドするための仕組みが紹介されています。
本記事では、この仕組みを使ってAndroidでSwiftコードを動かす最小の例を紹介します。
事前準備
現在、Android用のSwift SDKは公式からは提供されていないため、以下のリポジトリで配布されているものを使用します。
(本記事が説明する内容もほとんどREADME.mdと同じです。)
Swift SDKsを使う際、Swift 6.0においては、SDKをビルドしたSwiftのバージョンと使用するSwiftのバージョンを揃える必要があります[1]。
そのため、Xcodeに内蔵されているSwiftは基本バージョンが揃わない[2]ので利用できず、手動でSwiftをインストールする必要があります。
本記事で利用するSwiftツールチェーンは6.0.2です。以下のSwiftとSwift SDKを使用します。
- https://download.swift.org/swift-6.0.2-release/xcode/swift-6.0.2-RELEASE/swift-6.0.2-RELEASE-osx.pkg
- https://github.com/finagolfin/swift-android-sdk/releases/download/6.0.2/swift-6.0.2-RELEASE-android-24-0.1.artifactbundle.tar.gz
pkgを使ってSwiftツールチェーンをインストールしたあと、以下のコマンドでAndroid用SDKをインストールします。
swift sdk install \
https://github.com/finagolfin/swift-android-sdk/releases/download/6.0.2/swift-6.0.2-RELEASE-android-24-0.1.artifactbundle.tar.gz \
--checksum d75615eac3e614131133c7cc2076b0b8fb4327d89dce802c25cd53e75e1881f4
Androidプロジェクトの作成
テンプレートはEmpty Activityにして、Androidプロジェクトを作成します。
JNI用クラスの用意
今回はJNIを使用してSwiftコードを呼び出します。
Kotlin側でcom.example
パッケージを用意して、JNI呼び出し用のクラスを宣言します。
package com.example
class MySwiftLib {
external fun swiftHello(name: String): String
companion object {
init {
System.loadLibrary("AppModule")
}
}
}
今回は、MySwiftLib
クラスのメンバ関数としてswiftHello
を用意し、その実装をJNIを経由してSwiftで行います。
生成するSwiftライブラリの名前はlibAppModule.so
という想定にします。
JNI用クラスを利用するコードを書いておく
現代のテンプレートではMainActivity
クラスにJetpack ComposeによるUI実装があると思います。
適当な箇所をMySwiftLib
を使うコードに変えておきます。
@Composable
fun Greeting(name: String, modifier: Modifier = Modifier) {
val lib = MySwiftLib()
Text(
text = lib.swiftHello(name),
modifier = modifier
)
}
Swiftパッケージのセットアップ
適当なディレクトリでswift package init --type library
します。
今回は成果物のバイナリを手でAndroidプロジェクトに組み込むため、場所はどこでも良いです。
パッケージ定義
Package.swiftを編集して、以下のようにします。
// swift-tools-version: 6.0
import PackageDescription
let package = Package(
name: "AppModule",
products: [
.library(
name: "AppModule",
type: .dynamic,
targets: ["AppModule"]
),
],
targets: [
.target(
name: "AppModule"
),
]
)
重要な点はAppModule
というライブラリ名と、type: .dynamic
の指定です。
先程Kotlinコードで指定した名前と同じにして、かつダイナミックライブラリとしてビルドすることでJNIから呼び出せるようにします。
SwiftでJNI関数を実装する
実行したいSwiftコードを記述します。
今回は文字列を受け取って文字列を返す関数です。以下の形で実装します。
import jni
@_cdecl("Java_com_example_MySwiftLib_swiftHello")
public func swiftHello(env: UnsafeMutablePointer<JNIEnv?>?, obj: jobject, name: jstring) -> jstring {
let nameCStr = env!.pointee!.pointee.GetStringUTFChars(env, name, nil)!
let name = String(cString: nameCStr)
let result = "Hello, \(name)! from Swift"
return result.withCString { resultCStr in
env!.pointee!.pointee.NewStringUTF(env, resultCStr)!
}
}
import jni
は、Android用SDKに含まれているパッケージです。Android NDKにおけるjni.hそのものであり、C及びC++用のヘッダーファイルです。
SwiftにはC++ interoperabilityがありますが、jni.hをC++として読み込むとビルドできませんでした。Swiftが非対応の依存がありうまく利用できなかったため、今回はCとして使います。
引数のobj: jobject
はこの関数を呼び出しているレシーバオブジェクト、name: jstring
はJava文字列オブジェクトを表しています。
GetStringUTFChars
あたりでJavaの文字列からC言語用の文字列ポインタを取り出してSwift文字列に変換しています。
その後、加工したSwift文字列をC文字列として展開したあとNewStringUTF
でJavaの文字列に変換しています。
@_cdecl
では、この関数をC言語の呼び出し規約に従って出力するよう指示しています。
引数はその出力する際の名前で、JNIはこの名前を使ってネイティブ実装から目的の関数を探します。
このへんのお作法はCでJNIを使う際と全く同じであるため、詳細は割愛します。
Swiftライブラリをビルド
以下のコマンドでSwiftパッケージをビルドします
swift build \
--swift-sdk aarch64-unknown-linux-android24 \
--toolchain /Library/Developer/Toolchains/swift-6.0.2-RELEASE.xctoolchain
ポイントは、Swift SDKの指定とツールチェーンの指定です。
今回インストールしたAndroid用SDKの名前がaarch64-unknown-linux-android24
なので、それを使う指示をします。
事前準備の章で述べたように、SDKとツールチェーンのバージョンを合わせる必要があります。準備したツールチェーンのパスを指定します。
ビルドに成功すると、.build/debug/libAppModule.so
ができてると思います。
ライブラリをAndroidプロジェクトに配置
Androidプロジェクト構成のことはよくわかっていないのですが、JNI用の動的ライブラリは
app/src/main/jniLibs/<ABI>
ディレクトリの中に配置すると勝手にアプリケーションバンドルに転送してくれるみたいです。<ABI>
にはarm64-v8a
などの文字列が入ります。
先程ビルドしたSwiftライブラリをそこに配置します。
cp <Swiftパッケージルート>/.build/debug/libAppModule.so \
<Androidプロジェクトルート>/app/src/main/jniLibs/arm64-v8a/
またSwiftライブラリはSwiftの標準ライブラリに依存しているため、標準ライブラリの.soも配置する必要があります。
Android用のSwift標準ライブラリはSwift SDKに含まれているようでした。
cp ~/.swiftpm/swift-sdks/swift-6.0.2-RELEASE-android-24-0.1.artifactbundle/swift-6.0.2-release-android-24-sdk/android-27c-sysroot/usr/lib/aarch64-linux-android/24/*.so \
<Androidプロジェクトルート>/app/src/main/jniLibs/arm64-v8a/
過剰なものもありますが丸ごとコピーして配置します。
配置した結果、次のようなディレクトリツリーになると思います。
実行
ここまでで実際にAndroidアプリを動かしてみると、クラッシュしてエラーがでてしまいました。
java.lang.UnsatisfiedLinkError: dlopen failed: empty/missing DT_HASH/DT_GNU_HASH in "/data/app/~~ObOIVvQB0bspEsAqPa64dg==/com.gmail.side.junktown.swiftonandroid-hg6vqhNArV35Tf7XbvP-cA==/base.apk!/lib/arm64-v8a/libdispatch.so" (new hash type from the future?)
これについてはよくわかっていないのですが、Androidツールチェーンがなにか悪さしていることが原因のようです。
build.gradle.kts
のandroid
ブロックに以下の記述を追加すると対応できます。
android {
...
packaging {
jniLibs.keepDebugSymbols.add("*/arm64-v8a/libdispatch.so")
}
}
無事、動作が確認できました。
おわりに
Swift SDKsを使うことで、簡単にAndroid用のバイナリを出力することができました。
とはいえ、JNIをSwiftから使うのは大変面倒ですし、gradleとのインテグレーションもまだ適当です。
今回は、基礎を学ぶためにswift-javaを使わずあえて素のままのSwiftを使いました。
swift-javaに含まれるJavaKitを使うと、マクロによってJNIが簡単に利用できそうな雰囲気があります。ドキュメントが少なく使い方がよくわかっていないので、いつか調べてみたいと思っています。
Discussion