8月24日(土)に開催された「Kotlin Fest 2019」において、サイバーエージェントはひよこスポンサーを務めました。

サイバーエージェントからは「タップル誕生」のAndroidエンジニア・佐藤(@stsn_jp )と「Amebaブログ」のサーバーサイドエンジニア・荻野(@youta1119 )の2名が登壇いたしました。

今日は、荻野(@youta1119 )が発表した「Kotlin / Nativeはなぜ動くのか?」のセッションについて、本人による解説をお届けします。

Kotlin/Nativeとは?

Kotlin/NativeとはLLVMを使ってKotlinのプログラムをネイティブバイナリにコンパイルする技術です。
以下のように多くのプラットフォームへのコンパイルをサポートしています。

  • iOS (arm32, arm64, simulator x86_64)
  • macOS (x86_64)
  • Android (arm32, arm64)
  • Windows (mingw x86_64, x86)
  • Linux (x86_64, arm32, MIPS, MIPS little endian, Raspberry Pi)
  • WebAssembly (wasm32)

Kotlinで動く仕組み

結論からいうとKotlinがネイティブで動くのはRuntimeというものがあるからです。
Kotlin/NativeにおけるRuntimeとはKotlinをネイティブで動かす為に必要なもののことで、具体的には以下のものをさします。

  • 標準ライブラリ
  • メモリ管理(GC)
  • プログラムランチャー

JVMでKotlinを動かしていた時にはJREが標準ライブラリの提供やメモリ管理をやってくれていました。
しかし、ネイティブでKotlinを動かす際にはJREなど存在しないため、標準ライブラリの提供やメモリ管理をやってくれるものが必要になります。それがRuntimeです。Runtimeが存在するおかげでKotlinはネイティブ上で動くことができます。

それではRuntimeについて解説します。

標準ライブラリ

標準ライブラリとはプリミティブ型やコレクションといったKotlinを使う上で必要な色々なものを提供してくれるライブラリです。
Kotlin/NativeではKotlin/Native用に標準ライブラリを一から実装しています。実装にはKotlinではなく、主にC++が使われています。
では標準ライブラリの実装について解説します。
標準ライブラリのコードを読んでみたところ、2パターンの実装方法があることがわかりました。

  • Kotlinのみで実装
  • Kotlin側では関数の定義のみを行い、実装はC++側で行う

1つ目のパターンはKotlinのみで標準ライブラリを実装する方法です。1つ目のパターンに関しては、普通のKotlinプログラムですのとくに解説はしません。

2つ目のパターンはKotlin側では関数の定義のみを行い、実装はC++側で行う方法です。print関数やString型のようにネイティブのIOやポインターを操作する必要のあるものは2つ目のパターンで実装されています。
2つ目のパターンについて実際のコードを出しながら詳しく解説します。
2つ目のパターンで実装されているものの例としてKotlinのPrint関数があります。

//https://github.com/JetBrains/kotlin-native/blob/v1.3.50/runtime/src/main/kotlin/kotlin/io/Console.kt#L10
package kotlin.io

/** Prints the given [message] to the standard output stream. */
@SymbolName("Kotlin_io_Console_print")
external public fun print(message: String)

Print関数のKotlin側の実装見てみるとPrint関数の定義のみがあることがわかります。
ではPrint関数のC++側での実装はどこにあるのでしょうか?
ここでPrint関数のKotlin付けられているexternalというあまり見かけない修飾子とSymbolNameという謎のアノテーションに注目します。
これがPrint関数のC++側での実装がどこにあるかを知る為の鍵になっています。

external修飾子はKotlin/Nativeのコンパイラに対して関数の実体は外部のどこかにあるよと教えてあげる為のものです。
SymbolNameアノテーションは関数の実体のシンボル名をコンパイラに教えてあげる為のものです。
SymbolNameアノテーションの値が関数の実体のシンボル名を指していて、このシンボル名はC++における関数名に対応しています。
つまりC++におけるKotlin_io_Console_printという関数がPrint関数のC++側の実装になります。
C++におけるPrint関数の実装はこんな感じになっています。

//https://github.com/JetBrains/kotlin-native/blob/v1.3.50/runtime/src/main/kotlin/kotlin/io/Console.kt#L10
void Kotlin_io_Console_print(KString message) {
  if (message->type_info() != theStringTypeInfo) {
    ThrowClassCastException(message->obj(), theStringTypeInfo);
  }
  // TODO: system stdout must be aware about UTF-8.
  const KChar* utf16 = CharArrayAddressOfElementAt(message, 0);
  KStdString utf8;
  // Replace incorrect sequences with a default codepoint (see utf8::with_replacement::default_replacement)
  utf8::with_replacement::utf16to8(utf16, utf16 + message->count_, back_inserter(utf8));
  konan::consoleWriteUtf8(utf8.c_str(), utf8.size());
}

