Gradle Plugin for GraalVM Native Image の使い方(Windows platform)


はじめに

GraalVM 公式のGradleネイティブイメージプラグインの使い方です。

Gradleネイティブイメージプラグインのリポジトリは以下となっています。

github.com

ここでのバージョンは以下となります。

  • 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ネイティブイメージの制限については以下を参照してください。

blog1.mammb.com

実行時に動的な振る舞いをするアプリケーションをネイティブイメージ化するには、動的な挙動を事前に定義してメタデータとして提供する必要があります。

アプリケーション実行時にエージェントを仕込むことで、このメタデータの生成を自動化できます。

これには、プロファイルとして 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 の場合は以下を参考にしてください。

blog1.mammb.com