Kotlin/Nativeの標準ライブラリの実装を追いかける

はじめに

この記事は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のコンパイラ上でどのようにネイティブバイナリに変換されるかについて詳しく書こうと思います。 需要ありますかね?