Kotlinの概要とモバイルアプリ開発

  • 104
    いいね
  • 0
    コメント
この記事は最終更新日から1年以上が経過しています。

社内のKotlin勉強会で、Kotlinをプロダクションに導入できるか検討するために調べて話したので、そのスライドの内容を当たり障りのない文章に起こしました。私はモバイルアプリの開発者なので、モバイルアプリ開発者の視点に寄っていると思います。

Runs on JVMとは

Kotlin is a statically-typed programming language that runs on the Java Virtual Machine and also can be compiled to JavaScript source code.

Kotlin (programming language) - Wikipedia, the free encyclopedia

まずはじめに "Runs on JVM" とはどういうことかを考えました。JVMは決められたフォーマットのバイトコードを実行します。たとえば "Hello World" と出力するクラスファイルは以下のようになります。

00000000  ca fe ba be 00 00 00 34  00 1d 0a 00 06 00 0f 09  |.......4........|
00000010  00 10 00 11 08 00 12 0a  00 13 00 14 07 00 15 07  |................|
00000020  00 16 01 00 06 3c 69 6e  69 74 3e 01 00 03 28 29  |.....<init>...()|
00000030  56 01 00 04 43 6f 64 65  01 00 0f 4c 69 6e 65 4e  |V...Code...LineN|
00000040  75 6d 62 65 72 54 61 62  6c 65 01 00 04 6d 61 69  |umberTable...mai|
00000050  6e 01 00 16 28 5b 4c 6a  61 76 61 2f 6c 61 6e 67  |n...([Ljava/lang|
00000060  2f 53 74 72 69 6e 67 3b  29 56 01 00 0a 53 6f 75  |/String;)V...Sou|
00000070  72 63 65 46 69 6c 65 01  00 0a 48 65 6c 6c 6f 2e  |rceFile...Hello.|
00000080  6a 61 76 61 0c 00 07 00  08 07 00 17 0c 00 18 00  |java............|
00000090  19 01 00 0b 48 65 6c 6c  6f 20 57 6f 72 6c 64 07  |....Hello World.|
000000a0  00 1a 0c 00 1b 00 1c 01  00 05 48 65 6c 6c 6f 01  |..........Hello.|
000000b0  00 10 6a 61 76 61 2f 6c  61 6e 67 2f 4f 62 6a 65  |..java/lang/Obje|
000000c0  63 74 01 00 10 6a 61 76  61 2f 6c 61 6e 67 2f 53  |ct...java/lang/S|
000000d0  79 73 74 65 6d 01 00 03  6f 75 74 01 00 15 4c 6a  |ystem...out...Lj|
000000e0  61 76 61 2f 69 6f 2f 50  72 69 6e 74 53 74 72 65  |ava/io/PrintStre|
000000f0  61 6d 3b 01 00 13 6a 61  76 61 2f 69 6f 2f 50 72  |am;...java/io/Pr|
00000100  69 6e 74 53 74 72 65 61  6d 01 00 07 70 72 69 6e  |intStream...prin|
00000110  74 6c 6e 01 00 15 28 4c  6a 61 76 61 2f 6c 61 6e  |tln...(Ljava/lan|
00000120  67 2f 53 74 72 69 6e 67  3b 29 56 00 21 00 05 00  |g/String;)V.!...|
00000130  06 00 00 00 00 00 02 00  01 00 07 00 08 00 01 00  |................|
00000140  09 00 00 00 1d 00 01 00  01 00 00 00 05 2a b7 00  |.............*..|
00000150  01 b1 00 00 00 01 00 0a  00 00 00 06 00 01 00 00  |................|
00000160  00 01 00 09 00 0b 00 0c  00 01 00 09 00 00 00 25  |...............%|
00000170  00 02 00 01 00 00 00 09  b2 00 02 12 03 b6 00 04  |................|
00000180  b1 00 00 00 01 00 0a 00  00 00 0a 00 02 00 00 00  |................|
00000190  03 00 08 00 04 00 01 00  0d 00 00 00 02 00 0e     |...............|
0000019f

つまり "Runs on JVM" と謳っているプロダクトはJVMのclass verifierをパスするバイトコードを吐くということに等しいということです。なので、アセンブラも "Runs on JVM" ですし

.class public Main
.super java/lang/Object

.method public static main([Ljava/lang/String;)V
   .limit stack 2
   getstatic java/lang/System/out Ljava/io/PrintStream;
   ldc "Hello World"
   invokevirtual java/io/PrintStream/println(Ljava/lang/String;)V
   return
.end method

もちろんJavaも "Runs on JVM" ですし

public class Main {
    public static void main(String[] args) {
        System.out.println("Hello World");
    }
}

Kotlinも "Runs on JVM" であり

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

JVM上で走る何か違いは、その抽象度とパラダイムだと思います。

Screen Shot 2016-03-23 at 23.06.14.png

これだけ見ると最終的にvalidなバイトコードを吐けば何でもいいのではとなってしまうので、もう少し詳しく見てみます。

Kotlinリポジトリについて

KotlinはGitHub上で公開されており(JetBrains/kotlin)、今すぐダウンロードしてコンパイルすることができます。KotlinのissueはGitHubではなく、JetBrainsのBug & Issue TrackerであるYouTrackが使われています(Kotlin (KT) | YouTrack)。
ビルドツールにはantとmavenが使われていますが、とりあえずantがあればいいと思います。

Kotlinのリポジトリを開くとたくさんディレクトリがありますが、とりあえずよく見るのは

あたりになるかなと思います。

stdlibとは

Kotlinはランタイムが小さいとよく言われますが、具体的にどういうことなのか手を動かして確認してみましょう。まず "Hello World" を出力する Hello.kt を書きます。

// Hello.kt
fun main(args: Array<String>) {
    println("Hello World")
}

これを javac Hello.java でクラスファイルを生成して java Hello で実行するようにkotlincコマンドを使います。生成されたクラスファイルが HelloKt.class なのは、Kotlinではクラスを定義することなく直接メソッドの定義ができるのですが、JVMの仕様上クラスは必要なのでkotlincがコンパイル時に外側のクラスを生成しているためです。

$ kotlinc Hello.kt
$ kotlin HelloKt
Hello World

"Hello World" が出力されましたね。では好奇心からjavaコマンドでKotlinのクラスファイルを実行してみます。

$ java HelloKt
Exception in thread "main" java.lang.NoClassDefFoundError: kotlin/jvm/internal/Intrinsics
    at HelloKt.main(Hello.kt)
Caused by: java.lang.ClassNotFoundException: kotlin.jvm.internal.Intrinsics
    at java.net.URLClassLoader$1.run(URLClassLoader.java:372)
    at java.net.URLClassLoader$1.run(URLClassLoader.java:361)
    at java.security.AccessController.doPrivileged(Native Method)
    at java.net.URLClassLoader.findClass(URLClassLoader.java:360)
    at java.lang.ClassLoader.loadClass(ClassLoader.java:424)
    at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:308)
    at java.lang.ClassLoader.loadClass(ClassLoader.java:357)
    ... 1 more

NoClassDefFoundErrorになりました。何が起こったのか知るために逆アセンブルしてみます。

$ javap -c HelloKt.class
Compiled from "Hello.kt"
public final class HelloKt {
  public static final void main(java.lang.String[]);
    Code:
       0: aload_0
       1: ldc           #9     // String args
       3: invokestatic  #15    // Method kotlin/jvm/internal/Intrinsics.checkParameterIsNotNull:(Ljava/lang/Object;Ljava/lang/String;)V
       6: ldc           #17    // String Hello World
       8: invokestatic  #23    // Method kotlin/io/ConsoleKt.println:(Ljava/lang/Object;)V
      11: return
}

続いてJavaで書かれたコードのクラスファイルを逆アセンブルした結果です。デフォルトコンストラクタは自動生成されたものなので無視して大丈夫です。

$ javap -c Hello.class
Compiled from "Hello.java"
public class Hello {
  public Hello();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return

  public static void main(java.lang.String[]);
    Code:
       0: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
       3: ldc           #3                  // String Hello World
       5: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
       8: return
}

両者を比べると、Kotlinのクラスファイルには 3: invokestatic #15 // Method kotlin/jvm/internal/Intrinsics.checkParameterIsNotNull:(Ljava/lang/Object;Ljava/lang/String;)V が追加されています。このクラスがstdlibの中に定義されていて、kotlincはそれがある前提でクラスファイルを吐きます。そのためstdlibがなければ実行時にエラーになります。
コンパイル時に -include-runtime オプションを渡すことでstdlibが入ったjarを生成することができ、それをjavaコマンドで実行することができるようになります。

$ kotlinc Hello.kt
$ kotlin HelloKt
Hello World

$ kotlinc Hello.kt -include-runtime -d Hello.jar
$ java -jar Hello.jar
Hello World

jarを解凍して中身を見てみると、たしかに自分が書いた HelloKt.class とstdlibが入っていることが分かります。

Scalaの標準ライブラリと単純にファイル数を比較すると、Kotlinが70ファイルでScalaが751ファイルで、Kotlinのランタイムが小さいということが分かります。

これは、Kotlinは便利なシンタックスと拡張関数(stdlibのほとんどは拡張関数)を提供していて、いわばBetter Javaというポジションなのに対して、Scalaが目指しているのはファンクショナルプログラミングの完全なサポートなので、そのパラダイムの実現のために多くのクラスが必要になっているのだと思います。

ランタイムが大きいのならばProguardで削ればいいと思われますが、すべては使わないにしろ必要だからstdlibに入っているのであって、もちろん削れることは削れると思いますが、よりScalaらしいコードを書くとサイズが大きくなるのは仕方ないと思います。

さて、そんなstdlibですが、変更するのは簡単なので、試しにstdlibの中の適当なファイルを開いて自分の好きな拡張関数を追加して、コンパイルして実行してみましょう。

// libraries/stdlib/src/kotlin/text/Strings.kt
public inline fun String.hello(): String = "Hello World"

追加したら ant runtime を実行するだけです。成果物は dist 以下に吐かれるので、早速実行してみましょう。

$ dist/kotlinc/bin/kotlinc
Type :help for help, :quit for quit
>>> "".hello()
Hello World

簡単ですね。

kotlincとは

他のコンパイラがそうであるように、kotlincもLexer analyzerがあって、Parserがあって、Code generatorがあり、それぞれ以下のように対応しています。

Screen Shot 2016-03-23 at 23.11.08.png

Lexcal analyzerは JFlex - The Fast Scanner Generator for Java というLexical analyzer generatorを使っていて Kotlin.flex という定義ファイルから _JetLexer.java というLexerを生成しています。

Screen Shot 2016-03-23 at 23.12.07.png

Parserにも何らかのParser generatorが使われたりしますが、kotlincではIntelliJ platform SDKというフレームワークを使っていて、トークンに対して先読みをしてマークするというのをマニュアルで書いてASTを構築しています。

Implementing Parser and PSI

ASTからOptimizerが不必要なBoxingやNull checkやDead codeを削除したのち、Code generatorがコード生成を行います。

Code generatorにはGroovyやClojureやJRubyやIntelliJやEclipseやLombokなど、あらゆるところで使われている ASM が使われています。イメージしやすくするために具体例を出すと、ASMで "Hello World" を出力するコードは以下のようになります。

MethodVisitor methodVisitor = classWriter.visitMethod(Opcodes.ACC_PUBLIC + Opcodes.ACC_STATIC,
        "main",
        "([Ljava/lang/String;)V",
        null,
        null);
methodVisitor.visitCode();
methodVisitor.visitFieldInsn(Opcodes.GETSTATIC,
        "java/lang/System",
        "out",
        "Ljava/io/PrintStream;");
methodVisitor.visitLdcInsn("Hello World");
methodVisitor.visitMethodInsn(Opcodes.INVOKEVIRTUAL,
        "java/io/PrintStream",
        "println",
        "(Ljava/lang/String;)V");
methodVisitor.visitInsn(Opcodes.RETURN);
methodVisitor.visitEnd();

ほぼアセンブラに近いですね。JVMの仕様を理解していないと書くことができません。kotlincではラップしたクラス群を作っていて、多少書きやすくなっています。

Kotlinとモバイルアプリ開発

モバイルアプリ開発にKotlinは使えるのか、という最初の目的に立ち返ると、現状多少バグっているとはいえ言語としても、言語開発環境自体もそんなに悪くない、しかしアプリのコードを書くには少しオートコンプリートが遅いとか、Lint系のツールが充実してないなど、日々良いところ悪いところの新しい発見があり、私の意見は数日おきに変わっています。

特にJack & Jillの出現で突然全滅したりしないかと心配になりましたが、Jillはクラスファイルを読み込んでJackで解釈できる中間コードを出力できるみたいなので(それができないと既存のライブラリが全滅しますもんね…)、aptのサポートは時間がかかるかもしれませんが、Kotlinがまったく動かなくなるということはないと思います。

何か問題があったとしても、もし心からやっていくぞという気持ちがあれば、Kotlinはオープンで誰でもコントリビュート可能なので、いくらでもコードを速くしたり周辺ツールを作っていけると思います。私が調べたのはここまでなので、ここからはみなさんの目で確かめてみてください。