7
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

Kotlin Nativeってandroid向けにも書けるんですよ

Posted at

Kotlin Nativeってandroid向けにも書けるんですよ

この記事は2022年7月20日に開催されたQiita Night〜2022年、Androidアプリはどう作る?〜でLTした内容の記事版です。
Qiiita様にまとめていただいたイベントレポートはこちらです。
検証に使ったプログラムはGitHubにて公開しています。

Android NDK使ってますか?

  • androidエンジニアは普段kotlinもしくはJavaを用いてアプリを作成します。
  • Android NDKは処理を高速化したいときやメモリ管理を手動で行いたい場合に使用します。
  • linkerの設定やCmakeList.txtの準備などいくつか超えなければならないハードルがありますが、CもしくはC++で書く必要があるというのが一番高いハードルではないでしょうか?
  • 今回はトレンドであるマルチプラットフォーム技術を用いてハードルを乗り越えてみます。

Kotlin Native for Android NDKを使う

MPPとは

  • Kotlin Multiplatform Projectはkotlinで書いたコードを様々なプラットフォーム向けにビルドしてくれる仕組みです。
  • Kotlin NativeはこのProjectの一部で各種プラットフォーム向けの実行バイナリを生成できます。

Kotlin Native

  • 現在サポートはmacOS、iOS, tvOS, watchOS, Linux, MinGW, Android NDKに対応しています。
  • Kotlin Nativeは上記のプラットフォーム向けに静的もしくは動的ライブラリを生成できます。
  • Kotlin NativeはKotlin Native Runtime上で動きます。

イメージ図

※実際には異なりますがイメージとしてはこんな感じです

対応状況

  • IDE上で確認できる感じでは現状APIレベル21からサポートされているネイティブAPIに対応しています。
    *各SourceSet分のライブラリ定義が参照できる関係上、非常に多く羅列されているので画像は拡大してご確認ください。
    IDE上で確認できるNDK on K/N①
    IDE上で確認できるNDK on K/N②

  • Android NDKの呼び出しをkotlin nativeで呼び出せるようにAPIをラップしたライブラリが公開されています。そのためNative Activityなどをkotlinを用いて記述できます。

NDKのライブラリをimportしている図
*正しい呼び出し方ではないので図のコードのようにNativeAcitvityは呼べません

Kotlin Native for Android NDKを設定する

  • K/NはKotlin Multiplatformの一部です。
  • そのためKotlin Multiplatformとして設定を施す必要があります。
  • まずソールファイルを認識させるためにSourceSet設定します。
  • K/N for Android NDK用に拡張関数が用意されているので使用します。
    ※Kotlin Multiplatformのディレクトリによるソース周りの設定についてはこちら
    https://kotlinlang.org/docs/multiplatform-dsl-reference.html#targets
plugins {
    // Kotlin Multiplatform用のgradle pluginを適用
    kotlin("multiplatform")
}
kotlin {
    // androidNative用に用意されているプリセット用の関数を使って定義
    listOf(
        androidNativeX86(),
        androidNativeX64(),
        androidNativeArm32(),
        androidNativeArm64()
    ).forEach {
        // 吐き出されるライブラリが静的なのか動的なのかを定義する
        // 今回は動的ライブラリ
        it.binaries.sharedLib()
    }

    sourceSets {
        val androidNativeX86Main by getting
        val androidNativeX64Main by getting
        val androidNativeArm32Main by getting
        val androidNativeArm64Main by getting
        // 各アーキテクチャに同一コードを提供するためのsourceSet
        // hierarchy structure周りの設定で記述は変わります
        // https://kotlinlang.org/docs/multiplatform-hierarchy.html#for-multiplatform-project-authors
        val androidNativeMain by creating {
            androidNativeX86Main.dependsOn(this)
            androidNativeX64Main.dependsOn(this)
            androidNativeArm32Main.dependsOn(this)
            androidNativeArm64Main.dependsOn(this)
        }
    }
}


K/NのSourceSetは以下の通りにandroid ABIに対応しています
android abis

アーキテクチャ arm 32bit arm 64bit x86 x64
K/N SourceSet androidNativeArm32 androidNativeArm64 androidNativeX86 androidNativeX64
android abi armeabi-v7a arm64-v8a x86 x86_64

これでsrc/androidMain配下に記述したkotlinコードが静的もしくは動的ライブラリにビルドされます。
ただこのままではビルドするたびに.aファイルもしくは.soファイルを手動でコピペする必要があります。
煩わしいのでgradleでビルドするたびにコピーしてあげます。

val arm32SoFolder = File(buildDir, "bin/androidNativeArm32/releaseShared")
val jniArm32Folder = File(projectDir, "../androidApp/src/main/cpp/libs/armeabi-v7a")
val arm64SoFolder = File(buildDir, "bin/androidNativeArm64/releaseShared")
val jniArm64Folder = File(projectDir, "../androidApp/src/main/cpp/libs/arm64-v8a")

val x86SoFolder = File(buildDir, "bin/androidNativeX86/releaseShared")
val jniX86Folder = File(projectDir, "../androidApp/src/main/cpp/libs/x86")
val x64SoFolder = File(buildDir, "bin/androidNativeX64/releaseShared")
val jniX64Folder = File(projectDir, "../androidApp/src/main/cpp/libs/x86_64")

