日曜プログラミング

休日趣味でやってるプログラミング関連記事をダラダラと

Windows GraalVM で Clojure 製CLI ツールの native-image 作ってみた

2020/07/01 追記

GraalVM 20.1.0出たので インストールしてる時 native-imagegu install native-image で入れようとした時に 入れられなかったのでローカルでインストールした手順も追加。20.0 でも同じ PKIX なんちゃらのエラーだったので おそらく証明書かネットワークの問題だと思い keytool で色々やってたが上手く行かなかったので。

GraalVM 20.0.0 になって native-imageWindows 対応が experimental はまだ取れてないが improves significantly な状態になったそうなのでそろそろどうかなと試してみた。

自分の native-image の理解

  • JDK, JRE 無しで単体動作する exe が作れる
  • JVM 起動時間が無いためプログラムの起動が速い
  • 起動後の動作については JVM で動作するものと比べ必ずしも速いとは限らない
    • 検証した人によればむしろ遅いらしい

他でも言われているが、これならJava(Clojure)製アプリの配布が大分楽になる。開発ではないが仕事で必要に応じてちょこちょこ CLI ツールを作っていた自分にとっては前から気になっていた。

ビルド完了までの手順

Graal VM 本体 JDK11 インストール

公式GithubからWindows版のgraalvm-ce-java11-windows-amd64-xx.x.x.zipをダウンロード(xx.x.x は最新版。今回は 20.0.0)

これを展開したフォルダのbinを PATH に通しておく。 自分は既に入れている OpenJDK13 と競合しないよう JAVA_HOME 環境変数を GraalVM の方へ切り替え、PATH%JAVA_HOME%\bin を設定した。

java -version で表示される中に GraalVM が見えていれば OK。

Native Image コンポーネントインストール

GraalVM 20.0.0 リリースノート によると Native Image コンポーネントは GraalVM をインストールしたフォルダの bin 以下にある gu 経由でインストールしてねとあるのでそれに従う。gu ってのは GraalVM Updater の事らしい。

公式ドキュメントInstallation from Catalogの項目によると以下でダウンロード・インストールまでしてくれる。

gu install native-image

他にどんなコンポーネントが入れられるのかはgu availableで一覧が出る。 Windows だと 20.0.0 時点では native-image のみの模様。他 OS だと Ruby とかのコンポーネントあるのかな?

gu install native-image で失敗する場合 [2020/07/01 追記]

ローカルにコンポーネントをダウンロードしてから gu でファイル指定してローカルインストールも可能。 試したのは GraalVM 20.1.0。

Github のリリースから Windows だと graalvm-ce-java8-windows-amd64-20.1.0.zip をダウンロードし、 コマンドプロンプトでダウンロードしたフォルダに移動し gu -L install graalvm-ce-java8-windows-amd64-20.1.0.zip とする。

>gu -L install native-image-installable-svm-java11-windows-amd64-20.1.0.jar
Processing Component archive: native-image-installable-svm-java11-windows-amd64-20.1.0.jar
Installing new component: Native Image (org.graalvm.native-image, version 20.1.0)

公式ドキュメント では Enterprise 版での説明になってるが Community 版でも同様にできるみたい。

Visual Studio 2019 インストール

native-image で単体動作する exe を作るにはどうやら C/C++ コンパイラが必要らしく、また Windows では コンパイラで exe を作るのに必要なヘッダ、ライブラリをあらかじめ指定した状態で立ち上がる開発者用コマンドプロンプトから native-image を 実行する必要があるみたい。

で、これらを満たすにはこの記事時点だと Visual Studio 2019 をインストールする必要がある。

ただこの辺りは上手く動いた推測で話しているので間違っていたり余計な手順があるかもしれない。

実際の手順

  1. Visual Studio 2019インストーラダウンロード
  2. インストーラ起動後、ワークロードC++によるデスクトップ開発を選択

サイズがデカいのでしばらく待つ。

lein-native-image のインストール・設定

leiningen のタスクで native-image を生成してくれる lein-native-image と言うプラグインが既にあるのでありがたくこれを使わせてもらう。

今回テストしたツールのproject.cljはこんな感じ。多分これが最低限の設定。 ツールとしては詳細は書けないが複数ファイルの CSV を読み込んでゴニョゴニョするもの。Clojure 1.9 なのは元々 Java7 JRE で動かす事を想定していたため。

(defproject native-image-test "1.0.0"
  :plugins [[io.taylorwood/lein-native-image "0.3.1"]]

  :dependencies [[org.clojure/clojure "1.9.0"]
                 [org.clojure/data.csv "1.0.0"]
                 [commons-io/commons-io "2.6"]]
  :main ^:skip-aot native-image-test.core
  :target-path "target/%s"

  :profiles {:uberjar 
             {:aot :all
              :native-image
              {:opts ["--initialize-at-build-time"]
               :jvm-opts ["-Dclojure.compiler.direct-linking=true"]}}})

