概要
ScalaとGraalVMを使ってクリスマスっぽいAAを出すだけの超シンプルなCLIツールを作ってみようというお話です。
CLIツールなどで起動時間が気になる場合はGraalVMでnative image作ればよさそうと考えて、GRAALVM, PICOCLIとJAVAでときめくネイティブコマンドラインアプリを作ろうを参考にScala+Picocli+GraalVMで何か作ってみようと思ったのですが、今日いざ書こうとしたら既に先駆者がいらっしゃいました。
もう既にピンズドな記事があるのでこの記事の意義は割と皆無ですが、他にネタもないのでこのままやっていこうと思います。
準備
GraalVMのセットアップ
使用するGraalVMのバージョンは19.3.4です。
GraalVMの最新バージョンは20.3.0ですが、このバージョンではnative imageを作成できませんでした。1
そこで、GraalVMのバージョンは去年リリースされたものを採用しています。2
GraalVMはこちらを参考に以下のようにセットアップしました。
$ 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のセットアップ後にインストールしてください。
$ 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を使用して作成します。
以下のようにしてScalaプロジェクトにプラグインを追加してください。
addSbtPlugin("com.typesafe.sbt" % "sbt-native-packager" % "1.8.0")
次にGraalVMNativeImagePluginを有効にします。
lazy val root = (project in file("."))
  .enablePlugins(GraalVMNativeImagePlugin) // GraalVMNativeImagePluginを有効化
  .settings(
    name := "merry-christmas",
  )
オプションパーサーはPicocliを使うとパクリになってしまうので、ほんのりオリジナリティを出します。
PicocliではAnnotationProcessorを使っていてビルドの設定がちょっぴり複雑になるため、ここではScalaで書かれたscoptを採用しました。
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を読みながら雰囲気で書いています。
作成するツールは-nまたは--nameで名前を受け取って、クリスマスツリーとともにMerry Christmas, <NAME>!と印字するだけです。
とても簡素な内容なので、特に参考になるものでもないと思います。
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が上がっていますが、コメントにもあるようにプログラム規模が大きくなっても5MBのオーバーヘッドは変わらないので小さいimageでのみの問題だそうです。(本当?)
では実行してみます。
$ ./target/graalvm-native-image/merry-christmas --name tsatow
          ⭐
         彡ミ
        彡*◎。
       彡彡‡*。
     +彡★ミ♪ミ。
     ‡彡※◎ミ▲+ミ
   +彡彡▲彡★ミミ+  Merry Christmas, tsatow!
   彡゚◎彡♪ミ☆*ミ☆
 。彡★*彡彡◆ミ+ミ◎。
 彡彡彡☆彡彡ミ★ミミ
         ┃┃
       ■■■■■■
        ■■■■
        ■■■■
ツリーがちょっと歪んでますが、とにもかくにも表示されました。
自分の作ったツールが自分にMerry Christmasと言ってくれると、なんだか心が満たされる感じがしますね。
気になる実行時間ですが、以下の環境で動作させたところ大体0.010ms程度でした。
$ 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程度なので、小さなプログラムであれば3native-imageの恩恵を受けられそうです。
まとめ
起動時間が気になるようなCLIツールも、native-imageを使えばScalaで作ることができそうです。
しばらく色々作って試して遊んでみます。
それでは皆様、良いクリスマスを。