Windows GraalVM で Clojure 製CLI ツールの native-image 作ってみた
2020/07/01 追記
GraalVM 20.1.0出たので
インストールしてる時 native-image
を gu install native-image
で入れようとした時に
入れられなかったのでローカルでインストールした手順も追加。20.0 でも同じ PKIX なんちゃらのエラーだったので
おそらく証明書かネットワークの問題だと思い keytool
で色々やってたが上手く行かなかったので。
GraalVM 20.0.0
になって native-image
の Windows 対応が 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 をインストールする必要がある。
ただこの辺りは上手く動いた推測で話しているので間違っていたり余計な手順があるかもしれない。
実際の手順
- Visual Studio 2019のインストーラダウンロード
- インストーラ起動後、
ワークロード
で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 を解消する事で解決できた。
これが外部ライブラリで出るようだったら面倒かも。一応 手動作成した設定ファイルで明示的に指定する 方法もあるみたいだが極力この方法は避けたいところ。
ビルド
- 64bit 向け開発者用コマンドプロンプトの起動
- VS2019 を前述の手順でインストールするとスタートメニューに色々な種類のコマンドプロンプトが出るが、インストールした GraalVM は 64bit 版なので
x64 Native Tools Command Prompt for VS 2019
を選択して起動
- VS2019 を前述の手順でインストールするとスタートメニューに色々な種類のコマンドプロンプトが出るが、インストールした GraalVM は 64bit 版なので
- そこで
lein native-image
実行- それなりに時間がかかるのでしばらく待つ。
比較
実行時間の計測
ファイルをいじったりするものだったら Clojure の -main
の中の処理全体を time
マクロで囲ってしまうのが手軽だがここらで UNIX の time
コマンドのように Windows でも外部から実行時間を測る方法がないものかと調べてみた。
PowerShell の Measure-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 なのかね。