native-imageに渡すオプションの--initialize-at-build-timeはこれを付けないとコンパイル途中で以下のワーニングが出て exe が生成されても JDK が無いと動かないものになるので必須。

Warning: Aborting stand-alone image build due to unsupported features

JVM の方に渡すclojure.compiler.direct-linking=trueコンパイル時間改善になるらしいので付けておく。 他にもオプション設定が色々あるが とりあえずはこれだけ追加している。

Reflection への対処

さて、どんな Java コードも生成できるわけではないようで、 色々と制限があるみたい。

その一つが Reflection で、自分は Clojure で開発する際あまり気にせずに作っていたが、今回 native-image 実行時に初めてつまづいた。

具体的には Clojure コードで *warn-on-reflection*true にしてコンパイル時に出る Reflection warning を放置して lein native-image を実行するとコンパイル途中で以下のワーニングが出て JDK に依存する exe しか生成されない。

Warning: Aborting stand-alone image build due to unsupported features

今回の場合は自分が書いたコードで発生していたもので何の型を受け取るか分かっていたのでタイプヒントを付けて Reflection warning を解消する事で解決できた。

これが外部ライブラリで出るようだったら面倒かも。一応 手動作成した設定ファイルで明示的に指定する 方法もあるみたいだが極力この方法は避けたいところ。

ビルド

  1. 64bit 向け開発者用コマンドプロンプトの起動
    • VS2019 を前述の手順でインストールするとスタートメニューに色々な種類のコマンドプロンプトが出るが、インストールした GraalVM は 64bit 版なのでx64 Native Tools Command Prompt for VS 2019を選択して起動
  2. そこで lein native-image 実行
    • それなりに時間がかかるのでしばらく待つ。

比較

実行時間の計測

ファイルをいじったりするものだったら Clojure-main の中の処理全体を time マクロで囲ってしまうのが手軽だがここらで UNIXtime コマンドのように Windows でも外部から実行時間を測る方法がないものかと調べてみた。

PowerShellMeasure-Command が良さそう(参考)

と言うわけで以下の名前がtimeにならないバッチファイルを PATH の通った所に用意した(時刻設定をする同名 DOS コマンドがあるので)。

@echo off
powershell -C (Measure-Command {%*}).TotalMilliseconds

batch からさらに別のプロセス呼び出すようなのは別プロセス実行分の時間は測れないのとカレントパスの exe の場合 .\ を明示的に指定する必要があるがそこはまあ良しとする。

結果比較

lein uberjarして作ったstandalone版のファイルサイズと実行時間は以下。

Java7 JRE native-image
ファイルサイズ 4.41MB 10.3MB
実行時間 1563.58ms 418.89ms

一見ファイルサイズが増大してるが、JRE7フォルダのサイズが122MBな事を加味すれば 相当小さくなっている。

実行時間も速い。ただ、これはもともと CPU 時間を大して使わないプログラムなので 大部分は起動時間が短縮された事が大きいと思う。Java プログラム特有の起動時のモタつきなく動くのはちょっとした感動。

プログラム本処理部分動作の動作やメモリ使用量がどうかなども気になる所ではあるがそれはまた別の気が向いた時にでも。

ビルドできるまでに調べた事

以下はここまでの手順に到達するまでの調査経緯メモ。興味なければ飛ばして構わない。

最初はVS2019インストールする前にlein-native-imageのインストール・設定のみ終わらせてlein native-imageを実行したのだが、以下のエラーが出てつまづいた。

Error: Unable to compile C-ABI query code. Make sure native software development toolchain is installed on your system.
Error: Use -H:+ReportExceptionStackTraces to print stacktrace of underlying exception
Error: Image build request failed with exit status 1
Failed to create native image

初っ端にエラーが出た後ググると以下がすぐ出た。

Unable to compile C-ABI query code #1258

Windows SDK for Windows 7 をインストールして SDK 開発者用コマンドプロンプトから実行すると良いとの事。自分の環境は Windows10 だし Windows 10 SDK でいいだろと入れてみたものの見当たらない。

Windows 10 での開発者用コマンドプロンプトってどうなってんのと探してみると Windows 7.1 SDKが開発者用コマンドプロンプトが含まれてた最終バージョン らしく、それ以降は Visual Studio の方に含まれてるらしい。どこからかは不明。直前リンクにあるトピだと少なくとも VS2015 以降はそうなってるっぽい。

また、Visual Studio 用開発者コマンド プロンプト って何なのと言うのも気になってみてみると、 公式ドキュメント によれば、

それは、特定の環境変数を自動的に設定するコマンド プロンプトです。

との事。

しかしこの辺の事は 公式ドキュメントの Prerequisites 辺りに書いてくれんのかなあ。どう書くと良いのか決めかねてるからまだ experimental なのかね。