HN 日本語サマリー

← 一覧へ戻る
プログラミング

Raspberry Pi 5 上での JavaFX 27 ネイティブイメージ

JavaFX 27 Native Image on a Raspberry Pi 5 (ennerf.github.io)

33 pointsby 0x54MUR414 コメント

要約

この記事では、GraalVM Native Image を使用して JavaFX アプリケーションを Raspberry Pi 5 上でネイティブ実行する手法について解説しています。ネイティブイメージ化により、起動時間や初回表示時間が大幅に短縮され、ユーザーエクスペリエンスが向上することが、ベンチマーク結果と共に示されています。また、JavaFX をネイティブイメージ化するための既存ツール(Gluon Substrate、BellSoft Liberica NIK)の課題と、それらを克服するために開発された新しいツールキット StaticFX についても紹介されています。

全文翻訳

Raspberry Pi 5 上での JavaFX 27 ネイティブイメージ

当初、GraalVM Native Image は JavaFX をモバイルターゲットにデプロイする手段として使用していました。しかし、モバイルアプリがデスクトップアプリよりもキビキビと動作することを確認した後、GUI および CLI アプリケーションをすべて Ahead-of-Time (AOT) コンパイルに移行していきました。GUI はほとんどの場合、解釈モードで実行されます(ユーザーが JIT の閾値に達するには数千回クリックする必要があるでしょう)。AOT を使用すると、起動時間と初回アクセス時間が最大 90% 削減され、ユーザーエクスペリエンスが著しく向上します。

表 1. Raspberry Pi 5 上での AtlantaFX サンプラーの比較

AtlantaFX サンプラー (RPi5)jlink (JIT)jlink + CDS (JIT)ネイティブイメージ (AOT)
ディストリビューションサイズ139 MB155 MB (+12%)124 MB (-11%)
初回ウィンドウ表示時間3.6 秒2.7 秒 (-26%)0.5 秒 (-86%)
初回アクセス: HTMLEditor (WebView)2.39 秒2.31 秒 (-3%)0.22 秒 (-91%)
初回アクセス: Overview (FXML)1.90 秒1.50 秒 (-21%)0.32 秒 (-83%)
起動後のプライベートメモリ224 MB244 MB (+9%)164 MB (-27%)
全ページ表示後のプライベートメモリ1424 MB1396 MB (-2%)634 MB (-55%)

実際、高性能な Ryzen 9 9950X デスクトップで AtlantaFX サンプラーを jlink + CDS で起動するには、小さな Raspberry Pi 5 で AOT コンパイルされた同じアプリケーションを実行するよりも 2.5 倍以上(1.3 秒 vs 0.5 秒、ベンチマーク参照)時間がかかります。FXML、WebView、MediaPlayer、AWT 統合を含む全ページを対象とした短いビデオウォークスルーで、全体の実行が記録されています。

JavaFX は、すべてが正しく設定されていれば、ネイティブイメージ上で非常にうまく動作しますが、複雑なアプリケーションを初めて実行できるようにするのは、通常、簡単な経験ではありません。この最初の高いハードルにより、JavaFX、そしてデスクトップ Java 全般が、ネイティブイメージとの互換性がない、またはうまく動作しないという評判を得てしまいました。この原因の大部分は、ネイティブレイヤーを隠すために舞台裏で多くの「マジック」を実行する複雑で特殊なツール群にあると考えています。何かが壊れると、多くの Java 開発者がデバッグ方法を知らないエラーが発生します。

この記事では、これらのツールの内部構造を解明し、最新の Oracle GraalVM を使用して最新の JavaFX 27 リリースをデスクトップターゲットで実行するために、なぜ独自のツールを構築したのかを示します。

3 つのアプローチ

Ahead-of-Time コンパイルや制御されたサーバー環境向けに設計された Quarkus や Micronaut のようなフレームワークとは異なり、JavaFX は、ネイティブイメージよりもはるかに前から存在し、任意のクライアントマシン上でさまざまなオペレーティングシステムで実行する必要がある、大規模で非常に動的な GUI フレームワークです。そのスタック全体を機能させるには、リンカー修正、置換、および約 1000 件のメタデータエントリを含む専用のソリューションが必要です。Gluon の Substrate と BellSoft の Liberica NIK が 2 つの確立されたオプションであり、私たちは最近、3 番目のオプションとして StaticFX をリリースしました。

Gluon Substrate

Gluon の Substrate は、Windows、macOS、Linux、およびモバイルターゲットの iOS と Android を含む、ほぼあらゆるものに JavaFX をポートできる複雑なツールチェーンです。JavaFX の有無にかかわらず実行可能なファイル、共有ライブラリ、静的ライブラリを作成でき、リソースを処理し、カスタム C 拡張機能を追加し、OS 固有のバンドルとインストーラーをビルドし、さらには小さなターゲットに SSH 経由でリモートデプロイすることもできます。

これらすべてを機能させるには、GraalVM ディストリビューションからモバイルツーリング、専用の Maven および Gradle プラグインに至るまで、ほぼすべてのレイヤーで変更されたビルドとカスタムツールが必要です。その結果は非常に印象的なスタックであり、Gluon は JavaFX がどこでも実行できるようにする多くのアップストリーム変更に貢献した功績があります。

