August 30, 2021

Kotlin/Nativeで生成したLinux向けバイナリをDockerで実行する

Kotlin/Nativeは現在

  • Android NDK(androidNativeArm32, androidNativeArm64)
  • Darwin
    • macOS(macosX64, macosArm64)
    • iOS(iosArm32, iosArm64, iosX64, iosSimulatorArm64)
    • watchOS(watchosArm32, watchosArm64, watchosX86, watchosX64, watchosSimulatorArm64)
    • tvOS(tvosArm64, tvosX64, tvosSimulatorArm64)
  • Linux(linuxArm64, linuxArm32Hfp, linuxMips32, linuxMipsel32, linuxX64)
  • Windows(mingwX64, mingwX86)
  • wasm(将来deprecated)

のプラットフォームに対応しています。
1.5.30からApple siliconの正式サポートもされました🎉

Kotlin MultiPlatform向けに作ってるログライブラリのNapier もAndroid, JVM, JS, iOS, watchOS, tvOS, macOS[各Intel, Apple Silicon] をサポートしているのですが、普段はMacで開発しているので他のプラットフォームでデバッグするための環境を用意するのが面倒です。
MacなのでAndroid, JVM, JS, DarwinまではいけるのですがKotlin/NativeのLinux, Windows(今回は触れない)を実行できません。 そこでこの記事では、Kotlin/NativeでLinux用に生成したBinaryをDocker上で実行する方法について書きます。

動作環境

OSのディストリビューションはなんでも良いのですがUbuntuのイメージ を使います

ubuntu:latest のタグはデフォルトでは amd64(x86_64) だったのですが、念のためイメージは amd64/ubuntu:latest を指定しておきます。

アーティファクト生成

gradleファイルにlinuxX64を指定します

plugins {
    kotlin("multiplatform")
}

kotlin {

  linuxX64 {
    binaries {
      sharedLib {
        baseName = "native"
      }
    }
  }

  sourceSets {
    val commonMain by getting {
      dependencies {
      }
    }

    val nativeMain by creating {
      dependsOn(commonMain)
    }
    val linuxX64Main by getting {
      dependsOn(nativeMain)
    }
  }
}

binariesには 動的ライブラリ(*.so)を生成する sharedLib を指定しています。
他にも 静的ライブラリ(*.a)を生成する staticLib, 実行可能ファイルを生成する executable などがあります。[Kotlin/Native Declare binaries]

Kotlin MultiPlatformのGradleの書き方は色々コツがあります。 今回の場合は LinuxX64 のみ対応しますが、例えば Linux64 以外にも linuxArm64iosArm32 などの同じKotlin/Nativeのコードを一括で書く場合は、 nativeMainのように適当な名前をつけて dependsOn で依存をまとめると楽です。
creatingした nativeMain のMainより前がディレクトリ名になります。

例えばNapierのディレクトリ構成はこのようになっています。

.
├── build.gradle.kts
├── gradle
└── src
    ├── androidMain
    ├── commonMain
    │   └── kotlin
    │       └── io
    │           └── github
    │               └── aakira
    │                   └── napier
    │                       ├── Antilog.kt
    │                       ├── DebugAntilog.kt
    │                       ├── LogLevel.kt
    │                       ├── Napier.kt
    │                       └── atomic
    │                           ├── AtomicMutableList.kt
    │                           └── AtomicRef.kt
    ├── darwinMain(iOS, watchOS, tvOS, macOS)
    ├── jsMain
    ├── jvmMain
    └── nativeMain
        └── kotlin
            └── io
                └── github
                    └── aakira
                        └── napier
                            ├── DebugAntilog.kt
                            └── atomic
                                └── AtomicRef.kt

これで準備はできました。

./gradlew task を実行すれば linuxX64に関するタスクが生成されていると思います。

コード

Kotlin MultiPlatform

Kotlinで書かれた共通部分のコードはこのようになっています。
cf. Napierのmpp-sampleディレクトリ

class Sample {

    fun hello(): String {
        Napier.v("Hello napier")
        Napier.d("optional tag", tag = "your tag")

        return "Hello Napier"
    }

    suspend fun suspendHello(): String {
        Napier.i("Hello")

        delay(3000L)

        Napier.w("Napier!")

        return "Suspend Hello Napier"
    }

