ScalaとGraalVMでクリスマスっぽいAAをプリントするだけのCLIツールを作る

# 概要

ScalaとGraalVMを使ってクリスマスっぽいAAを出すだけの超シンプルなCLIツールを作ってみようというお話です。

CLIツールなどで起動時間が気になる場合はGraalVMでnative image作ればよさそうと考えて、[GRAALVM, PICOCLIとJAVAでときめくネイティブコマンドラインアプリを作ろう](https://remkop.github.io/presentations/20191123/index.html)を参考にScala+Picocli+GraalVMで何か作ってみようと思ったのですが、今日いざ書こうとしたら既に[先駆者](https://takezoe.hatenablog.com/entry/2020/03/15/122842)がいらっしゃいました。

もう既にピンズドな記事があるのでこの記事の意義は割と皆無ですが、他にネタもないのでこのままやっていこうと思います。

# 準備

## GraalVMのセットアップ

使用するGraalVMのバージョンは19.3.4です。
GraalVMの最新バージョンは20.3.0ですが、このバージョンではnative imageを作成できませんでした。[^1]
そこで、GraalVMのバージョンは去年リリースされたものを採用しています。[^2]

GraalVMは[こちら](https://www.graalvm.org/docs/getting-started/macos/)を参考に以下のようにセットアップしました。

```shell
$ curl -LO https://github.com/graalvm/graalvm-ce-builds/releases/download/vm-19.3.4/graalvm-ce-java11-darwin-amd64-19.3.4.tar.gz
$ tar -xvf graalvm-ce-java11-darwin-amd64-19.3.4.tar.gz
$ sudo mv graalvm-ce-java11-19.3.4/ /Library/Java/JavaVirtualMachines/
$ export PATH=/Library/Java/JavaVirtualMachines/graalvm-ce-java11-19.3.4/Contents/Home/bin:$PATH
```

またnative imageの作成にはnative-imageが必要なので、GraalVMのセットアップ後にインストールしてください。

```shell
$ gu install native-image
```

## プロジェクトのセットアップ

元となるプロジェクトはsbtを使用して作成しました。

```
$ sbt new scala/scala-seed.g8 --name=merry-christmas
```

Scalaのバージョンは2.12.7としています。
こちらも最新ではありませんが、GraalVMの最新バージョンを使用した場合と同様のエラーが出るため、2.12系を採用しています。

native imageの作成は[sbt-native-packager](https://github.com/sbt/sbt-native-packager)を使用して作成します。
以下のようにしてScalaプロジェクトにプラグインを追加してください。

```scala:project/plugin.sbt
addSbtPlugin("com.typesafe.sbt" % "sbt-native-packager" % "1.8.0")
```

次にGraalVMNativeImagePluginを有効にします。

```scala:build.sbt
lazy val root = (project in file("."))
  .enablePlugins(GraalVMNativeImagePlugin) // GraalVMNativeImagePluginを有効化
  .settings(
    name := "merry-christmas",
  )
```

オプションパーサーはPicocliを使うとパクリになってしまうので、ほんのりオリジナリティを出します。
PicocliではAnnotationProcessorを使っていてビルドの設定がちょっぴり複雑になるため、ここではScalaで書かれたscoptを採用しました。

```scala:build.sbt
lazy val root = (project in file("."))
  .enablePlugins(GraalVMNativeImagePlugin)
  .settings(
    name := "merry-christmas",
    libraryDependencies += "com.github.scopt" %% "scopt" % "4.0.0", // scopt v4.0.0を追加
  )
```

# 本体の実装

scoptは[README](https://github.com/scopt/scopt)を読みながら雰囲気で書いています。
作成するツールは`-n`または`--name`で名前を受け取って、クリスマスツリーとともに`Merry Christmas, <NAME>!`と印字するだけです。
とても簡素な内容なので、特に参考になるものでもないと思います。

```scala:
package example

import scopt.OParser

case class Options(name: String = "")

object MerryChristmas {
  val builder = OParser.builder[Options]
  val parser = {
    import builder._
    OParser.sequence(
      programName("merry-christmas"),
      head("merry-christmas", "0.1.0"),
      opt[String]('n', "name")
        .required()
        .action((n, o) => o.copy(name = n))
        .text("Your name")
    )
  }

  def main(args: Array[String]): Unit = {
    OParser.parse(parser, args, Options()) match {
      case Some(options) =>
        print(options)
      case _ =>
    }
  }
  // print関数は省略...
}
```

# native imageの作成・実行

native imageは`sbt graalvm-native-image:packageBin`を実行すると`target/graalvm-native-image/`に作成されます。

今回作成されたnative imageは約10.2MBと、誰もが同じ感想を持つと思いますが結構なサイズです。
この問題は早くからGithub上に[issue](https://github.com/oracle/graal/issues/287)が上がっていますが、コメントにもあるようにプログラム規模が大きくなっても5MBのオーバーヘッドは変わらないので小さいimageでのみの問題だそうです。(本当？)

では実行してみます。

```shell_session
$ ./target/graalvm-native-image/merry-christmas --name tsatow

          ⭐
         彡ミ
        彡*◎。
       彡彡‡*。
     +彡★ミ♪ミ｡
     ‡彡※◎ミ▲+ミ
   +彡彡▲彡★ミミ+  Merry Christmas, tsatow!
   彡ﾟ◎彡♪ミ☆*ミ☆
 ｡彡★*彡彡◆ミ+ミ◎｡
 彡彡彡☆彡彡ミ★ミミ
         ┃┃
       ■■■■■■
        ■■■■
        ■■■■
```

ツリーがちょっと歪んでますが、とにもかくにも表示されました。
自分の作ったツールが自分にMerry Christmasと言ってくれると、なんだか心が満たされる感じがしますね。

気になる実行時間ですが、以下の環境で動作させたところ大体0.010ms程度でした。

```shell-session
$ sysctl machdep.cpu.brand_string
machdep.cpu.brand_string: Intel(R) Core(TM) i7-4770HQ CPU @ 2.20GHz
$ java -version
openjdk version "11.0.9" 2020-10-20
OpenJDK Runtime Environment GraalVM CE 19.3.4 (build 11.0.9+10-jvmci-19.3-b18)
OpenJDK 64-Bit Server VM GraalVM CE 19.3.4 (build 11.0.9+10-jvmci-19.3-b18, mixed mode, sharing)
```

sbt-assemblyでfat jarを作成して計測すると平均0.550ms程度なので、小さなプログラムであれば[^3]native-imageの恩恵を受けられそうです。

# まとめ

起動時間が気になるようなCLIツールも、native-imageを使えばScalaで作ることができそうです。
しばらく色々作って試して遊んでみます。

それでは皆様、良いクリスマスを。


[^1]: [issue](https://github.com/oracle/graal/issues/2420)が上がっています。
[^2]: 時間の都合上、どのバージョンから失敗するようになるのかは細かく調査していません。ざっくりissueが上がった時点の一つ前のバージョンを採用しています。
[^3]: 処理の実行自体が問題になるほどの規模ではないので、ここでは起動時間の差が大きく影響していると考えています。(ちゃんと調べろ)