val targets = listOf(
    arm32SoFolder to jniArm32Folder,
    arm64SoFolder to jniArm64Folder,
    x86SoFolder to jniX86Folder,
    x64SoFolder to jniX64Folder,
)

val nativeFiles = listOf("*.so", "*.h")

tasks.build {
    // buildタスクが実行されるたびにファイルを削除する
    doFirst {
        targets.map { (_, into) -> fileTree(into.path) { include(nativeFiles) } }
                .flatten()
                .forEach { it.delete() }
    }
    // buildタスクの最後にheaderファイルなどをndkを使用するandroidプロジェクトのディレクトリにコピーする
    doLast {
        targets.forEach { (from, into) ->
            copy {
                from(from)
                into(into)
                include(nativeFiles)
            }
        }
    }
}

tasks.clean {
    // cleanタスクでも生成されたファイルが削除されるようにする
    doLast {
        targets.map { (_, into) -> fileTree(into.path) { include(nativeFiles) } }
                .flatten()
                .forEach { it.delete() }
    }
}

K/Nで作成したライブラリを呼び出してみる

JNIはJavaのprimitiveな型については特に変換などを意識することなく使用できます。K/N上のkotlinコードでも同様にPrimitiveな型については変換などを意識する必要がありません。

java jni C++ C++ on K/N kotlin on K/N
boolean jboolean uint8_t ${libray}_KBoolean Boolean
byte jbyte int8_t ${libray}_KByte Byte
char jchar uint16_t ${libray}_KChar Char
short jshort int16_t ${libray}_KShort Short
int jint int32_t ${libray}_KInt Int
long jlong int64_t ${libray}_KLong Long
float jfloat float ${libray}_KFloat Float
double jdouble double ${libray}_KDouble Double

Primitiveでない型やクラスはどうなるのか?ビルドで生成されたヘッダーファイルを見ると👇のようになっています。

typedef struct {
  /* User functions. */
  struct {
    struct {
      struct {
        struct {
          struct {
            struct {
              libshared_KLong (*fibonacci)(libshared_KLong n);
            } android;
          } kotlin;
        } ryunen344;
      } com;
    } root;
  } kotlin;
} libshared_ExportedSymbols;
extern libshared_ExportedSymbols* libshared_symbols(void);

libshared_ExportedSymbolsからたどることで自分が記述した関数を呼び出せます:eyes:
libshared_ExportedSymbolsからたどる必要があるため、通常のNativeAcitvityのような記述をAndroidManifestに行ってつかえなさそうです(未検証)
その点は考慮が必要そうです

extern "C" JNIEXPORT jlong JNICALL
Java_com_ryunen344_kotlin_android_android_NdkWrapper_fibonacciKNative(
        JNIEnv *env,
        jobject,
        jlong count) {
    // K/N symbolsを取り出す
    libshared_ExportedSymbols *symbols = libshared_symbols();

    // K/N側のパッケージをたどってお目当ての関数を呼び出す
    return symbols->kotlin.root.com.ryunen344.kotlin.android.fibonacci(count);
}

Kotlin Native for Android NDKのパフォーマンスはどうなのよ

ここまでKotlin NativeでAndroid NDK向けにビルドする話をしましたが、パフォーマンスがあまりに悪くては全く使い物になりません。そこで計測を行いました。

ルール

  1. Kotlin Nativeのnew mmは不使用
    1. 現在Kotlin NativeにはGCが実装されようとしていますが今回は不使用
  2. Kotlin Nativeで.soを吐き出してそれをndkに取り込んで処理を呼び出す
  3. 今回はみんな大好きフィボナッチ関数で計測
    1. 一番Order数の多い再帰パターンで計測
  4. Coroutineなどを用いて処理中の実行スレッドの変更などは行わない
  5. tailrecを使った言語レベルの最適化は行わない
    1. kotlinには言語レベルでの再帰最適化手法がありますが不使用

計測結果

項数\実装 ms kotlin C++ K/N
30 20.2 14 10.4
35 187.2 118.4 92.8
40 2530.8 1301.2 982.2
41 4241.6 2417 1724.2
42 6534.8 3737.4 2825.6
43 10424.2 6156.8 4571.8
44 17550.6 10078.8 7456.2
45 27410.2 16546.2 13112.2
46 44664 26853.4 20091.4
  • 素のネイティブ実装を上回れる結果が出ちゃいました
  • NDKの知識不足で最適化不足な部分がある....はず....

まとめ

素敵なところ

  • IDE支援をさらに受けやすい(CLion持ってない人でもリッチな支援がもらえる)
  • 強力なstdlib周りを設定なしで使える
  • コアロジックに関しては慣れてるKotlinを使って記述することができる

あと一歩のところ

  • Kotlin使ってるのにPointerを意識した実装が箇所箇所で発生する
  • coroutine, atomicfuが対応されてない対応される見込みもない(が回避策自体はある)

ネイティブライブラリしかないけどどうしてもC++のキャッチアップもする余裕がない、という場合にどうでしょうか?
構造体互換編, CInterop編はいずれどこかで・・・・

7
6
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
7
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?