しかし、動くターゲットの数が多いため、壊れやすい組み合わせになり、簡単に破綻する可能性があります。Gluon によると、「iOS、Android、AOT コンパイラ、JVM コンポーネント、または JDK の各新しいリリースにはパッチと調整が必要でした」とのことなので、彼らは OpenJDK Mobile に移行しています。これは、メタデータの必要性を排除し、ユーザーエクスペリエンスを大幅に簡素化するはずの、より標準的な JDK + Leyden アプローチです。

とはいえ、既存のツールは引き続きメンテナンスアップデートを受けており(ドキュメント参照)、基盤となる膨大な複雑さを考慮すると、比較的使用しやすいです。しかし、Gluon が現在サポートしている最新バージョンは GraalVM 23 Community Edition (CE) と JavaFX 21 であり、新しい JavaFX リリースを使用するためのサポートされている方法は現在ありません。

私たちは内部で 1 ~ 2 年間 Gluon のツールを使用してきましたし、モバイルターゲットについては今でも参照しています。モバイルでの JavaFX は、当初予想していたよりもはるかにうまく機能しており、フル 2D および 3D レンダリング、スタイリングとレイアウトのホットリロード(Mobile Scope参照)を備えた 5 つのターゲットすべてにデプロイできるのは非常に便利です。

ライセンスモデルはよくある混乱のポイントです。ライセンスが必要なのは、彼らの リッチコンポーネントライブラリ だけですが、ビルドツールはすべてのプラットフォームで無料で使用できます。

BellSoft の Liberica NIK

BellSoft は、より焦点を絞ったアプローチを取り、デスクトップアプリ専用の GraalVM CE ディストリビューションである Liberica NIK (full) を作成しました。何かを素早く実行する必要がある場合、NIK が最も簡単な開始方法です。

このディストリビューションには、JavaFX と Swing/AWT モジュール、およびそれらに対応するメタデータがバンドルされています。ユーザーは GRAALVM_HOME を設定するだけで、GraalVM の標準の native-maven-plugin と一緒に使用できます。標準の Oracle GraalVM でさえ静的 AWT アーカイブをバンドルしていますが、ユーザーは必要なメタデータを自分で生成する必要があります。

執筆時点では、NIK (full) は GraalVM 25 CE と JavaFX 25 の LTS バージョン、および 4 つの主要なデスクトップアーキテクチャでの GPU アクセラレーションパイプラインをサポートしています。現在、ソフトウェアパイプラインまたは linux-aarch64 ターゲットはサポートしていません。Gluon も BellSoft も、メディアおよび Web モジュールをネイティブイメージでサポートしていません。

HEBI の StaticFX

私たちは約 2 年間、デスクトップアプリケーションで Liberica NIK を使用してきましたが、確かにうまく機能していました。しかし、私たちのアプリケーションの一部は、サブミリ秒レベルでレイテンシを測定しており、コミュニティエディションの逃避分析が HotSpot よりもはるかに少ない割り当てしか排除せず、GC の動作の違いも相まって、不正確な測定値に遭遇しました。私たちの最悪の割り当てソースは、HotSpot では決して割り当てられない単純な new double[]{a,b} in Math::pow であることが判明しました。

2023 年に Oracle GraalVM(旧 Enterprise Edition)が GFTC ライセンスの下で無料で使用可能になったため、より優れた逃避分析、すべてのプラットフォームでの G1 GC、およびプロファイルガイド付き最適化 (PGO) のようなその他のエンタープライズ限定機能を利用するために切り替えたいと考えました。

また、Python や C++ のような他の言語に高性能 JavaFX ビジュアライゼーションをネイティブ共有ライブラリ経由で公開する hebi-charts のために、より新しい JavaFX バージョンにアップグレードする必要がありました。私たちの目標は、JavaFX 26 で導入された Headless モードとソフトウェアパイプラインを使用して、linux-aarch64 デバイスで SSH 経由でグラフィックや画像を生成できるようにすることでした。

Substrate も Liberica NIK もこの特定の組み合わせをサポートしていないため、最終的に StaticFX を作成しました。ある意味、JavaFX を JDK から分離したことは、ベンダーサポートを必要とせずにいつでも更新できる通常のライブラリとして扱えるという点で、良い利点となりました。

StaticFX は、ビルドシステムを乗っ取るのではなく、標準の GraalVM Feature として標準ツールチェーン上で実行され、jfx のソースを一切変更せずに必要なすべての設定を native-image に渡すことができます。また、静的リンクが利用できない場合に動的リンクをサポートするため、メディア、Web、および Swing の相互運用性を含むすべての JavaFX モジュールをサポートできます。

これは 2 つの成果物で構成されています。バージョン固有の静的アーカイブとメタデータ用の jfx-static-libs、および比較的バージョンに依存しないグルー用の jfx-static-feature です。GraalVM はクラスパス上の成果物から自動的に設定ファイルを検出するため、ユーザーは org.openjfx JAR の隣に 2 つの依存関係を追加するだけで済みます。