2つ目のパターンでは、このようにKotlin側で定義したSymbolNameというアノテーションでC++の実装を指定することで実現されています。

メモリ管理

次にメモリ管理について解説します。
Kotlin/Nativeでのメモリ管理には基本的にGCが使われています。
メモリ管理には基本的にGCが使われています。
ただし、Cのライブラリを使う時はプログラマの責任でメモリの管理をします。そのため、メモリの解放を忘れるとメモリリークが発生します?
Kotlin/NativeではGCを自前で実装しています。これもC++で書かれています。コード上のコメントを読んだ感じBacon’s algorithmというGCアルゴリズムを使ってるようです。
GCの実装についての解説は、僕のC++力が足りずよくわからなかったので為割愛します。

プログラムランチャー

次にプログラムランチャーについて解説します。
プログラムランチャーとは名前の通り、Kotlinで書かれたプログラムを起動させるもののことです。プログラムランチャーではGCの初期化をしたのちKotlinで書かれたプログラムの起動を行う処理を行なっています。
Kotlinで書かれたプログラムの起動処理をいうと一見そんなに難しくなさそうに聞こえますが、ここに実行するプラットホームによってKotlinで書かれたプログラムの起動方法が異なるというマルチプラットフォーム特有のつらみがあります。

  • Mac・Linux・Windowsの場合: プログラム実行したときにKotlinで書かれたプログラムが起動
  • Androidの場合: プログラム実行したときにはKotlinで書かれたプログラムは起動せず、Kotlinで書かれたプログラムが起動するのはActivityが作られたとき
  • WebAssemblyの場合: プログラム実行したときに直ぐにはKotlinで書かれたプログラムが起動せず、Kotlinで書かれたプログラムが起動するのはブラウザ上でJavaScriptからWebAssemblyが呼ばれたとき

このようにプラットホームによって異なるKotlinプログラムの起動方法をいい感じは吸収するためにプログラムランチャーは存在します。
では実際にどういう感じでKotlinのプログラムが起動されるのかを知るためにMac, Linux, Windows向けのプログラムランチャーの実装を見ていきましょう!

//https://github.com/JetBrains/kotlin-native/blob/v1.3.50/runtime/src/launcher/cpp/launcher.cpp
extern "C" RUNTIME_USED int Init_and_run_start(int argc, const char** argv, int memoryDeInit) {
  Kotlin_initRuntimeIfNeeded(); // GCの初期化

  KInt exitStatus = Konan_run_start(argc, argv); // Kotlinで書かれたプログラムの起動

  if (memoryDeInit) Kotlin_deinitRuntimeIfNeeded(); // メモリ解放

  return exitStatus;
}

// プログラムランチャーのエントリーポイント(Main関数)
extern "C" RUNTIME_USED int Konan_main(int argc, const char** argv) { 
    return Init_and_run_start(argc, argv, 1);
}

Konan_mainという関数がMac, Linux, Windows向けのプログラムランチャーのエントリーポイントになっています。
Konan_main関数で行なっている処理はInit_and_run_start関数を呼び出しているだけす。
Init_and_run_start関数はプログラムランチャー本体を実装している関数で、各プラットホーム毎のプログラムランチャーによって呼び出されます。
今回解説しているMac, Linux, Windows向けのプログラムランチャーはMain関数から呼びだされますが、たとえばAndroidの場合、Activityが作られるタイミングで呼び出されます。

Init_and_run_start関数ではまずKotlin_initRuntimeIfNeededという関数を呼び出してGCの初期化を行います。
その後Konan_run_startという関数を呼び出します。この関数を呼び出すことによってKotlinで書かれたプログラムが呼び出されます。
その後Kotlinで書かれたプログラムの処理が終了したらKotlin_deinitRuntimeIfNeededという関数を呼び出しGCで確保されたメモリの解放を行います。
以上がプログラムランチャーの処理の流れになります。

Platform Library

RuntimeのおかげでKotlinがネイティブで動くようになりました。
しかし、Runtimeの提供する標準ライブラリには各プラットフォーム固有のAPIは含まれていないため、標準ライブラリだけだと実務的なプログラムは作れません。
そこでKotlin/Nativeでは「Platform Library」という各プラットフォーム固有のAPIにアクセスするためのライブラリを提供しています。
Platform Libraryでは以下のプラットホームをサポートしています

  • iOS
  • macOS
  • Android
  • Windows
  • Linux
    現状Platform LibraryではWebAssemblyはサポートしていません。

