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ファイルにします。
- コンパイルする
kotlinc hello.kt -include-runtime -d hello.jar
- できたhello.jarからclassファイルを取り出す
jar -xvf hello.jar
- 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
の結果っぽいですね。
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