<dependency>
    <groupId>us.hebi.graalvm</groupId>
    <artifactId>jfx-static-feature</artifactId>
    <version>1.0</version>
</dependency>
<dependency>
    <groupId>us.hebi.graalvm</groupId>
    <artifactId>jfx-static-libs</artifactId>
    <version>${javafx.version}</version>
    <scope>runtime</scope>
</dependency>

StaticFX リポジトリには、詳細なドキュメントと例が含まれています。

StaticFX は GraalVM がサポートするプラットフォームのみをターゲットにできます。モバイルターゲットにはクロスコンパイルが必要であり、現在、Substrate のようなカスタムビルドツールなしでは実行できません。

3 つの問題

ツールに関係なく、JavaFX をネイティブイメージとして実行するには、いくつかのレイヤーが連携して動作する必要があります。すべての「マジック」は、3 つの問題を解決することに集約されます。

  1. 到達可能性メタデータ: ネイティブイメージはクローズドワールドの仮定を使用するため、実行時にリフレクションでアクセスされるすべてのリソース、クラス、メソッドはビルド時に知られている必要があります(例: reachability-metadata.json)。メタデータは 2 つの別々の部分で構成されます。ユーザーアプリケーション固有のメタデータと、基盤となるエンジン用の汎用メタデータです。エンジンメタデータは JavaFX のバージョンとオペレーティングシステムに紐付けられています。
  2. 静的バイナリ: JavaFX の約半分は C/C++/Objective-C コード(ウィンドウ処理、レンダリング、フォント、画像読み込み、エフェクト)です。自己完結型の実行可能ファイルには、これらのライブラリを静的アーカイブとしてコンパイルおよびリンクする必要があります。jfx は既に -PSTATIC_BUILD=true を介してアーカイブのビルドをサポートしていますが、現在それらは公開されていません。ユーザーはサードパーティベンダーに依存するか、自分でネイティブビルドを行う必要があります。
  3. 静的統合グルー: AOT ではいくつかのことが異なって動作し、グルーコードでブリッジする必要があります。静的アーカイブは実行時にロードされるのではなくビルド時にイメージに焼き込まれるため、ネイティブコードは異なる方法で登録、リンク、初期化する必要があり、これらの各ステップには独自の落とし穴があります。

次に、各アプローチが各部分をどのように解決するかを見ていきます。

表 2. ビルドソリューション

Gluon SubstrateLiberica NIKStaticFX
メタデータJSON ファイル (OS ごと)ディストリビューション内のパッチアノテーション→JSON (条件付き)
静的ライブラリ独自のバージョンと入れ替えバンドル済みユーザー指定 ${javafx.version}
統合グルービルドツール内のコードディストリビューション内のパッチクラスパス上の GraalVM Feature
アプリケーションメタデータ

メタデータのアプリケーション部分は、このブログ記事の焦点であるビルドソリューションとは独立していますが、開発者の観点からは最も一般的な問題であるため、エンジンの部分の前にカバーしたいと思います。

アプリケーション側では、リフレクションとリソース読み込みのほとんどは FXML と CSS から来ています。デスクトップアプリケーションは、実行時に多くの種類のファイルを読み込みます(例: .css、.fxml、.png、.jpg、.ttf)。これらはネイティブイメージに含める必要があり、メタデータは他のリソースのインポートやクラスのリフレクションによるインスタンス化(例: @import、-fx-skin、fx:controller)のような副作用もカバーする必要があります。

対応するメタデータは、手動で維持し同期を保つのが難しい reachability-metadata.json ファイルにあります。メタデータを作成するための一般的に推奨される方法は、GraalVM のトレーシングエージェントを使用することです。これは通常の JVM 実行にアタッチし、すべてリフレクションアクセスをトレースします。これは、すべてのエンドポイントに到達する自動テストスイートを持つバックエンドサービスにはかなりうまく機能しますが、GUI アプリケーションにとっては大きな苦痛です。開発者は、すべてのオペレーティングシステムで、すべての機能を繰り返しクリックし、アプリが変更されるたびにやり直す必要があります。

デフォルトでは、欠落している項目はサイレントに失敗するか、非常に役に立たない実行時エラーを生成する可能性があります。例: FXML ファイルが見つからない場合。

Caused by: java.lang.IllegalStateException: Location is not set.
        at javafx.fxml.FXMLLoader.loadImpl(...)

JDK 23 用の GraalVM は、オプトインの --exact-reachability-metadata ビルドフラグを追加しました。これにより、欠落している登録が詳細な指示に変わります。

Caused by: com.oracle.svm.core.jdk.resources.MissingResourceRegistrationError: Cannot access resource at path 'sampler/hello.png'. To allow this operation, add the following to the 'resources' section of 'reachability-metadata.json' and rebuild the native image:

    {
      "glob": "sampler/hello.png"
    }