    fun handleError() {
        try {
            throw Exception("throw error")
        } catch (e: Exception) {
            Napier.e("Napier Error", e)
        }
    }
}

(Kotlin部分の詳しい説明はしません)
MultiPlatform部分の準備ができたら linkDebugSharedLinuxX64のタスクを実行します。

./gradlew linkDebugSharedLinuxX64

タスクが成功すると /build/bin/linuxX64/debugShared/ ディレクトリ配下に

  • libnative.so
  • libnative_api.h

が生成されています。

native部分はgradleのbaseNameに指定した名前になります。なにも指定しない場合はプロジェクト(ディレクトリ)名になります。

C言語

先程生成されたheaderファイルはこのようになっています。

...

typedef struct {
  ...

  /* User functions. */
  struct {
    struct {
      struct {
        struct {
          struct {
            struct {
              struct {
                void (*debugBuild)();
                void (*suspendHelloKt)(libnative_kref_io_github_aakira_napier_mppsample_Sample thiz);
                struct {
                  libnative_KType* (*_type)(void);
                  libnative_kref_io_github_aakira_napier_mppsample_Sample (*Sample)();
                  void (*handleError)(libnative_kref_io_github_aakira_napier_mppsample_Sample thiz);
                  const char* (*hello)(libnative_kref_io_github_aakira_napier_mppsample_Sample thiz);
                } Sample;
              } mppsample;
            } napier;
          } aakira;
        } github;
      } io;
    } root;
  } kotlin;
} libnative_ExportedSymbols;
extern libnative_ExportedSymbols* libnative_symbols(void);
#ifdef __cplusplus
}  /* extern "C" */
#endif
#endif  /* KONAN_LIBNATIVE_H */

生成されたライブラリをincludeしてコードを書きます。
パッケージ名を指定するので少し長くなってしまいますが他の言語と同様に、問題なく利用できます。

#include <stdio.h>
#include "libnative_api.h"

int main(int argc, char **argv) {
  // init kotlin native
  libnative_ExportedSymbols* lib = libnative_symbols();

  // init napier
  lib->kotlin.root.io.github.aakira.napier.mppsample.debugBuild();

  // run napier
  libnative_kref_io_github_aakira_napier_mppsample_Sample newInstance = lib->kotlin.root.io.github.aakira.napier.mppsample.Sample.Sample();
  lib->kotlin.root.io.github.aakira.napier.mppsample.Sample.hello(newInstance);

  // handleError
  lib->kotlin.root.io.github.aakira.napier.mppsample.Sample.handleError(newInstance);

  // suspend function
  lib->kotlin.root.io.github.aakira.napier.mppsample.suspendHelloKt(newInstance);

  // dispose
  lib->DisposeStablePointer(newInstance.pinned);

  return 0;
}

Dockerで動かす

実行用ShellScript

Docker内でコンパイルして実行するShellScriptを作っておきます。

#!/bin/bash

cd /work && \
  gcc napier.c -o napier -Wl,-rpath ./ -L. -lnative && \
  ./napier

Kotlin側で生成したビルドファイルと生成したShellScriptを任意のディレクトリにコピーしておきます。(ここでは/work)

cp -r /build/bin/linuxX64/debugShared/ /work

DockerFileでは gcc をインストールします。

FROM amd64/ubuntu:latest

RUN apt-get update && \
    apt-get -y install gcc && \
    mkdir work

COPY /work /work

CMD /work/build.sh

あとは実行するだけです。

docker build -t napier .
docker run napie
todo time 💜 VERBOSE todo - Hello napier
todo time 💚 DEBUG your tag - optional tag
todo time ❤️ ERROR todo - Napier Error
...

まとめ

Gradleの設定さえできれば、Dockerでも簡単にKotlin/Nativeのライブラリを読み込んで実行できました。
Flutterも便利ですが、UIより下のレイヤーではKotlin MultiPlatform Projectの方が扱いやすいかなと思います。

Help! (2021/08/30)

Napier というログのライブラリでLinuxを対応したいのですが、 Kotlin/Naitve(Linux, Windows)でbacktraceの取得方法がわかりませんでした。
linux branchを用意してあるので、もし知っていたら それを使って(参考にして)プルリクくれると嬉しいです!!!!!!!

© AAkira 2023