はじめに
GraalVM 公式のGradleネイティブイメージプラグインの使い方です。
Gradleネイティブイメージプラグインのリポジトリは以下となっています。
ここでのバージョンは以下となります。
- Gradle : 8.2
- Gradleネイティブイメージプラグイン:0.9.25
- GraalVM :20.0.2+9.1
Gradleネイティブイメージプラグイン 0.9.25 は、Gradle 8.3 では動作しないので注意してください。
本事象は GraalVM Native Image Plugin 0.9.26 で改善されたようです
Visual Studio Build Tools の準備
ネイティブイメージ生成には、Visual Studio Build Tools が必要となるため、インストールしておきます。
> winget add Microsoft.VisualStudio.2022.BuildTools
Visual Studio Build Tools 単品ではなく、 Visual Studio コミュニティを入れてしまっても良いです。
> winget add Microsoft.VisualStudio.2022.Community
インストーラでは「C++によるデスクトップ開発」を選択します。
ネイティブイメージ作成には、Visual Studio の SDK のパスが通ったプロンプトから操作する必要があります。
Visual Studio インストールで作成されるスタートメニューから「x64 Native Tools Command Prompt for VS 2022」を選択してプロンプトを起動します。
以降の操作はこのプロンプトから行います。
プロジェクトの準備
Gradle init タスクでプロジェクトを生成します。
> mkdir example > cd example > gradle init --type java-application --dsl kotlin --test-framework junit-jupiter --project-name example --package example
settings.gradle.kts
を以下のように編集します。
pluginManagement { repositories { mavenCentral() gradlePluginPortal() } } plugins { id("org.gradle.toolchains.foojay-resolver-convention") version "0.7.0" } rootProject.name = "example" include("app")
Gradleネイティブイメージプラグインは、まだGradleプラグインポータルで利用できないため、pluginManagement
の定義が必要です。
GraalVM をGradleツールチェーンで使うため foojay-resolver-convention
のバージョンを最新のものにしておきます。
続いて app/build.gradle.kts
を以下のように編集します。
plugins { application id("org.graalvm.buildtools.native") version "0.9.25" } repositories { mavenCentral() } application { mainClass.set("example.App") } java { toolchain { languageVersion.set(JavaLanguageVersion.of(20)) vendor.set(JvmVendorSpec.GRAAL_VM) } }
Gradleネイティブイメージプラグイン org.graalvm.buildtools.native
を追加します。
ツールチェーンで JvmVendorSpec.GRAAL_VM
を指定することで、GraalVM は自動でダウンロードされて利用可能になります(%HOMEDRIVE%%HOMEPATH%\.gradle\jdks\graalvm_community-20-amd64-windows
に配備されます)。
ツールチェーンで GraalVM を指定しても、現在の Gradleネイティブイメージプラグイン(0.9.25) では、以下のように javaLauncher
を指定する必要があります(指定しない場合、Cannot query the value of property 'javaLauncher' because it has no value available.
のようにエラーになります)。
graalvmNative { binaries { named("main") { javaLauncher.set(javaToolchains.launcherFor { languageVersion.set(JavaLanguageVersion.of(20)) vendor.set(JvmVendorSpec.GRAAL_VM) }) } } }
javaLauncher
を指定することで、ネイティブイメージの生成は可能になりますが、トレースエージェントでの実行の際に、以下の様に、GRAALVM_HOME
または JAVA_HOME
からホームディレクトリを取得しており、取得できない場合はエラーになります。
public class NativeImageExecutableLocator { public static Provider<String> graalvmHomeProvider(ProviderFactory providers, Diagnostics diagnostics) { return diagnostics.fromEnvVar("GRAALVM_HOME", providers) .orElse(diagnostics.fromEnvVar("JAVA_HOME", providers)); }
なので現時点では、結局 GRAALVM_HOME
を設定せざるを得なくなるので、gradlew build
などでビルドして、配備された GraalVM に対して以下のように指定してしまう方がハマらなくて良いです。
set GRAALVM_HOME=%HOMEDRIVE%%HOMEPATH%\.gradle\jdks\graalvm_community-20-amd64-windows\graalvm-community-openjdk-20.0.2+9.1
今後の Gradleネイティブイメージプラグインのバージョンアップで改善されると思われます。
Gradle ツールチェーン経由でなく、別の方法で GraalVM を導入した場合も同様に、GRAALVM_HOME
を設定しておきます。
単純なアプリをネイティブイメージ化
以下の Hello World からネイティブイメージを作成します。
package example; public class App { public String getGreeting() { return "Hello World!"; } public static void main(String[] args) { System.out.println(new App().getGreeting()); } }
このような単純なソースは、Gradleネイティブイメージプラグインで単に nativeCompile
を実行するだけです。
> gradlew nativeCompile
以下のようなログと共に、ネイティブイメージが生成されます。
> Task :app:generateResourcesConfigFile [native-image-plugin] Resources configuration written into C:\...\temp\example\app\build\native\generated\generateResourcesConfigFile\resource-config.json > Task :app:nativeCompile [native-image-plugin] GraalVM Toolchain detection is disabled [native-image-plugin] GraalVM location read from environment variable: GRAALVM_HOME [native-image-plugin] Native Image executable path: C:\Users\...\.gradle\jdks\graalvm_community-20-amd64-windows\graalvm-community-openjdk-20.0.2+9.1\bin\native-image.cmd ======================================================================================================================= GraalVM Native Image: Generating 'app' (executable)... ======================================================================================================================== For detailed information and explanations on the build output, visit: https://github.com/oracle/graal/blob/master/docs/reference-manual/native-image/BuildOutput.md ------------------------------------------------------------------------------------------------------------------------ [1/8] Initializing... (8.3s @ 0.09GB Java version: 20.0.2+9, vendor version: GraalVM CE 20.0.2+9.1 Graal compiler: optimization level: 2, target machine: x86-64-v3 C compiler: cl.exe (microsoft, x64, 19.37.32822) Garbage collector: Serial GC (max heap size: 80% of RAM) [2/8] Performing analysis... [*****] (14.4s @ 0.32GB 3,140 (74.27%) of 4,228 types reachable 3,795 (50.39%) of 7,531 fields reachable 15,218 (46.16%) of 32,966 methods reachable 944 types, 81 fields, and 493 methods registered for reflection 61 types, 51 fields, and 52 methods registered for JNI access 1 native library: version [3/8] Building universe... (2.4s @ 0.33GB [4/8] Parsing methods... [*] (1.8s @ 0.33GB [5/8] Inlining methods... [***] (1.1s @ 0.40GB [6/8] Compiling methods... [****] (15.3s @ 0.32GB [7/8] Layouting methods... [*] (1.4s @ 0.50GB [8/8] Creating image... [**] (2.6s @ 0.31GB 5.45MB (42.09%) for code area: 8,770 compilation units 7.34MB (56.63%) for image heap: 94,839 objects and 5 resources 169.30kB ( 1.28%) for other data 12.95MB in total ------------------------------------------------------------------------------------------------------------------------ Top 10 origins of code area: Top 10 object types in image heap: 4.12MB java.base 1.16MB byte[] for code metadata 969.17kB svm.jar (Native Image) 954.75kB java.lang.String 112.17kB java.logging 937.59kB byte[] for general heap data 63.52kB org.graalvm.nativeimage.base 730.96kB java.lang.Class 47.59kB jdk.proxy1 714.29kB byte[] for java.lang.String 39.02kB jdk.proxy3 269.84kB com.oracle.svm.core.hub.DynamicHubCompanion 27.18kB jdk.internal.vm.ci 250.08kB java.util.HashMap$Node 24.18kB org.graalvm.sdk 212.95kB java.lang.Object[] 11.42kB jdk.proxy2 181.78kB java.lang.String[] 8.15kB jdk.internal.vm.compiler 154.27kB byte[] for embedded resources 1.26kB for 2 more packages 1.37MB for 888 more object types ------------------------------------------------------------------------------------------------------------------------ Recommendations: HEAP: Set max heap for improved and more predictable memory usage. CPU: Enable more CPU features with '-march=native' for improved performance. ----------------------------------------------------------------------------------------------------------------------- 3.0s (6.1% of total time) in 137 GCs | Peak RSS: 1.00GB | CPU load: 5.13 ------------------------------------------------------------------------------------------------------------------------ Produced artifacts: C:\...\example\app\build\native\nativeCompile\app.exe (executable) ======================================================================================================================== Finished generating 'app' in 47.9s. [native-image-plugin] Native Image written to: C:\...\example\app\build\native\nativeCompile
生成されたネイティブイメージは nativeRun
で以下のように実行することができます。
> gradlew nativeRun Hello World!
直接、生成されたネイティブイメージを実行することもできます。
> app\build\native\nativeCompile\app.exe Hello World!
リフレクションを利用したアプリをネイティブイメージ化
App を少し変更しましょう。app/build.gradle.kts
に依存を追加します。
dependencies {
implementation("com.fasterxml.jackson.core:jackson-databind:2.15.2")
}
App.java
を以下のように変更します。
package example; import com.fasterxml.jackson.core.PrettyPrinter; import com.fasterxml.jackson.databind.ObjectMapper; import java.nio.file.Files; import java.nio.file.Path; public class App { public static void main(String[] args) throws Exception { if (args == null || args.length < 1) { return; } String ppName = (args.length > 1) ? args[1] : "com.fasterxml.jackson.core.util.DefaultPrettyPrinter"; PrettyPrinter pp = (PrettyPrinter) Class.forName(ppName) .getDeclaredConstructor().newInstance(); ObjectMapper mapper = new ObjectMapper(); Object json = mapper.readValue(Files.readString(Path.of(args[0])), Object.class); String string = mapper.writer(pp).writeValueAsString(json); System.out.println(string); } }
Json ファイルを読み込んで、Jackson で整形して出力するだけのアプリになります。
Jsonファイルは test.json
として以下のようなサンプルとしておきます。
{"fruit":"Apple","size":"Large","color":"Red"}
整形は、実行時のコマンドライン引数で取得したクラスで動的に変更できます。
これを、先と同様にネイティブイメージ生成して実行します。
> gradlew nativeCompile
作成されたネイティブイメージを実行すると以下のような例外となります。
> app\build\native\nativeCompile\app.exe test.json Exception in thread "main" java.lang.NoSuchMethodException: com.fasterxml.jackson.core.util.DefaultPrettyPrinter.<init>() at java.base@20.0.2/java.lang.Class.checkMethod(DynamicHub.java:1038) at java.base@20.0.2/java.lang.Class.getConstructor0(DynamicHub.java:1204) at java.base@20.0.2/java.lang.Class.getDeclaredConstructor(DynamicHub.java:2854) at example.App.main(App.java:19)
ネイティブイメージの生成は、実行前にクラスの依存を全て調べ、到達する対象をネイティブコンパイルします。
実行時に動的に切り替わるクラス間の依存があった場合は、上のような例外となります。
以下のように静的にクラスを決定できるケースでは、リフレクションを使用していても、対象を一意に決定できるので、(現在のGraalVMでは)実行時にエラーは発生しません。
String ppName = "com.fasterxml.jackson.core.util.DefaultPrettyPrinter";
PrettyPrinter pp = (PrettyPrinter) Class.forName(ppName)
.getDeclaredConstructor().newInstance();
このようなGraalVMネイティブイメージの制限については以下を参照してください。
実行時に動的な振る舞いをするアプリケーションをネイティブイメージ化するには、動的な挙動を事前に定義してメタデータとして提供する必要があります。
アプリケーション実行時にエージェントを仕込むことで、このメタデータの生成を自動化できます。
これには、プロファイルとして agent
を指定してアプリケーションを実行します。
> gradlew -Pagent run --args ../test.json
エージェントによりメタデータが収集されます。これを以下でアプリケーション側にコピーします。
> gradlew metadataCopy --task run --dir src/main/resources/META-INF/native-image
再度ネイティブイメージを生成して実行すると
> gradlew nativeCompile > app\build\native\nativeCompile\app.exe test.json { "fruit" : "Apple", "size" : "Large", "color" : "Red" }
以下のように実行することができます。
では、com.fasterxml.jackson.core.util.MinimalPrettyPrinter
というクラスを指定した場合はどうでしょう。
この場合は、ネイティブイメージ生成時に呼び出されるクラスを知り得ないため、以下のようなエラーになります。
> app\build\native\nativeCompile\app.exe test.json com.fasterxml.jackson.core.util.MinimalPrettyPrinter Exception in thread "main" java.lang.ClassNotFoundException: com.fasterxml.jackson.core.util.MinimalPrettyPrinter at org.graalvm.nativeimage.builder/com.oracle.svm.core.hub.ClassForNameSupport.forName(ClassForNameSupport.java:123) at org.graalvm.nativeimage.builder/com.oracle.svm.core.hub.ClassForNameSupport.forName(ClassForNameSupport.java:87) at java.base@20.0.2/java.lang.Class.forName(DynamicHub.java:1322) at java.base@20.0.2/java.lang.Class.forName(DynamicHub.java:1285) at java.base@20.0.2/java.lang.Class.forName(DynamicHub.java:1278) at example.App.main(App.java:18)
これを解決するには、再びエージェントを使用してメタデータを収集することもできますが、ここでは定義を手動で編集してみましょう。
リフレクションに関する情報は、 src/main/resources/META-INF/native-image/reflect-config.json
になり、エージェントが生成した内容は以下のようになっています。
[ { "name":"com.fasterxml.jackson.core.util.DefaultPrettyPrinter", "methods":[{"name":"<init>","parameterTypes":[] }] }, { "name":"com.fasterxml.jackson.databind.ext.Java7SupportImpl", "methods":[{"name":"<init>","parameterTypes":[] }] }, { "name":"java.util.concurrent.atomic.AtomicReference", "fields":[{"name":"value"}] } ]
MinimalPrettyPrinter
を使えるようにするには以下を追加します。
[ ... { "name":"com.fasterxml.jackson.core.util.MinimalPrettyPrinter", "methods":[{"name":"<init>","parameterTypes":[] }] }, ... ]
再度ネイティブイメージを生成します。
> gradlew nativeCompile
これにて、いずれのケースでも動作するネイティブイメージが生成できました。
> app\build\native\nativeCompile\app.exe test.json com.fasterxml.jackson.core.util.DefaultPrettyPrinter { "fruit" : "Apple", "size" : "Large", "color" : "Red" } > app\build\native\nativeCompile\app.exe test.json com.fasterxml.jackson.core.util.MinimalPrettyPrinter com.fasterxml.jackson.core.util.MinimalPrettyPrinter {"fruit":"Apple","size":"Large","color":"Red"}
まとめ
Windows platform における Gradle GraalVM ネイティブイメージプラグインの使い方について紹介しました。
プラグインのバージョンは 0.9.25 と、メジャーリリース以前であり、なかなかすんなり動かないことも多いです。
macOS の場合は以下を参考にしてください。