Building JavaFX app native image with GraalVM: New achievement unlocked!

Loïc Lefèvre
db-one
Published in
8 min readApr 5, 2024

--

JavaFX application native executable built using Oracle GraalVM.

TL;DR — Building JavaFX application native image used to require GluonFX plugins and GluonHQ substrate VM. In this post, I show it is possible to rely on the latest GraalVM distribution only. This simplifies greatly the process but requires some challenges to be solved first… are you ready to read my journey?

Background

When developing JavaFX applications, releasing it seems the way to go. Up to now, I’ve been relying on Jlink to do it but then all the Java archive files (*.jar) are available including the resources inside it or the code to be decompiled.

Being able to release a native executable (aka image) presents multiple advantages:

  • end-user experience is better: no need to manage JVM locally
  • startup time is reduced
  • memory consumption is reduced
  • looks like a native executable
  • resources are protected (somewhat)
  • code can’t be decompiled (easily)

GraalVM Native Image

Basically, it is created using the native-image[.cmd] CLI. Now in the context of JavaFX applications, there are some topics to take care of:

  • Toolchain management
  • Maven plugin and IDE environment variables
  • JavaFX and Java shared libraries
  • Configure native image with the tracing agent
  • JavaFX application resources: CSS, PNG, JPG, FXML, etc.
  • JavaFX native platform resources: shaders (drawing, visual effects)
  • Java modules (JavaFX native image requires it) and Maven dependencies management

I’ve chosen to make the demonstration using the Windows 11 operating system… so let’s go!

Toolchain management

On Windows, you’ll first need to install Visual Studio 2022 Build Tools and the Windows SDK. For more information, check out this post from Olga Gupalo.

The thing to remember: before running the %GRAALVM_HOME%/bin/native-image.cmd script, you’ll need to set up (a lot of) environment variables. This can be accomplished by running the following script for an x86_64 architecture:

call %VISUAL_STUDIO_FOLDER%\VC\Auxiliary\Build\vcvars64.bat

There are other scripts inside this folder that can be used according to your needs. Again, this is a prerequisite!

Maven plugin and IDE environment variables

For building native images, you can use the Maven plugin (there is one for Gradle as well):

<plugin>
<groupId>org.graalvm.buildtools</groupId>
<artifactId>native-maven-plugin</artifactId>
<version>0.10.1</version>
...

Now one of the things that isn’t obvious is, given you use IntelliJ IDEA as your IDE: How do you set up environment variables from the vcvars64.bat script from inside your IDE so that the Maven plugin which runs the native-image.cmd script will know how to access the compiler, the linker, the libraries, etc?

Answer for IntelliJ IDEA: configure your project .idea/workspace.xml file.

Example:

 <component name="MavenRunner">