これは、スタイルシートを読み込む際に JavaFX がチェックするバイナリ .bss バージョンのようなオプションファイルを探す場合にも失敗することに注意してください。幸いなことに、グロブは受け入れられるため(-Areachability.outputFormat=1.2.0 のみ)、これは単一の @Reachable(resources = "/sampler/***/**.bss") で回避できます。

実際には、本番環境で遭遇したほとんどすべての問題は、エージェントデータが古くなったことが原因でした。これは非常に煩わしくなったため、ソースの真実の隣にあり、ビルドごとに再生成されるアノテーションを介してメタデータを定義できる reachability-annotations を作成しました。

// このクラスが含まれる場合、リソースは自動的に追加されます
@Reachable(resources = { "images/*.png" })
public class ImageLoader {}

また、FXML/CSS ファイルを自動的に解析し、参照されているすべてのものに対して適切なメタデータを生成する、JavaFX 固有のアノテーションも追加しました。

@ReachableFxResources({
        "/assets/images/*.png",
        "views/**/*.fxml",
        "views/**/*.css",
})
public class MyApp extends Application {}

いくつかの注釈を使用することで、エージェントを実行することなく、すべてのプラットフォームで、すべての gluon-samples(マージされていない PR 参照)を実行できるようになります。最も複雑なアプリケーションでさえ、探索的な実行以外ではエージェントを使用しなくなりました。デフォルトでは、すべてのエントリは条件付きであるため、使用されていないビューはイメージサイズを増加させません。

残念ながら、リフレクションベースの解析ライブラリなど、ネイティブイメージで実行するのが非常に難しいサードパーティライブラリがまだいくつか存在し、より互換性のあるものに置き換える必要があるかもしれません。これは JavaFX に限定されるものではありませんが、ネイティブイメージ全般にとって依然として問題点です。

問題 1: エンジンメタデータ

アプリケーションメタデータとは異なり、エンジンメタデータは特定のバージョンとオペレーティングシステムに対して一定です。ユーザーは純粋な Java API を見ますが、内部的には OS の動作やレンダリングパイプラインを処理するための多くのネイティブコードと、実行時に適切な実装を選択するための多くのリフレクションがあります。リフレクション、JNI、およびリソース(シェーダー、CSS ファイル、フォントなど)の間で、フレームワーク全体をカバーするには、現在約 1000 件のメタデータエントリが必要です。

さらに、多くのメタデータは特定のオペレーティングシステムにのみ適用されます。プラットフォーム固有のクラス(例: WinApplication.class)の多くはプラットフォーム JAR からフィルタリングされますが、いくつかのクラス(例: プラットフォームフォント)は常に利用可能です。これらを追加すると、ネイティブイメージが不必要に肥大化し、ビルドの問題が発生する可能性があります。

メタデータはトレーシングエージェントで生成することはできません。これは、単一のアプリケーションですべての可能なパスをカバーすることが不可能だからです。シェーダーとエフェクトだけでも大規模なクラスファミリーを形成し、各実行は単一のパイプラインに限定され、一部のアクセスはクラスローディング順序にさえ依存します。したがって、このメタデータは何らかの方法でキュレーションする必要があり、理想的にはワイルドカードを使用してすべてのシェーダーリソースを一度にカバーする必要があります。

Substrate は、ダウンロードした fx JAR(META-INF/substrate/config/reflectionconfig.json)に JSON メタデータをバンドルし、ネイティブイメージに渡す前にターゲット固有のメタデータを条件付きで追加するカスタムメカニズム(例: reflectionconfig-x86_64-linux.json および enableSWRendering が設定されている場合の -javafxsw)を追加します。これらのファイルを読み取るのは Substrate だけなので、標準の native-maven-plugin を使用するとメタデータは機能しません。

Liberica NIK は JSON ファイルを配布しませんが、svm.jar 内の JavaFXFeature を大幅に拡張し、コードから直接ルールを登録します。メタデータはバンドルされた jfx バージョンに紐付けられており、条件は Platform.includedIn チェックと特定のクラスのトリガーを使用して生成されます。

StaticFX は、OS フィルタリングを近似する条件を持つ従来の JSON ファイルを配布します。例: typeReachable = ${OS}Application.class。これは標準ツールと互換性があるため、現在の GraalVM リリースでそのまま動作します。メタデータ自体は現在、jfx フォーク内の @Reachable アノテーションを使用して生成され、リリースごとに 1 回更新されます。

アノテーションをアップストリームにマージしようと検討しましたが、マージするには外部アノテーションプロセッサへの依存関係を持つビルドシステム変更が必要になり、OpenJFX がネイティブイメージを公式にサポートするという期待を生み出すことになります。これは大きなコミットメントです。初期メタデータ(約 315 件のアノテーション、約 280 件のファイルで約 1,000 件のエントリを生成)が判明した今では、新しいリリースをカバーして自動テストスイートを実行するのに数時間しかかかりません。メンテナンスの容易さを考慮して設計し、プロセスを jfx-static-libs に文書化しました。タイムリーに新しいビルドをリリースする予定ですが、サードパーティのアップデートに依存しないように、独自のビルドを作成することも可能です。現在、typeReached 条件が HeadlessApplication 条件を表現できないため、古い 1.0.0 メタデータ形式の生成に限定されています。つまり、OS 依存のコードをロードしますが、OS 条件として使用できるクラスには一切到達しません。これにより、メタデータをコミュニティメタデータリポジトリにアップロードできません。

