A summary of my lessons learned converting an existing JavaFX application to a native,
AOT compiled, real-world application using the Gluon Client Maven plugin which is based
on the GraalVM native-image
toolchain.
1. Overview
First of all I would like to thank the team at Gluon which made all the following possible. Without their continuous, hard work it would not be possible at all to compile a modern JavaFX application into a native platform application. My thanks of course also include all the people involved in the GraalVM project which laid the foundation for the work done for JavaFX by Gluon.
This article summarizes the experiences which I collected while converting one of my existing Java applications to a native application using the Gluon Client Maven plugin. This is not one of the many demos you can find on the internet, but an application that is still relatively small and manageable but otherwise a real-world application with a lot of external dependencies and technical challenges. It was not always an easy ride but in the end it worked out nicely.
Actually this exercise was just the prelude to the real goal which is to also run this, and other similar applications, on mobile platforms like iOS and Android. (My main target is Android though, just because I own an Android phone and tablet but not any iOS device.) Now that the first step has been taken, I now eagerly await the general availability of the Android support of course.
2. Requirements
Just follow the instructions given on the project page at GitHub. https://github.com/gluonhq/client-maven-plugin Further documentation and some samples can be reached from this entry page too. There is also a Gradle plugin, but at the moment the focus of the development seems to be on the Maven plugin.
3. Build infrastructure
The GraalVM native-image
tool is very memory hungry. When your build times get longer
and longer, you are probably running out of memory. My initial hello-world builds were
mostly finished within 2 - 4 minutes but when I started to do bigger builds the times
went up to 7, 22 and finally more than an hour. This turned out to be caused
by the insuffcient amount of RAM in my old MacBook Pro, which only had 8 GB of RAM.
Now I use a Mac mini with 16 GB of RAM and my build times, even for larger projects,
are back in the 3 minutes range. So, using a development machine with enough memory
is essential and having even 32 GB of RAM available certainly does not hurt.
Having a fast multi-core CPU also does not hurt. I have seen CPU utilizations of up to 1200%, which is probably the best you can get from a 6 core CPU with hyperthreading.
4. Build configuration
4.1. Options
It is advisable to use the following native-image
options for the build:
-
-ea
-
--verbose
The option -ea
enables assertions in the resulting image and this is very helpful.
It is not as easy as in Java to debug a native image and therefore it is helpful to use
a lot of assertions in your code to be notified as early as possible about potential
problems, e.g., resources which have not been loaded.
The option --verbose
makes the output of the build process more verbose and this helps in
case something goes wrong. As a build takes a while, it makes sense to always use this option
so that you do not have to repeat the build in case something goes wrong and you don’t know why.
4.2. System properties
When calling native-image
you can define system properties but these are only
visible to the VM during the build process but not later at run-time of the native
application. This can cause some confusion because for classes which are initialized
at build-time, these system properties would be defined, whereas for classes which are
initialized at run-time they wouldn’t.
A concept, how they can be made visible at run-time too, is explained here
https://github.com/oracle/graal/issues/779 but this does not seem to work anymore
because classes are now initialized at run-time by default and not at build-time
as in previous versions. In order to circumvent this problem I created a separate
class for this and defined via the appropriate command line option --initialize-at-build-time
that this
particular class should be initialized at build time. This did the trick and it works now.
Don’t try to be too smart when writing this class. Only write primitive code because
otherwise native-image will refuse to initialize this class at build-time.
|
5. GraalVM/native-image limitations and issues
GraalVM native-image
still has several limitations which may bite you in real-world
projects. So I strongly advise you to read the following document which
summarizes most of these limitations.
The ones I stumbled over most often where:
-
Reflection configuration (Everywhere)
-
Method Handles not supported (Log4J, NSMenuFX)
(This issue seems to be mostly fixed now but NSMenuFX still does not work for other reasons.) -
Serialization not supported (Disk cache)
(This issue seems to be fixed with GraalVM 21.0.0) -
Soft-References not working as expected (RAM cache)
(This issue seems to be fixed now: https://github.com/oracle/graal/issues/2145) -
Only a single (default) locale
(This issue is supposed to be fixed in version 21.1.0 (20. April 2021): https://github.com/oracle/graal/issues/911#issuecomment-745209431) -
Media not supported on all platforms
I’ll go into more details in the following sections.
5.1. Reflection
The use of reflection is ubiquitous in the Java world which poses a problem for any AOT (ahead of time) compilation of Java code because which classes are accessed via reflection is not always known at build time. Some uses can be detected automatically but for others a list of classes must be provided by the user at build time.
One way to make this task less tedious and error prone, is to use the tracing agent.
This agent collects relevant data by analyzing the software when executed via a standard Java virtual machine. It’s a pity though that the output of this agent cannot yet be integrated directly into the configuration of the client-maven-plugin.
(This issue seems to be mostly fixed now because you can use the tracing agent via the client-maven-plugin.)
5.2. Resources
Resources can be delt with in a similar way as reflection. The nice thing is that you can specify which resources to load via wild cards. In my case it was enough to specify the following resource list:
<resourcesList> <list>.*\\.properties$</list> <list>.*\\.vert$</list> <list>.*\\.wav$</list> <list>.*\\.json$</list> <list>.*\\.COF$</list> </resourcesList>
A special case of this are language resource bundles which are also properties but have to be specified in a separate list. It would be very tedious if you would have to explicitly differentiate between general properties and language bundles but in my case I found it to be ok to keep the properties wild card in the resource list and separately add the language bundles to the bundles list like this.
<bundlesList> <list>com.mycompany.myproject.Main</list> <list>com.mycompany.myproject.airspaces.Airspaces</list> <list>com.mycompany.myproject.maps.Maps</list> <list>controlsfx</list> </bundlesList>
5.3. Method handles
According to the documentation, method handles are not supported.
This has severe consequences for several libraries and frameworks.
5.3.1. Logging
Logging frameworks are notorious users of all kind of reflection magic (I still don’t understand why) which
falls onto your feet when you use native-image
. The worst of all is Log4J.
I finally had to completely abandon Log4J (and in retrospect I wonder why I have ever used it at all).
This switch was made easy for me by the fact that I have consistently used the SLF4J facade throughout all my
software, so the only necessary change was the configuration of the logging framework and rewriting my own
JFX logging handler. I finally ended up using the standard Java logging because that is supported out of the
box with native-image
. The simple variant of SLF4J also worked but it would have been more complicated to
rewrite my JFX logging handler.
One problem remains though. I simply can’t get the FileHandler working. See: https://github.com/gluonhq/client-maven-plugin/issues/125
5.3.2. NSMenuFX
Another library I used was NSMenuFX to get a decent system menu integration for the Mac, which JavaFX
does not provide by default, but it failed with native-image
. After a lot of research
(thanks José https://github.com/gluonhq/substrate/issues/118 ) I finally learned that this is also due
to the internal use of method handles.
So I first created an issue https://github.com/codecentric/NSMenuFX/issues/31 on GitHub and finally fixed the problem myself and created a pull-request, which has now been integrated into the latest release of NSMenuFX.
However, my frustration grew again when I finally realized that this was all in vain and NSMenuFX still did not work because the system menu bar is in general not yet supported. This isn’t nice for the Mac version but as my real goal is the Android version it is not such a big problem because on Android I won’t need the system menu bar anyway.
5.4. Serialization
I used Java serialization for a temporary disk cache but serialization is currently not supported. So I now have to live without disk cache. (The issue was not serious enough to justify a switch to another fast serialization technique.)
5.5. Soft references
(This issue seems to be fixed now: https://github.com/oracle/graal/issues/2145)
I used a temporary RAM cache in my code which was based on Javas soft-references.
The result was that my native code felt slow and was not very responsive and I was
actually very disappointed. Finally I found out that this happened because my cache
was almost always empty and so my software had to load everything from disk over and over
again. GraalVMs native-image
handles references differently than the Java VM does, which
has the effect that all soft-references are always immediately cleared and thus became
useless to me.
There is only one small sentence in the documentation which hints at this deviation.
I learned from Laurent Bourgès that the MarlinFX renderer uses soft-references
by default to hold its own renderer context. It should therefore be tuned for
GraalVM native-image to use hard references instead: -Dprism.marlin.useRef=hard
|
5.6. Single locale
A severe, not very well documented, limitation of native-image
is the fact that
currently only one locale is supported. You have to decide at build time which locale
you want to use for your application. If you want to support more than one locale you
have to build separate versions of your application. One for each supported locale.
This is already a pain but it gets worse if you look at the possible side effects this can have. In fact you cannot even parse a simple string value which does not adhere to the conventions of your chosen built-in locale.
(This issue is supposed to be fixed in version 21.1.0 (20. April 2021): https://github.com/oracle/graal/issues/911#issuecomment-745209431)
6. JavaFX/Substrate limitations and issues
The JavaFX part of the native image creation currently also has some limitations.
6.2. AWT
AWT is currently not supported. This would not be such a big deal if some features of JavaFX did not depend on it.
-
javafx.application.HostServices.showDocument (fails on Mac)
Some other uses of AWT do work, e.g., image reading and writing. In order to save a JavaFX image it has to be converted to an AWT BufferedImage first, so that it can then be saved via ImageIO. That works although it is part of AWT.
It would probably be a good idea in general to make JavaFX completely independent from AWT.
6.3. Audio
Playing AudioClips currently does not seem to work because the glib-lite library is missing.
6.4. Image size
The size of the created executable file currently seems to be quite large. In my case, of a still quite small application, the size is already 100 MB, which is more than the whole .app bundle created by jpackage, which has only 73.8 MB if I bundle everything or only 58.8 MB if I use the Maven shade plugin with the option minimizeJar switched on.
If jlink would put a bit more effort into it, the size of the .app bundle could even be further reduced substantially by more selectively loading code and resources and not just doing so on a whole module basis.
(This issue can be solved via UPX. See: https://upx.github.io/)
6.5. Performance
The performance of the community editon of native-image
sometimes seems to be
much worse than the standard VM with HotSpot due to some missing code optimizations.
See: https://github.com/bourgesl/perfFX
7. Special cases
7.1. SQLite
It took me some time to get SQLite working but in the end all I had to do is to add the following items to the POM.
<jniList> <list>org.sqlite.core.DB</list> <list>org.sqlite.core.NativeDB</list> <list>org.sqlite.BusyHandler</list> <list>org.sqlite.Function</list> <list>org.sqlite.ProgressHandler</list> <list>org.sqlite.Function$Aggregate</list> <list>org.sqlite.Function$Window</list> <list>org.sqlite.core.DB$ProgressObserver</list> </jniList>
<resourcesList> <list>org/sqlite/native/Mac/${os.arch}/.*</list> <!-- Only for SQLite --> </resourcesList>
The last entry is tricky. The path contains the platform specifc shared library
of the native part of SQLite. (Change Mac
to the right one for your platform.
Just ${os.name} does not work.)
8. Open issues
8.1. Fully or partially blank panes
When something goes wrong during the initialization of a view, I often have the situation that I am just confronted with a blank stage or pane without any error message or stack trace. It is then very difficult to track down what the actual cause of the problem is. I mostly have this problem when initializing views via FXML.
8.2. FXML
The use of FXML is a PITA. All classes are loaded via reflection and so must be
present in the final reflection list. Some classes are already included in this list
by default, others (most ?) must be added manually. I finally adopted the habbit to
just copy the import
section of each FXML file because there you already have a list
of all classes used by this file if this file was created by SceneBuilder which luckily
does not use the wildcard notation.
In order to make this task at least a little bit less cumbersome, I have written a tool for myself to collect this information. I have published it on GitHub, just in case someone has the same need for such a tool like I had. It is not perfect but it helps a little. https://github.com/mipastgt/JFXToolsAndDemos#fxml-checker
Another annoying problem is that sometimes it is not sufficient to just put the class you
want to load into this list. E.g., if you want to load a ProgressBar and have put this
class into the refection list, you will still get the following error:
ProgressBar Property "progress" does not exist or is read-only
.
The reason is that the property "progress" is defined in the super-class of ProgressBar
and so you have to specify ProgressIndicator as well.
8.3. UnsatisfiedLinkErrors
Some native libraries seem to be missing from substrate and so you will get UnsatisfiedLinkErrors.
-
java.util.logging.FileHandler
See: https://github.com/gluonhq/client-maven-plugin/issues/125 -
com.sun.imageio.plugins.jpeg.JPEGImageReader
symbol: Java_com_sun_imageio_plugins_jpeg_JPEGImageReader_initJPEGImageReader or Java_com_sun_imageio_plugins_jpeg_JPEGImageReader_initJPEGImageReader__ -
no jfxwebkit in java.library.path
8.4. Misleading error messages
Very often the error messages you get are very misleading.
At a first glance an error message like java.lang.IllegalArgumentException: Unable to coerce CENTER to class javafx.geometry.HPos.
is very cunfusing because CENTER definitely is a valid member of HPos. The actual reason for this
error message is that javafx.geometry.HPos
is just missing in the reflection list. Error messages
should give a more precise hint on the real cause for an error.
8.5. JAXB
For me JAXB is the workhorse for dealing with XML files but this seems to be a hard problem for GraalVM/native-image.
I got this working in a separte test program for some GPX files by following the hints in the above link. I can now read and write such files. (At least the ones I have tested, but that is another issue.)
However, this involves the use of the tracing agent which is currently not supported by the Client-Maven-Plugin and when I tried to transfer the results of the agent manually I got stuck because there is currently also no proxy list support.
Until now I have not found a solution for this and thus cannot read or write any XML files in my real software, which limits its usability quite a bit.
(This issue seems to be mostly fixed now because you can use the tracing agent via the client-maven-plugin.)
9. Java VM vs. GraalVM/native-image comparison
This is a subjective comparison of a standard Java VM (Oracle OpenJDK 14 EA) versus the GraalVM/native-image community edition (20.0.0 utilized by GluonHQ/substrate via Client-Maven-Plugin).
Feature | Java VM | GraalVM/native-image |
---|---|---|
Works on Mobile |
- |
+ |
Development experience |
+ |
- |
Feature completeness |
+ |
0 |
Startup time |
0 |
+ |
Warmup time |
0 |
+ |
Peak performance |
+ |
0 |
Bundle size |
0 |
- (pure), + (with UPX) |
Some remarks on the table:
-
The startup time of the Java VM could be further reduced if AppCDS would also work for reduced runtime images created via jlink. The current advantage of AOT compilation could be reduced in this respect.
-
Also the warmup time of the Java VM could be further reduced via profile guided optimization.
Taking all this into account, the real driver to use GraalVM/native-image is the promise that it will allow the use of the latest standard Java/JavaFX on mobile devices too and thus make it possible to cover the mobile, embedded and desktop sector with a single code base. For a pure desktop environment its usefulness is currently still questionable due to various limitations and the development overhead, but we are making progress.
10. Conclusion
This is only a snapshot of my experiences so far in getting a real-world JavaFX application compiled into a native image. If I have missed something important or you think you can help me with one of the open issues, just drop me a line or create an issue here.
Once you have circumvented all the mentioned problems, the resulting binary seems to be quite stable and the performance is also relatively good. So, I am looking forward to do the next step and compile the whole application as an Android app.