<option name="environmentProperties">
<map>
<entry key="CommandPromptType" value="Native" />
<entry key="DevEnvDir" value="c:\dev\MicrosoftVisualStudio\Common7\IDE\" />
<entry key="EXTERNAL_INCLUDE" value="c:\dev\MicrosoftVisualStudio\VC\Tools\MSVC\14.38.33130\include;c:\dev\MicrosoftVisualStudio\VC\Tools\MSVC\14.38.33130\ATLMFC\include;c:\dev\MicrosoftVisualStudio\VC\Auxiliary\VS\include;C:\Program Files (x86)\Windows Kits\10\include\10.0.22621.0\ucrt;C:\Program Files (x86)\Windows Kits\10\\include\10.0.22621.0\\um;C:\Program Files (x86)\Windows Kits\10\\include\10.0.22621.0\\shared;C:\Program Files (x86)\Windows Kits\10\\include\10.0.22621.0\\winrt;C:\Program Files (x86)\Windows Kits\10\\include\10.0.22621.0\\cppwinrt;C:\Program Files (x86)\Windows Kits\NETFXSDK\4.8\include\um" />
<entry key="ExtensionSdkDir" value="C:\Program Files (x86)\Microsoft SDKs\Windows Kits\10\ExtensionSDKs" />
<entry key="FSHARPINSTALLDIR" value="c:\dev\MicrosoftVisualStudio\Common7\IDE\CommonExtensions\Microsoft\FSharp\Tools" />
<entry key="Framework40Version" value="v4.0" />
<entry key="FrameworkDir" value="C:\Windows\Microsoft.NET\Framework64\" />
<entry key="FrameworkDir64" value="C:\Windows\Microsoft.NET\Framework64\" />
<entry key="FrameworkVersion" value="v4.0.30319" />
<entry key="FrameworkVersion64" value="v4.0.30319" />
<entry key="HTMLHelpDir" value="C:\Program Files (x86)\HTML Help Workshop" />
<entry key="IFCPATH" value="c:\dev\MicrosoftVisualStudio\VC\Tools\MSVC\14.38.33130\ifc\x64" />
<entry key="INCLUDE" value="c:\dev\MicrosoftVisualStudio\VC\Tools\MSVC\14.38.33130\include;c:\dev\MicrosoftVisualStudio\VC\Tools\MSVC\14.38.33130\ATLMFC\include;c:\dev\MicrosoftVisualStudio\VC\Auxiliary\VS\include;C:\Program Files (x86)\Windows Kits\10\include\10.0.22621.0\ucrt;C:\Program Files (x86)\Windows Kits\10\\include\10.0.22621.0\\um;C:\Program Files (x86)\Windows Kits\10\\include\10.0.22621.0\\shared;C:\Program Files (x86)\Windows Kits\10\\include\10.0.22621.0\\winrt;C:\Program Files (x86)\Windows Kits\10\\include\10.0.22621.0\\cppwinrt;C:\Program Files (x86)\Windows Kits\NETFXSDK\4.8\include\um;" />
<entry key="LIB" value="c:\dev\MicrosoftVisualStudio\VC\Tools\MSVC\14.38.33130\ATLMFC\lib\x64;c:\dev\MicrosoftVisualStudio\VC\Tools\MSVC\14.38.33130\lib\x64;C:\Program Files (x86)\Windows Kits\NETFXSDK\4.8\lib\um\x64;C:\Program Files (x86)\Windows Kits\10\lib\10.0.22621.0\ucrt\x64;C:\Program Files (x86)\Windows Kits\10\\lib\10.0.22621.0\\um\x64;" />
<entry key="LIBPATH" value="c:\dev\MicrosoftVisualStudio\VC\Tools\MSVC\14.38.33130\ATLMFC\lib\x64;c:\dev\MicrosoftVisualStudio\VC\Tools\MSVC\14.38.33130\lib\x64;c:\dev\MicrosoftVisualStudio\VC\Tools\MSVC\14.38.33130\lib\x86\store\references;C:\Program Files (x86)\Windows Kits\10\UnionMetadata\10.0.22621.0;C:\Program Files (x86)\Windows Kits\10\References\10.0.22621.0;C:\Windows\Microsoft.NET\Framework64\v4.0.30319" />
<entry key="NETFXSDKDir" value="C:\Program Files (x86)\Windows Kits\NETFXSDK\4.8\" />
<entry key="Path" value="c:\dev\MicrosoftVisualStudio\VC\Tools\MSVC\14.38.33130\bin\HostX64\x64;c:\dev\MicrosoftVisualStudio\Common7\IDE\VC\VCPackages;c:\dev\MicrosoftVisualStudio\Common7\IDE\CommonExtensions\Microsoft\TestWindow;c:\dev\MicrosoftVisualStudio\Common7\IDE\CommonExtensions\Microsoft\TeamFoundation\Team Explorer;c:\dev\MicrosoftVisualStudio\MSBuild\Current\bin\Roslyn;c:\dev\MicrosoftVisualStudio\Team Tools\Performance Tools\x64;c:\dev\MicrosoftVisualStudio\Team Tools\Performance Tools;C:\Program Files (x86)\Microsoft SDKs\Windows\v10.0A\bin\NETFX 4.8 Tools\x64\;C:\Program Files (x86)\HTML Help Workshop;c:\dev\MicrosoftVisualStudio\Common7\IDE\CommonExtensions\Microsoft\FSharp\Tools;c:\dev\MicrosoftVisualStudio\Team Tools\DiagnosticsHub\Collector;C:\Program Files (x86)\Windows Kits\10\bin\10.0.22621.0\\x64;C:\Program Files (x86)\Windows Kits\10\bin\\x64;c:\dev\MicrosoftVisualStudio\\MSBuild\Current\Bin\amd64;C:\Windows\Microsoft.NET\Framework64\v4.0.30319;c:\dev\MicrosoftVisualStudio\Common7\IDE\;c:\dev\MicrosoftVisualStudio\Common7\Tools\;C:\WINDOWS\system32;C:\WINDOWS;C:\WINDOWS\System32\Wbem;C:\WINDOWS\System32\OpenSSH\;C:\Program Files\dotnet\;c:\dev\MicrosoftVisualStudio\VC\Tools\Llvm\x64\bin;c:\dev\MicrosoftVisualStudio\Common7\IDE\CommonExtensions\Microsoft\CMake\CMake\bin;c:\dev\MicrosoftVisualStudio\Common7\IDE\CommonExtensions\Microsoft\CMake\Ninja;c:\dev\MicrosoftVisualStudio\Common7\IDE\VC\Linux\bin\ConnectionManagerExe;c:\dev\MicrosoftVisualStudio\VC\vcpkg" />
<entry key="UCRTVersion" value="10.0.22621.0" />
<entry key="UniversalCRTSdkDir" value="C:\Program Files (x86)\Windows Kits\10\" />
<entry key="VCIDEInstallDir" value="c:\dev\MicrosoftVisualStudio\Common7\IDE\VC\" />
<entry key="VCINSTALLDIR" value="c:\dev\MicrosoftVisualStudio\VC\" />
<entry key="VCPKG_ROOT" value="c:\dev\MicrosoftVisualStudio\VC\vcpkg" />
<entry key="VCToolsInstallDir" value="c:\dev\MicrosoftVisualStudio\VC\Tools\MSVC\14.38.33130\" />
<entry key="VCToolsRedistDir" value="c:\dev\MicrosoftVisualStudio\VC\Redist\MSVC\14.38.33135\" />
<entry key="VCToolsVersion" value="14.38.33130" />
<entry key="VS170COMNTOOLS" value="c:\dev\MicrosoftVisualStudio\Common7\Tools\" />
<entry key="VSCMD_ARG_HOST_ARCH" value="x64" />
<entry key="VSCMD_ARG_TGT_ARCH" value="x64" />
<entry key="VSCMD_ARG_app_plat" value="Desktop" />
<entry key="VSCMD_VER" value="17.8.6" />
<entry key="VSINSTALLDIR" value="c:\dev\MicrosoftVisualStudio\" />
<entry key="VSSDK150INSTALL" value="c:\dev\MicrosoftVisualStudio\VSSDK" />
<entry key="VSSDKINSTALL" value="c:\dev\MicrosoftVisualStudio\VSSDK" />
<entry key="VisualStudioVersion" value="17.0" />
<entry key="WindowsLibPath" value="C:\Program Files (x86)\Windows Kits\10\UnionMetadata\10.0.22621.0;C:\Program Files (x86)\Windows Kits\10\References\10.0.22621.0" />
<entry key="WindowsSDKLibVersion" value="10.0.22621.0\" />
<entry key="WindowsSDKVersion" value="10.0.22621.0\" />
<entry key="WindowsSDK_ExecutablePath_x64" value="C:\Program Files (x86)\Microsoft SDKs\Windows\v10.0A\bin\NETFX 4.8 Tools\x64\" />
<entry key="WindowsSDK_ExecutablePath_x86" value="C:\Program Files (x86)\Microsoft SDKs\Windows\v10.0A\bin\NETFX 4.8 Tools\" />
<entry key="WindowsSdkBinPath" value="C:\Program Files (x86)\Windows Kits\10\bin\" />
<entry key="WindowsSdkDir" value="C:\Program Files (x86)\Windows Kits\10\" />
<entry key="WindowsSdkVerBinPath" value="C:\Program Files (x86)\Windows Kits\10\bin\10.0.22621.0\" />
<entry key="__DOTNET_ADD_64BIT" value="1" />
<entry key="__DOTNET_PREFERRED_BITNESS" value="64" />
<entry key="__VSCMD_PREINIT_INCLUDE" value="" />
<entry key="__VSCMD_PREINIT_PATH" value="C:\windows\system32;C:\windows;C:\windows\System32\Wbem;C:\windows\System32\OpenSSH\;C:\Program Files\dotnet\" />
<entry key="is_x64_arch" value="true" />
</map>
</option>
</component>

