読者です 読者をやめる 読者になる 読者になる

アナログ金木犀

つれづれなるまままにつれづれする

Kotlinでjarを作って逆コンパイルしてみて、どんなjavaになるのか見てみた

こんにちは、@kgmyshinです。

この記事はKotlin Advent Calendar 2015の15日目の記事です。

早速。

KotlinにはExtensionという機能があります。

fun String.foo() { println("foooooooooooooooooooo") }

こう定義すると、本来Stringクラスにないfoo()という関数を追加することができます。

"kgmyshin ".foo()
// foooooooooooooooooooo

これは本来のJavaにはない機能ですが、jvmで動いている以上はjvmのオペコードに落ちているはずです。 落ちているはずなのですが、それがどのようになっているのか、Javaにはない機能をコンパイラがどう解釈しているのかということがが少し気になっていました。

そして、先日会社のブログで

[Java]〈Hello World〉をバイナリエディタだけで使って出力させてみた – NET BIZ DIV. TECH BLOG

を執筆して以降、コンパイラの理解への欲が高まり、その気持ちはさらに強くなりました。

ということで、早速 コンパイラのソースを見てみたけど時間かかりそうだったのでそれはやめてコンパイルしてできたjarファイルをjavaファイルに逆コンパイルしてみました。

手順

この手順でktファイルをjavaファイルにします。

  1. コンパイルする kotlinc hello.kt -include-runtime -d hello.jar
  2. できたhello.jarからclassファイルを取り出す jar -xvf hello.jar
  3. classファイルを逆コンパイルする。jad -s java -d src -r **/*.class

.kt -> .jar -> .class -> .java という流れ

オペコードが見れれば良いじゃん、javapコマンドの出力見るだけで良いじゃんって意見が聞こえてきそうで、またそれは確かに正しいのですが、こちらの方が多くの人が見やすいと思うのでこの手順を選びました。

あと、kotlinc-jvm hello.ktでそのままclassファイルでもよかったのですが、-include-runtimeの挙動もみたかったのでこの手順で実行しました。

検証

Hello, World!

まずは簡単なところから。

fun main(args: Array<String>) {
    println("Hello, World!")
}

これをjavaにすると該当部分はこうなります。

import kotlin.io.ConsoleKt;
import kotlin.jvm.internal.Intrinsics;

public final class HelloKt
{

    public static final void main(String args[])
    {
        Intrinsics.checkParameterIsNotNull(args, "args");
        ConsoleKt.println("Hello, World!");
    }
}

そして、このクラスだけではなくjar内にはkotlinのruntimeも含まれています。これが-include-runtimeの結果っぽいですね。

f:id:kgmyshin:20151123163323p:plain

javaで同じコードを書いてjarにすると760Bとなるのに対して、上記のKotlinで作ったjarが895Kなので基本的にKotlinを使うと + 900K弱くらい(runtime分)のサイズになるってのは把握しておいたほうがよさそうです。

Extension

適当にこういう感じのコードを書いてみます。


class C

fun C.foo() { println("Fooooooooooooooooooooooo") }

fun String.hoo() { println("Hooooooooooooooooooooo") }

fun main(args: Array<String>) {
    C().foo()
    "fuuuuu".hoo()
}

自分で作ったCというクラスにメソッドを追加した場合と、もともとあるStringクラスにメソッドを追加した場合をみたかったのでこういう形にしました。

早速javaファイルをみてみます。結果は、C.javaとHelloKt.javaが生成されました。

C.java

public final class C
{
    public C()
    {
    }
}

HelloKt.java

import kotlin.io.ConsoleKt;
import kotlin.jvm.internal.Intrinsics;

public final class HelloKt
{

    public static final void foo(C $receiver)
    {
        Intrinsics.checkParameterIsNotNull($receiver, "$receiver");
        ConsoleKt.println("Fooooooooooooooooooooooo");
    }

    public static final void hoo(String $receiver)
    {
        Intrinsics.checkParameterIsNotNull($receiver, "$receiver");
        ConsoleKt.println("Hooooooooooooooooooooo");
    }

    public static final void main(String args[])
    {
        Intrinsics.checkParameterIsNotNull(args, "args");
        foo(new C());
        hoo("fuuuuu");
    }
}

肝心の拡張されたメソッドはstatic関数になることが確認できました

    public static final void foo(C $receiver)
    {
        Intrinsics.checkParameterIsNotNull($receiver, "$receiver");
        ConsoleKt.println("Fooooooooooooooooooooooo");
    }

    public static final void hoo(String $receiver)
    {
        Intrinsics.checkParameterIsNotNull($receiver, "$receiver");
        ConsoleKt.println("Hooooooooooooooooooooo");
    }

実装場所についてはMainクラスにできているように見えるけど、どうなのか。

ということで試してみます。下記の二つのktファイルをjavaファイルにしてみます。

hello.kt

fun main(args: Array<String>) {
    "fuuuuu".poyo()
}

extension.kt

fun String.poyo() { println("poyo") }

結果はこうなりました。

HelloKt.java

import kotlin.jvm.internal.Intrinsics;

public final class HelloKt
{

    public static final void main(String args[])
    {
        Intrinsics.checkParameterIsNotNull(args, "args");
        ExtensionKt.poyo("fuuuuu");
    }
}

ExtensionKt.java

import kotlin.io.ConsoleKt;
import kotlin.jvm.internal.Intrinsics;

public final class ExtensionKt
{

    public static final void poyo(String $receiver)
    {
        Intrinsics.checkParameterIsNotNull($receiver, "$receiver");
        ConsoleKt.println("poyo");
    }
}

つまるところ、メソッドを拡張した場合は

  • メソッドを拡張した場所にある ファイル名 + "Kt.java" というクラスができる
  • そのクラスにstatic関数ができる

ということらしいです。

所感

やってみると単純な形なんだなぁという印象でした、変な話。

おまけ

コンパイルのコマンドあたりの処理が下記あたり。

https://github.com/JetBrains/kotlin/tree/master/compiler/cli/src/org/jetbrains/kotlin/cli/jvm

でコード生成らへんは下記あたりを見ればよさそう。

https://github.com/JetBrains/kotlin/tree/master/compiler/backend/src/org/jetbrains/kotlin/codegen