欠落したメタデータは、実行時に非常に迷惑な方法で失敗します。失敗は、ハードクラッシュ(例: 欠落クラスでのセグメンテーション違反)から、サイレントに悪いレンダリングアーティファクトを生成する(例: BoxShadow エフェクトのピアが見つからない)まで多岐にわたります。問題を早期に検出するために、2D および 3D シーン、エフェクト、ダイアログ、ポップアップ、リッチテキストなど、可能な限り多くのコードパスを実行する検証プロジェクトを設定しました。各シーンはスクリーンショットをエクスポートし、自動ピクセルチェックを受け、手動でのサイドバイサイド検証用にエクスポートされます。実行可能ファイルは、パイプライン(-Dprism.order=d3d/es2/mtl)を切り替えて、それらの結果がすべて一致することを確認することもできます。合成テストを超えて、AtlantaFX サンプラーと Scope のような独自の内部アプリケーションもチェックしています。

問題 2: 静的ライブラリ

静的バイナリは主に配布の問題です。アップストリーム OpenJFX は、ほとんどのモジュール(メディアまたは Web は除く)に対して長年 -PSTATIC_BUILD=true を介して静的ビルドをサポートしてきましたが、結果のアーカイブを公開したことはありません。ユーザーはサードパーティベンダーに依存するか、ネイティブビルドを自分で行う必要があります。これは、バージョンが正確に一致する必要があるという事実によって複雑になります。つまり、静的ライブラリはランタイム JAR と同じコミットに基づいている必要があります。

Substrate は、Gluon 独自の静的 JavaFX SDK をダウンロードし、ネイティブイメージを呼び出す前に、ユーザー指定の JavaFX JAR をサイレントに置き換えます。これにより、JAR とアーカイブの一致が保証されますが、新しいバージョンを使用することは不可能です。JFX 27 を指定しても、ビルドツールは JFX 21 を使用します。Substrate はデフォルトでハードウェアパイプラインのみを含みますが、enableSWRendering フラグを介してソフトウェアフォールバックを含めるオプションを提供します。

Liberica NIK は、JDK に JavaFX モジュールをバンドルすることでバージョンを修正します。これは、ビルドで宣言された org.openjfx JAR よりも優先されます。LTS リリースをサポートしているため、最新は現在 JDK 25 + JavaFX 25 です。彼らの JavaFX ビルドには、公開されていないいくつかのマイナーなソースパッチが含まれていることに注意してください。彼らの機能はソフトウェアパイプラインを省略するため、アプリケーションは適切な GPU がないマシンでは失敗します。

StaticFX は、公式の org.openjfx リリース JAR と連携し、同じコミットからビルドされた対応する静的ビルドを提供します。ビルドは現在、私の openjfx フォークである jfx にあり、アノテーション以外には Java またはネイティブコードに変更はありません。各リリースには、アノテーションプロセッサを設定するための 1 つのコミットと、メタデータ生成用のアノテーションを追加する 2 番目のコミットが追加されます。出力形式は Substrate のものと一致するため、カスタムダウンロード URL を受け入れるようになれば互換性があります。静的アーカイブは実際には比較的サイズが小さいため、Maven Central の成果物として、5 つのプラットフォーム(linux-aarch64 を含む)すべてとメタデータを、分類子なしの単一の約 10 MB の JAR にバンドルします。アーカイブは jfx-static-feature によってのみ使用されるため、成果物はクラスパス上の到達可能性メタデータを追加するためだけのスタンドアロン依存関係として使用できます。

問題 3: 統合グルー

最後に、実際のリンク引数の追加と、静的リンク中に発生するいくつかの落とし穴を修正するために必要なグルーが必要です。

Substrate はビルドチェーン全体を所有しているため、すべてのリンカー引数はビルドツール内にあります。

Liberica NIK は GraalVM の標準 native-maven-plugin で動作しますが、svm.jar 内に大幅に変更された JavaFXFeature を出荷し、バンドルされた JavaFX バージョンのライブラリとメタデータを登録します。

StaticFX は、標準ビルドツールとソースコードの変更なしで動作するように設計されたため、統合グルーはカスタム Feature として実装する必要があります。Feature はクラスパスから自動的に検出され、プラットフォーム固有のアーカイブを抽出し、バージョンが一致することを確認し、静的ライブラリとそのシステム依存関係に必要なリンカーコマンドを追加します。

静的リンク

静的にリンクされた JNI コードは、2 つの方法でイメージに組み込むことができます。簡単な方法は、アーカイブ全体を実行可能ファイルに強制することです。例えば、Windows では /WHOLEARCHIVE、macOS では -force_load、Linux では --whole-archive を使用します。Java_* エントリポイントはエクスポートされたシンボルであるため、実行可能ファイル自体のエクスポートテーブルに含まれ、イメージは静的にリンクされた JNI に対して JVM が使用するのと同じルックアップで実行時にそれらを解決します(JEP 178)。これにより、ビルド時の検証が無効になり、使用されるかどうかにかかわらず、すべてのオブジェクトファイルが含まれます。