You can compute it using the set command twice, before executing the vcvars64.bat script and after; so that you can diff the outputs and extract the values to put inside the .idea/workspace.xml file.

JavaFX and Java shared libraries

Some shared libraries will be required when creating a native image. The list will depend on your Operating System. For Windows 11, using the Oracle GraalVM with JDK 22 along with JavaFX 22 SDK, the list will be:

awt.dll
fontmanager.dll
freetype.dll
instrument.dll
jaas.dll
javaaccessbridge.dll
jawt.dll
lcms.dll
w2k_lsa_auth.dll
api-ms-win-core-heap-l1-1-0.dll
api-ms-win-core-interlocked-l1-1-0.dll
api-ms-win-core-libraryloader-l1-1-0.dll
api-ms-win-core-localization-l1-2-0.dll
api-ms-win-core-memory-l1-1-0.dll
api-ms-win-core-namedpipe-l1-1-0.dll
api-ms-win-core-processenvironment-l1-1-0.dll
api-ms-win-core-processthreads-l1-1-0.dll
api-ms-win-core-processthreads-l1-1-1.dll
api-ms-win-core-profile-l1-1-0.dll
api-ms-win-core-rtlsupport-l1-1-0.dll
api-ms-win-core-string-l1-1-0.dll
api-ms-win-core-synch-l1-1-0.dll
api-ms-win-core-synch-l1-2-0.dll
api-ms-win-core-sysinfo-l1-1-0.dll
api-ms-win-core-timezone-l1-1-0.dll
api-ms-win-core-util-l1-1-0.dll
api-ms-win-crt-conio-l1-1-0.dll
api-ms-win-crt-convert-l1-1-0.dll
api-ms-win-crt-environment-l1-1-0.dll
api-ms-win-crt-filesystem-l1-1-0.dll
api-ms-win-core-handle-l1-1-0.dll
api-ms-win-crt-locale-l1-1-0.dll
api-ms-win-crt-math-l1-1-0.dll
api-ms-win-crt-multibyte-l1-1-0.dll
api-ms-win-crt-private-l1-1-0.dll
api-ms-win-crt-process-l1-1-0.dll
api-ms-win-crt-runtime-l1-1-0.dll
api-ms-win-crt-stdio-l1-1-0.dll
api-ms-win-crt-string-l1-1-0.dll
api-ms-win-crt-time-l1-1-0.dll
api-ms-win-crt-utility-l1-1-0.dll
api-ms-win-core-file-l2-1-0.dll
decora_sse.dll
api-ms-win-core-file-l1-2-0.dll
api-ms-win-core-file-l1-1-0.dll
fxplugins.dll
glass.dll
glib-lite.dll
gstreamer-lite.dll
api-ms-win-core-errorhandling-l1-1-0.dll
api-ms-win-core-debug-l1-1-0.dll
vcruntime140_1.dll
api-ms-win-core-datetime-l1-1-0.dll
javafx_font.dll
javafx_iio.dll
api-ms-win-core-console-l1-2-0.dll
jfxmedia.dll
jfxwebkit.dll
vcruntime140.dll
api-ms-win-crt-heap-l1-1-0.dll
msvcp140.dll
msvcp140_1.dll
msvcp140_2.dll
prism_common.dll
prism_d3d.dll
prism_sw.dll
ucrtbase.dll
api-ms-win-core-console-l1-1-0.dll
java.dll
jvm.dll