多くの場合プラットフォーム固有のAPIはC言語のヘッダファイルで提供されています。プラットフォーム固有のAPIをKotlinから使うためにはCとKotlinのブリッジしてあげる必要があります。
CとKotlinのブリッジをラットフォーム固有のすべてAPIに対して書くのはめちゃめちゃ大変なので、Platform LibraryではKotlin/Nativeが提供しているCInteropというツールを使ってCとKotlinのブリッジを自動生成しています。

C Interop

C InteropとはCとKotlinの相互運用を可能にするものです。Cのヘッダファイルを解析してCとKotlinのブリッジスタブを生成してくれます。
C Interopでは.defという拡張子のファイルからCとKotlinのブリッジスタブを生成します。
.defファイルには生成するスタブのパッケージ名、ブリッジスタブに含めるヘッダファイル、ブリッジスタブ生成に使うコンパイラオプションなどを記述します。
以下の例はmacOsのposix api用の.defファイルです。

# https://github.com/JetBrains/kotlin-native/blob/master/platformLibs/src/platform/osx/posix.def
depends =
# 生成するスタブのパッケージ名
package = platform.posix /

# ブリッジスタブに含めるヘッダファイル
headers = alloca.h ar.h assert.h complex.h dirent.h dlfcn.h err.h errno.h fcntl.h \
    fenv.h float.h fnmatch.h fts.h ftw.h getopt.h grp.h inttypes.h libgen.h limits.h \
    locale.h math.h memory.h netdb.h paths.h poll.h \
    pthread.h pwd.h regex.h resolv.h sched.h search.h semaphore.h setjmp.h sgtty.h signal.h \
    stdatomic.h stdint.h stdio.h stdlib.h string.h strings.h syslog.h termios.h \
    time.h ucontext.h unistd.h utime.h utmp.h wchar.h wctype.h xlocale.h \
    net/ethernet.h net/if.h net/if_arp.h net/route.h \
    netinet/icmp6.h netinet/if_ether.h netinet/in.h netinet/in_systm.h \
    netinet/ip.h netinet/ip6.h netinet/ip_icmp.h netinet/tcp.h netinet/udp.h \
    sys/acl.h sys/ioctl.h sys/ipc.h sys/mman.h sys/poll.h sys/ptrace.h \
    sys/queue.h sys/select.h sys/shm.h sys/socket.h sys/stat.h \
    sys/syslimits.h sys/time.h sys/times.h sys/utsname.h sys/wait.h

# ブリッジスタブ生成に使うコンパイラオプション
compilerOpts = -D_XOPEN_SOURCE -DSHARED_LIBBIND -D_DARWIN_NO_64_BIT_INODE
# -D_ANSI_SOURCE, sigh, breaks user_addr_t
excludedFunctions = KERNEL_AUDIT_TOKEN KERNEL_SECURITY_TOKEN add_profil             \
                    addrsel_policy_init                                             \
		    in6addr_linklocal_allv2routers                                  \
		    pfctlinput regwnexec_l res_send_setqhook res_send_setrhook      \
		    unwhiteout zopen profil openat acl_valid_link_np
linkerOpts = -ldl -lresolv

.defファイルを作成したらcinteropというコマンドを実行してCとKotlinのブリッジスタブを作成します。
cinteropコマンドを実行すると.klibというファイルが作られます。この.klibファイルの中にブリッジスタブが入っています。
klib`ファイルの中に入っているブリッジスタブはこんな感じになっています。

@CCall("knifunptr_platform_posix2")
external fun printf(@CCall.CString arg0: String?, vararg variadicArguments: Any?): Int

fun putc(arg0: Int, arg1: CValuesRef<FILE>?): Int {
    memScoped {
        return kniBridge24(arg0, arg1?.getPointer(memScope).rawValue)
    }
}
//(中略)

そしてコンパイル時にコンパイラオプション-libraryに生成した.klibファイルを指定してやるあげるとKotlinからCの関数や変数が使えるようになります。

まとめ

Kotlin/Nativeがどのように動くのか解説してみました。
この記事を読んでKotlin/Nativeに興味を持って頂ければ幸いです。

Androidエンジニアの佐藤による「Kotlin型実践入門」のセッション解説もぜひあわせてご覧ください。
来年もKotlin Festを楽しみましょう! Have a Nice Kotlin!

最後に

サイバーエージェントでは、学生インターンを受付しています。

ご興味お持ちの方はコチラをご覧ください。

月間数千億のリクエストを処理するアドテク のサーバーサイド