代替案は、アーカイブを組み込み JNI ライブラリとして登録することです。これは GraalVM が JDK 自体のライブラリに使用するのと同じメカニズムです。Native-image は、到達可能なネイティブメソッドごとにリンク時参照を発行するため、リンカーは実際に必要なオブジェクトのみを取り込みます。検証はビルド時に行われるため、実際に欠落している実装は早期に失敗します。

歴史的に、OpenJFX の glass ライブラリは低い JNI バージョンを報告していました。GraalVM 25 は静的リンクに対して JEP 178 の最小 JNI_VERSION_1_8 を厳密に強制するため、公式ソースはこれがアップストリームで更新されるまで互換性がありません。

Substrate は Windows と macOS で全体アーカイブを強制しますが、Linux では内部の GluonFeature が組み込み登録を行います。サポートされている最高の GraalVM バージョンは 23 なので、JNI バージョンは問題ありません。

Liberica NIK は、すべてのプラットフォームで組み込み登録を行います。バンドルされた glass ビルドは、JNI_VERSION_1_8 を報告するようにパッチされています。

StaticFX もすべてのプラットフォームで組み込み登録を行い、GraalVM の置換を使用して、実際のソースを変更せずに互換性を確保するために呼び出しサイトをパッチします。

欠落シンボルのリンク

メタデータセクションで述べたように、一部のプラットフォーム固有のクラスは、ネイティブコードが存在しない場合でも、すべてのプラットフォームの JAR に存在します。ネイティブメソッドを宣言する 95 個のグラフィックスクラスのうち、これはフォントレンダリングに関連する 6 つのクラスと iOS 画像ローダーにのみ適用されます。

メタデータが完全に提供されていても、ユーザーは、例えばエージェント生成ファイルをコミットして別のプラットフォームでビルドするなどの方法で、簡単に問題を引き起こす可能性があります。これらのクラスがアップストリームの除外リストに追加されるまで、例えば macOS の CoreText フォントバックエンドを Windows で到達可能にすると、それらのいずれも必要とされないにもかかわらず、55 個の未解決シンボルでビルドが失敗します。欠落シンボルエラーは、メタデータの問題を強調するのではなく、バイナリが正しくコンパイルされなかったという印象を与えます。

helloworld-graal.obj : error LNK2001: unresolved external symbol
                       Java_com_sun_javafx_font_coretext_OS_CFArrayGetCount
[... 54 more unresolved CoreText symbols ...]
helloworld-graal.exe : fatal error LNK1120: 55 unresolved externals

1 つの可能な解決策は、Substrate がモバイルターゲットのいくつかの欠落 JDK シンボルに対して行うように、欠落しているシンボルを小さな C スタブでカバーすることです。しかし、フォントバックエンドだけでもプラットフォームごとに数十個のスタブが必要になり、アーカイブと一緒にバンドルして保守する必要があります。

Substrate の全体アーカイブリンクは、Windows と macOS では問題を回避します。間違った OS のクラスは、JVM がそれらを許容するのと同じように許容され、呼び出されない限り、イメージの肥大化しか追加しません。実際に欠落している実装は、UnsatisfiedLinkError として最初の呼び出しで表示されるだけです。

java.lang.UnsatisfiedLinkError: Can't load library: javafx_font_pango
java.library.path = [...]

Linux では、GluonFeature はキュレートされたプレフィックスリストを登録し、間違った OS のフォントバックエンドを除外するため、それらのシンボルは参照されません。

Liberica NIK は、各プラットフォームのキュレートされたプレフィックスリストを登録するために JavaFXFeature を使用するため、ネイティブメソッドはリンク時にバインドされます。悪いメタデータによってイメージに入り込んだ間違った OS のクラスは、通常の JNI として扱われるため、イメージの肥大化を追加しますが、ビルドを壊すことはありません。

StaticFX は、結合されたプレフィックスリストを登録し、GraalVM の @Delete 置換を使用して間違った OS のクラスを削除します。それらを登録しないでおくと、単に実行時に失敗する通常の JNI にダウングレードされるだけです。削除されたクラスは分析から完全に削除されるため、悪いメタデータはリンカーに到達せず、コードパスが削除されたクラスにヒットすると、UnsatisfiedLinkError ではなく、より意味のある UnsupportedFeatureError で失敗します。

@Delete
@TargetClass(onlyWith = {NotLinux.class, ClassPresent.class},
    className = "com.sun.javafx.font.freetype.FTFactory"
)
static final class Target_FTFactory {}

条件は否定(NotLinux、NotMacOS)と存在チェック(ClassPresent)の組み合わせであるため、将来のプラットフォームポートは、スタブ化されたバックエンドを実行するのではなく、視覚的に失敗します。

メディア、WebView、および Swing