These DLLs (Dynamic Load Libraries) originate from 2 places:

  • GraalVM: ./bin/*.dll
  • JavaFX SDK: ./bin/*.dll

For the final distribution of the application, you can create a folder that will contain these shared libraries.

What about static libraries?

Good question! Although they are provided with the GraalVM JDK (check ./lib/static/<your OS and architecture>/), this is not the case for JavaFX SDK. GluonHQ with their substrate VM provides them and that’s one of their added value.

But in case you really need them, there is a (manual) way to recreate them from the shared libraries themselves. It would require (on Windows) to “play” with dumpbin and lib tools. You can check this post which explains the process although you may want to not mention the /machine flag and let the tool detect automatically the right machine architecture at the cost of a warning. (also this is available in a StackOverflow question without the details about the content of the rewritten .DEF file)

I’ve preferred to use the shared libraries, as they avoid this tedious manual step (as of now).

Configure native image with the tracing agent

This step is critical when dealing with JavaFX applications as specific metadata for reflection, JNI, and resources must be provided for building the native image.

Basically, you want to run your application using your IDE Run command with the tracing agent enabled for each new JavaFX feature you are adding to your application (and ensure to test those right away). Example: I add a new Button in the application: I need to run the application and click on that Button.

You can use this switch to run your JavaFX application with the tracing agent:

java -agentlib:native-image-agent=config-merge-dir=src/main/resources/META-INF/native-image ...

More information can be found inside the GraalVM documentation.

When running the application with the tracing agent, new JSON files will be created in the specified folder:

jni-config.json
predefined-classes-config.json
proxy-config.json
reflect-config.json
resource-config.json
serialization-config.json

JavaFX application resources: CSS, PNG, JPG, FXML, etc.

Among the files generated by the tracing agent, the resource-config.json file is where you’ll need to specify the application resource files used such as the FXML, the CSS, and the Images. Indeed, it looks like the tracing agent is not smart enough yet to discover files loaded by other files (e.g. loading an image referenced by a CSS being loaded from an FXML file).

As such, adding them manually was the only solution for me:

{
"resources":{
"includes":[{
"pattern":"com/oracle/connect/.*\\.png"
}, {
"pattern":"com/oracle/connect/.*\\.css"
}, {
"pattern":"com/oracle/connect/.*\\.fxml"
},
...

Debugging native images with Graphical User Interface

It may appear that when you run such a native application, there is nothing happening because the standard output may not be managed properly (no logging into a file.log, etc.). One quick way to check if there is an exception being thrown is to redirect the standard error output stream into a file:

nativeJavaFX.exe 2> error.txt

JavaFX native platform resources: shaders (drawing, visual effects)

Another critical step not to miss is related this time to the resources being used by JavaFX: the shaders. The JavaFX shaders are embedded inside the *.jar files for targeted platforms. They have a .obj file extension and they are loaded dynamically when the GUI is built. If these shaders are not present within the native image, then nothing will be displayed.

Hence these shader resources must be referenced in the resource-config.json:

... 
{
"pattern":"com/sun/prism/d3d/hlsl/.*\\.obj"
}, {
"pattern":"com/sun/scenario/effect/impl/hw/d3d/hlsl/.*\\.obj"
}
...

In the same spirit, the shader loaders have to be referenced in the reflect-config.json file:

{
"name":"com.sun.prism.shader.FillPgram_Color_Loader",
"allDeclaredFields":true,
"allPublicFields":true,
"allDeclaredMethods":true,
"allPublicMethods":true,
"allDeclaredConstructors":true,
"allPublicConstructors":true,
"methods":[{"name":"loadShader","parameterTypes":["com.sun.prism.ps.ShaderFactory","java.io.InputStream"] }]
},
{
"name":"com.sun.prism.shader.FillPgram_ImagePattern_Loader",
"allDeclaredFields":true,
"allPublicFields":true,
"allDeclaredMethods":true,
"allPublicMethods":true,
"allDeclaredConstructors":true,
"allPublicConstructors":true,
"methods":[{"name":"loadShader","parameterTypes":["com.sun.prism.ps.ShaderFactory","java.io.InputStream"] }]
},
{
"name":"com.sun.prism.shader.FillPgram_LinearGradient_PAD_Loader",
...

Java modules (JavaFX native image requires it) and Maven dependencies management

The very last issue you may encounter is related to the Java 9 Platform Module System (aka JPMS). If you put all the dependencies on the classpath, then starting the native image will result in the following warning:

Unsupported JavaFX configuration: classes were loaded from 'unnamed module @XXXXXXXX'
Warning when starting the native image: JavaFX requires the module system.

The only solution I found to solve this last issue was to invoke native-image[.cmd] using my own arguments file (with all the parameters) and not rely on the current GraalVM native image Maven plugin. Using this approach allowed me to properly specify the JavaFX dependencies on the module path and other dependencies on the classpath. Also, It allowed me to use the right module/class as the application entry point.

Native App without the console on Windows

So far, this console remains when creating the native image. Although this can help with debugging, it is not yet the native behavior of the JavaFX apps we want.

To remove this console, one can edit the native image and configure it to have a Windows sub-system /SUBSYSTEM:WINDOWS:

editbin /SUBSYSTEM:WINDOWS target\distrib\connectme.exe

(under progress…)

Final note

If your experience is different and if you have interesting advice, please don’t hesitate to share them inside comments.

--

--