はじめに
この記事はKotlinアドベントカレンダー24日目の記事です。 Advent Calendarに空きがあったので投稿することにしました。 この記事ではKotlin/Nativeの標準ライブラリの実装について解説します。
検証環境
- Kotlin Native v1.0.3
- macOS Mojave 10.14.2
Kotlin/Nativeとは
Kotlin/NativeはKotlinのコードをネイティブバイナリにコンパイルしてくれるものです。 各プラットフォーム・アーキテクチャ向けのネイティブバイナリが生成されるので、Jvm KotlinのようなVM環境なども必要なく、ネイティブ環境のままで動作します。
現在は以下のプラットフォームがサポートされています。
- iOS (arm32, arm64, emulator x86_64)
- macOS (x86_64)
- Android (arm32, arm64)
- Windows (mingw x86_64)
- Linux (x86_64, arm32, MIPS, MIPS little endian)
- WebAssembly (wasm32)
Kotlin/Nativeの標準ライブラリの実装について
Kotlin/Nativeの標準ライブラリの実装の解説の前にKotlin/Nativeのリポジトリの構造について解説したいと思います。 Kotlin/Nativeのリポジトリの構造は以下のようになっていて、直下のディレクトリgradleモジュールになっています。 ソースコードを軽く読んでみた感じそれぞれのモジュールは以下のような役割をもっているです。
.
├── Interop c言語との相互運用のためのモジュール
├── backend.native コンパイラ本体
├── buildSrc
├── cmd
├── common
├── dependencies
├── extracted
├── gradle
├── klib
├── konan
├── libclangext
├── licenses
├── llvmDebugInfoC
├── performance
├── platformLibs 各プラットフォーム向けの実装
├── runtime 標準ライブラリとプログラムランチャーの実装
├── samples
├── shared
├── tools
今回はKotlin/Nativeの標準ライブラリの実装の解説ですので、runtimeモジュールを見ていきます。
Kotlin/Nativeの標準ライブラリの実装はruntimeモジュールのsrc/main
以下にあります。
src/main
のディレクトリ構造はこのようになっています。
.
├── cpp
├── js
├── math.js kotlin.mathをjavascriptのMath関数で置き換えてる(今回は取り扱わない)
└── kotlin
ディレクトリ構造からお察しだと思いますが、なんとKotlin/Nativeの標準ライブラリではC++も使われています。
調べてみたところKotlin/Nativeの標準ライブラリの実装方法には2種類の方法があるようです。
- C++とKotlinがミックスされて実装されているもの
- Kotlinのみで実装されているもの
Kotlinのみで実装されているものに関しては、普通のKotlinプログラムですので特に解説はしません。 例としてranges, coroutinesがあります。
C++とKotlinがミックスされて実装されているものについて解説したいと思います。 C++とKotlinがミックスされて実装されているものの例としてStringクラスがあります。 標準ライブラリのStringの実装はこのようになっています。
package kotlin
import kotlin.native.internal.ExportTypeInfo
import kotlin.native.internal.Frozen
@ExportTypeInfo("theStringTypeInfo") //Objective-Cとの相互運用で必要ぽい
@Frozen //thread/worker
public final class String : Comparable<String>, CharSequence {
public companion object {
}
@SymbolName("Kotlin_String_hashCode")
external public override fun hashCode(): Int
public operator fun plus(other: Any?): String {
return plusImpl(other.toString())
}
override public fun toString(): String {
return this
}
public override val length: Int
get() = getStringLength()
@SymbolName("Kotlin_String_get")
external override public fun get(index: Int): Char
@SymbolName("Kotlin_String_subSequence")
external override public fun subSequence(startIndex: Int, endIndex: Int): CharSequence
@SymbolName("Kotlin_String_compareTo")
override external public fun compareTo(other: String): Int
@SymbolName("Kotlin_String_getStringLength")
external private fun getStringLength(): Int
@SymbolName("Kotlin_String_plusImpl")
external private fun plusImpl(other: Any): String
@SymbolName("Kotlin_String_equals")
external public override fun equals(other: Any?): Boolean
}
いろいろツッコミどころがあると思いますが一旦無視して、Stringクラスのgetメソッドに注目します。
@SymbolName("Kotlin_String_get")
external override public fun get(index: Int): Char
関数の頭にexternal
がついてますね、これはメソッドの定義が外部ファイルにあることをコンパイラに伝えるためのです。
おそらくC言語extern
と同様のものだと思われます。
では実際の関数の定義はどこにあるのでしょうか?@SymbolName
という謎のアノテーションが関数についてますね。
実はこれ、C++の関数名と対応していて、getメソッドが呼ばれるとC++の関数Kotlin_String_get
が呼ばれるようになっています。
ちなみに、Kotlin_String_get
のC++での実装はこうなっています。
KChar Kotlin_String_get(KString thiz, KInt index) {
if (static_cast<uint32_t>(index) >= thiz->count_) {
ThrowArrayIndexOutOfBoundsException();
}
return *CharArrayAddressOfElementAt(thiz, index);
}
このようにC++とKotlinがミックスされて実装されているものは、Kotlin側では関数の定義のみを行い、関数の実装はC++側で実装するという形になってます。
このとき@SymbolName
アノテーションの引数で指定した関数を呼び出すようなコードを生成するのは、コンパイラが行っています。
検証してみる
本当にKotlin側でStringクラスのgetメソッドがよばれたらC++の関数が呼ばれるのか検証したいと思います。 ものすごく簡単なコードを用意しました。
fun main(args: Array<String>) {
val s = "hoge"
println(s.get(0))
}
コンパイルして動かしてみます。
$ kotlinc-native main.kt
$ ./program.kexe
h
が出力されるはずです。
さて本当にKotlin_String_get
が呼ばれてるのか,アセンブリを見てみましょう。
objdump -S program.kexe > dump.asm
objdumpでアセンブリを出力します。めちゃくちゃ長いアセンブリが出力されます(17万行)。 9万行目くらいにmain関数のアセンブリをみつけました。
_kfun:main(kotlin.Array<kotlin.String>):
1000080a0: 55 pushq %rbp
1000080a1: 48 89 e5 movq %rsp, %rbp
1000080a4: 53 pushq %rbx
1000080a5: 48 83 ec 18 subq $24, %rsp
1000080a9: 48 89 fb movq %rdi, %rbx
1000080ac: 0f 57 c0 xorps %xmm0, %xmm0
1000080af: 0f 29 45 e0 movaps %xmm0, -32(%rbp)
1000080b3: 48 c7 45 f0 00 00 00 00 movq $0, -16(%rbp)
1000080bb: 48 8d 7d e0 leaq -32(%rbp), %rdi
1000080bf: be 01 00 00 00 movl $1, %esi
1000080c4: ba 03 00 00 00 movl $3, %edx
1000080c9: e8 52 3a 04 00 callq 277074 <_EnterFrame>
1000080ce: 48 89 5d e8 movq %rbx, -24(%rbp)
1000080d2: 48 8d 3d 97 60 07 00 leaq 483479(%rip), %rdi
1000080d9: 31 f6 xorl %esi, %esi
1000080db: e8 10 09 04 00 callq 264464 <_Kotlin_String_get>
1000080e0: 48 8d 75 f0 leaq -16(%rbp), %rsi
1000080e4: 89 c7 movl %eax, %edi
1000080e6: e8 85 90 01 00 callq 102533 <_kfun:kotlin.Char.<box>(kotlin.Char)kotlin.Any>
1000080eb: 48 89 c7 movq %rax, %rdi
1000080ee: e8 ed 84 02 00 callq 165101 <_kfun:kotlin.io.println(kotlin.Any?)>
1000080f3: 48 8d 7d e0 leaq -32(%rbp), %rdi
1000080f7: be 01 00 00 00 movl $1, %esi
1000080fc: ba 03 00 00 00 movl $3, %edx
100008101: e8 2a 3a 04 00 callq 277034 <_LeaveFrame>
100008106: 48 83 c4 18 addq $24, %rsp
10000810a: 5b popq %rbx
10000810b: 5d popq %rbp
10000810c: c3 retq
10000810d: 48 89 c3 movq %rax, %rbx
100008110: 48 8d 7d e0 leaq -32(%rbp), %rdi
100008114: be 01 00 00 00 movl $1, %esi
100008119: ba 03 00 00 00 movl $3, %edx
10000811e: e8 0d 3a 04 00 callq 277005 <_LeaveFrame>
100008123: 48 89 df movq %rbx, %rdi
100008126: e8 ed 7b 04 00 callq 293869
10000812b: 0f 1f 44 00 00 nopl (%rax,%rax)
main関数のアセンブリをじっくりみていくとありました。ちゃんKotlin_String_get
が呼ばれてますね。
1000080db: e8 10 09 04 00 callq 264464 <_Kotlin_String_get>
終わりに
今度気が向いたらKotlinのコードがKotlin/Nativeのコンパイラ上でどのようにネイティブバイナリに変換されるかについて詳しく書こうと思います。 需要ありますかね?