Substrate と Liberica NIK の両方で、メディアおよび Web モジュールは省略されています。これは、それらのネイティブ依存関係(GStreamer と WebKit)を静的にビルドするのが非常に困難だからです。これはおそらく今後も変わらないため、モジュールを完全にドロップするのは私たちにとって不必要に制限的すぎると考えました。

標準の JVM では、これらのモジュールは既にプラットフォーム JAR からバイナリを動的にロードしており、ネイティブイメージが同じことを行うのを妨げるものはありません。Feature は、クラスパスから一致する共有ライブラリ(.dll、.so、または .dylib)を出力ディレクトリに抽出し、適切なリンカー引数を追加するだけです。Web またはメディアを必要とするアプリケーションは、自己完結型の実行可能ファイルとしてビルドすることはできませんが、これは妥当なトレードオフのようです。

私たちの javafx.swing メタデータは、純粋な Java 相互運用グルーをカバーしています。完全な SwingNode の利用には、JDK の内部 AWT システムに対する追加のメタデータが必要ですが、2 つの @Reachable アノテーションのみを使用して 5 つのプラットフォームすべてで動作することを検証することに成功しました。ただし、これは積極的にサポートしているというよりは例として残っています。

残りのグルー

削除以外にも、StaticFX には、GraalVM と静的にリンクされた JavaFX の間の残りのギャップを回避する少数の置換が含まれています。それらのいくつかは潜在的な問題であり、JVM では表示されませんが、アップストリームで修正されるべきです。Gluon と NIK がそれらのいくつかをどのように処理するかを調べただけです。

GraalVM

  • オーバーロードされたネイティブメソッドはリンクされません。native-image は組み込み JNI メソッドを短い名前で解決しますが、オーバーロードされたネイティブは C 側ではそのシグネチャでマングリングされた名前の下でのみ存在します。これは JNI 仕様で要求されています(3 つのクラスに関連)。手動でマングリングされたシンボルにルーティングするために @CFunction 置換を追加しました。BellSoft は、オーバーロードされたネイティブの名前を変更する(例: CreateFontFace → CreateFontFaceIndexed)ソースパッチでこれを回避しており、Substrate は Windows と macOS での遅延解決(実行時にマングリングされた名前にフォールバックする)により影響を受けず、Linux ではこれらのクラスは関連性がありません。

  • GraalVM の組み込み JavaFX サポートは Application サブクラスをリフレクション用に登録しますが、ランチャーによって呼び出される引数なしコンストラクタは登録しません。JavaFX ネイティブイメージは現在、アプリケーションが自身を登録しない限り、NoSuchMethodException: MyApp.<init>() で起動時に失敗します。そのため、Feature は到達可能な各サブクラスのコンストラクタを登録します。

OpenJFX

  • System.loadLibrary は、静的 JavaFX ライブラリのいずれも初期化できません。パッチされていない glass を組み込みライブラリとしてロードすると、UnsatisfiedLinkError: Unsupported JNI version 0x10006, required by glass で失敗します。Linux と macOS では、JNI_OnLoad_<lib> エントリポイントは、イメージのエクスポートシンボルリストによってさらに隠されているため、Feature は初期化子を直接呼び出します。

  • Objective-C カテゴリは静的リンクから削除されます(標準のリンカー動作。カテゴリはリンカーが依存関係として追跡するシンボルを生成しないため)。これは、最初のウィンドウが開いたときに unrecognized selector 例外として表示されます。libglass.a は、それらを保持するために -force_load でリンクされます。

  • macOS では、最初のウィンドウが表示される前にプロセスがハングします。Cocoa は、glass が起動している間、プロセスの最初のスレッドが CFRunLoop を実行していることを必要とします。java ランチャーはこれを処理しますが、ネイティブイメージは最初のスレッドで main を実行します。これは Application::launch をカバーするためにハンドオフを置換できますが、Platform::startup は現在、外部のネイティブランチャーが必要です。

  • JavaFX は、Broadcom を含まないベンダーの許可リストに対して GPU サポートを照合するため、Raspberry Pi 上の es2 パイプラインは、ソフトウェアレンダリングへのフォールバックを回避するために -Dprism.forceGPU=true が必要です。

  • イメージ内に配布されるファイルは resource: スキームを使用してロードされますが、macOS の Media Player は jar: と jrt: のみをチェックし、URL を AVFoundation に渡してエラーを取得します。

ベンチマーク

アプリケーションサイズ

StaticFX で生成され、サイズ最適化された最小限のアプリケーションは約 33 MB(ZIP 圧縮で 12 MB)になります。これは、最小限のデフォルト jlink ランタイムの 98 MB(ZIP 圧縮で 32 MB)の約 3 分の 1、最大圧縮(--compress zip-9)を使用した 59 MB よりも 43% 小さいサイズです。

以下の表は、すべての 3 つのアプローチでサポートされている最も高いバージョンでの最小サイズを示しています。数値は、異なる JavaFX バージョンと GraalVM バージョン間での最適化の違いにより、直接比較できないことに注意してください。

表 3. HelloFX - サポートされている最も高いバージョンでの最小アプリケーションサイズ

HelloFX

Gluon SubstrateLiberica NIKStaticFX
GraalVMGluon 23 CELiberica 25 CEOracle 25.3
JavaFX212527
デフォルト最適化 (-O2)、生および ZIP 圧縮
Windows x86_6464.5 MB<br>23.3 MB45.3 MB<br>15.1 MB45.9 MB<br>18.0 MB
Linux x86_6469.8 MB<br>24.6 MB47.1 MB<br>15.4 MB52.0 MB<br>19.6 MB
macOS aarch6465.9 MB<br>24.0 MB68.6 MB<br>24.3 MB46.0 MB<br>18.0 MB
サイズ最適化 (-Os)、生および ZIP 圧縮
Windows x86_6460.1 MB<br>21.9 MB36.5 MB<br>12.7 MB30.8 MB<br>11.7 MB
Linux x86_6465.8 MB<br>23.0 MB38.4 MB<br>12.6 MB33.4 MB<br>12.1 MB
macOS aarch6461.8 MB<br>22.5 MB57.5 MB<br>20.3 MB32.3 MB<br>11.9 MB
レスポンシブネス

ほとんどの違いは最初の印象にあります。デスクトップでは、JIT は最終的に追いつきますが、起動と各ページの初回アクセスは、クラスロードとインタプリタを経由します。JavaFX 27 の AtlantaFX サンプラーを、高速デスクトップと Raspberry Pi 5 上で、jlink、トレーニング済みの CDS アーカイブ付き jlink、およびネイティブイメージとして測定しました。

表 4. 初回ウィンドウ表示時間、およびページロード(初回 → 2 回目アクセス)(時間はミリ秒)

AtlantaFX サンプラー

jlinkjlink + CDSネイティブイメージ
Ryzen 9 9950X (D3D)
初回ウィンドウ表示時間15581269501
Overview (FXML)402 → 102315 → 103100 → 52
HTMLEditor (WebView)852 → 34819 → 3434 → ~20
Typography99 → 34100 → ~2050 → 34
Raspberry Pi 5 (SW)
初回ウィンドウ表示時間35662655495
Overview (FXML)1901 → 7681500 → 661324 → 250
HTMLEditor (WebView)2388 → 2692309 → 262224 → 201
Typography397 → 302422 → 302262 → 241

初期クラスロード後、JIT はデスクトップではほとんどのギャップを埋めますが、Pi は AOT 下で顕著に高速なままです。

FXML は、複数回の呼び出し後でも AOT 下で大幅に高速にロードされることに注意してください。私たち自身のアプリケーションは、リフレクションでロードされる複雑なネストされた FXML ファイルでほぼ構成されています。過去にはパフォーマンス向上のために FXML をバイトコードに変換することを検討していましたが、Native Image によりそれが不要になりました。

各マシンはデフォルトのパイプラインで測定されました。つまり、デスクトップでは D3D、Pi 5 ではソフトウェアパイプラインです(V3D GPU はベンダーの許可リストに含まれていません)。ネイティブイメージは PGO なしのデフォルトのシリアル GC を使用し、CDS アーカイブは測定されたすべてのページでトレーニングされました。タイミングは外部画面キャプチャから取得されます。ページロードはクリックからコンテンツが表示され始めるまでの時間をカウントし、デスクトップでは 16 ms、Pi では約 50 ms のキャプチャ解像度で 3 回実行の中央値を取ります。~20 ms のエントリは、クリック後の最初のキャプチャで既に切り替わっていました。

結論

JavaFX が Native Image とどのように相互作用するか、そして潜在的なエラーのいくつかが何を意味するのかが、より明確になったことを願っています。Native Image は、HelloWorld のようなおもちゃの例にしか機能しないと不平を言う人をよく見かけますが、トレーシングエージェントなしで AtlantaFX サンプラーを使用することが、複雑なアプリケーションを確実に実行できる能力を十分に示していることを願っています。Gluon が OpenJDK Mobile に移行していることは知っていますが、ユーザー PC にデプロイされるデスクトップアプリには、Leyden の AOT キャッシュよりも、完全にコンパイルされたネイティブイメージの方が適していると今でも考えています。

デスクトップアプリケーションを初めて AOT コンパイルするのは依然として困難ですが、私の意見では、最初の障壁を乗り越える価値はあります。最も大きな問題は、一般的な JavaFX ライブラリとそのアプリケーション自体のメタデータが不足していることですが、FXML サポートを備えた私たちの reachability-annotations が、それをより扱いやすく、保守しやすくすることを願っています。実質的に既存のすべてのコンポーネントを使用する AtlantaFX サンプラーでさえ、完全な互換性にはいくつかの注釈しか必要ありませんでした。

もう 1 つの残りの問題は、ネイティブイメージのクローズドワールド仮定とうまく連携しない(古い)サードパーティライブラリへの依存です。エコシステムはより互換性のあるライブラリに向かって動いていますが、ライブラリの切り替えは常に面倒です。プロジェクト Crema とそのオープンワールドモデルがどのように役立つかを見ていくことになります。

この記事は、著者の CC BY 4.0 ライセンスの下で提供されています。