substrate
substrate copied to clipboard
Add more dummy methods to the Android launcher
Issue
This is a follow up to https://github.com/gluonhq/substrate/pull/971 and https://github.com/gluonhq/substrate/pull/972.
I made a minimal Android app with FXGL, the game engine which uses JavaFX. It turns out that GraalVM Native Image adds many more unresolved references when compiling FXGL. I added the dummy methods for them to launcher.c.
With these changes my minimal app works but I wonder if this is a good approach. All the methods I added have something to do with image handling. What if, when I start to write a more complex app, GraalVM Native Image will start adding even more references?
Here's my app: https://github.com/makingthematrix/scalaonandroid/tree/main/HelloFXGL
Progress
- [ ] Change must not contain extraneous whitespace
- [ ] License header year is updated, if required
- [ ] Verify the contributor has signed Gluon Individual Contributor License Agreement (CLA)
Could someone take a look and give me some feedback? :)
It's a complex issue, and your analysis is correct: what if there are more references? We will end up with dummy methods for the AWT libs in the launchers.
So the question we need to answer first is: why are there references to those methods? Based on your sample, we should be able to find that: running native-image with verbose creates a report with the usage-tree. If you add the
@johanvos I generated reports for Android and, for comparison, also for a desktop Linux app, which works. Here are the zip files:
I'm looking through them right now. I don't really know what to search for, to be honest, but I see the call to the methods I put in the commit have one thing in common. They are all followed by a similar sequence of calls to methods in com.oracle.svm.jni.JNIGeneratedMethodSupport and each line ends with @bci=-6->-6 (on other lines @bci is just one positive integer).
These lines also appear in the desktop report but there they are sometimes followed by more calls with normal @bci number.
For example, this is the call tree for com.sun.imageio.plugins.jpeg.JPEGImageReader.initReaderIDs on Android:
1370611 ├── entry com.sun.imageio.plugins.jpeg.JPEGImageReader.<clinit>():void id=1487
1370612 │ ├── directly calls com.sun.imageio.plugins.jpeg.JPEGImageReader$1.<init>():void id=3946 @bci=4
1370613 │ ├── directly calls com.sun.imageio.plugins.jpeg.JPEGImageReader.initReaderIDs(java.lang.Class, java.lang.Class, java.lang.Class):void id=3947 @bci=19
1370614 │ │ ├── directly calls com.oracle.svm.jni.JNIGeneratedMethodSupport.boxObjectInLocalHandle(java.lang.Object):com.oracle.svm.jni.nativeapi.JNIObjectHandle id-ref=2850 @bci=-6->-6
1370615 │ │ ├── directly calls com.oracle.svm.jni.JNIGeneratedMethodSupport.boxObjectInLocalHandle(java.lang.Object):com.oracle.svm.jni.nativeapi.JNIObjectHandle id-ref=2850 @bci=-6->-6
1370616 │ │ ├── directly calls com.oracle.svm.jni.JNIGeneratedMethodSupport.boxObjectInLocalHandle(java.lang.Object):com.oracle.svm.jni.nativeapi.JNIObjectHandle id-ref=2850 @bci=-6->-6
1370617 │ │ ├── directly calls com.oracle.svm.jni.JNIGeneratedMethodSupport.boxObjectInLocalHandle(java.lang.Object):com.oracle.svm.jni.nativeapi.JNIObjectHandle id-ref=2850 @bci=-6->-6
1370618 │ │ ├── directly calls com.oracle.svm.jni.JNIGeneratedMethodSupport.environment():com.oracle.svm.jni.nativeapi.JNIEnvironment id-ref=5432 @bci=-6->-6
1370619 │ │ ├── directly calls com.oracle.svm.jni.JNIGeneratedMethodSupport.nativeCallEpilogue(int):void id-ref=5434 @bci=-6->-6
1370620 │ │ ├── directly calls com.oracle.svm.jni.JNIGeneratedMethodSupport.nativeCallPrologue():int id-ref=5435 @bci=-6->-6
1370621 │ │ └── directly calls com.oracle.svm.jni.JNIGeneratedMethodSupport.rethrowPendingException():void id-ref=5436 @bci=-6->-6
And this is the same thing for Desktop:
1690671 ├── entry com.sun.imageio.plugins.jpeg.JPEGImageReader.<clinit>():void id=1999
1690672 │ ├── directly calls com.sun.imageio.plugins.jpeg.JPEGImageReader$1.<init>():void id=4600 @bci=4
1690673 │ ├── directly calls com.sun.imageio.plugins.jpeg.JPEGImageReader.initReaderIDs(java.lang.Class, java.lang.Class, java.lang.Class):void id=4601 @bci=19
1690674 │ │ ├── directly calls com.oracle.svm.jni.JNIGeneratedMethodSupport.boxObjectInLocalHandle(java.lang.Object):com.oracle.svm.jni.nativeapi.JNIObjectHandle id-ref=3368 @bci=-6->-6
1690675 │ │ ├── directly calls com.oracle.svm.jni.JNIGeneratedMethodSupport.boxObjectInLocalHandle(java.lang.Object):com.oracle.svm.jni.nativeapi.JNIObjectHandle id-ref=3368 @bci=-6->-6
1690676 │ │ ├── directly calls com.oracle.svm.jni.JNIGeneratedMethodSupport.boxObjectInLocalHandle(java.lang.Object):com.oracle.svm.jni.nativeapi.JNIObjectHandle id-ref=3368 @bci=-6->-6
1690677 │ │ ├── directly calls com.oracle.svm.jni.JNIGeneratedMethodSupport.boxObjectInLocalHandle(java.lang.Object):com.oracle.svm.jni.nativeapi.JNIObjectHandle id-ref=3368 @bci=-6->-6
1690678 │ │ ├── directly calls com.oracle.svm.jni.JNIGeneratedMethodSupport.environment():com.oracle.svm.jni.nativeapi.JNIEnvironment id=19736 @bci=-6->-6
1690679 │ │ │ └── directly calls com.oracle.svm.jni.JNIThreadLocalEnvironment.getAddress():com.oracle.svm.jni.nativeapi.JNIEnvironment id-ref=9814 @bci=0
1690680 │ │ ├── directly calls com.oracle.svm.jni.JNIGeneratedMethodSupport.nativeCallEpilogue(int):void id=19737 @bci=-6->-6
1690681 │ │ │ └── directly calls com.oracle.svm.jni.JNIObjectHandles.popLocalFramesIncluding(int):void id=28523 @bci=1
1690682 │ │ │ ├── directly calls com.oracle.svm.core.handles.ThreadLocalHandles.popFramesIncluding(int):void id-ref=30770 @bci=4
1690683 │ │ │ └── directly calls com.oracle.svm.jni.JNIObjectHandles.getExistingLocals():com.oracle.svm.core.handles.ThreadLocalHandles id-ref=23262 @bci=0
1690684 │ │ ├── directly calls com.oracle.svm.jni.JNIGeneratedMethodSupport.nativeCallPrologue():int id=19738 @bci=-6->-6
1690685 │ │ │ └── directly calls com.oracle.svm.jni.JNIObjectHandles.pushLocalFrame(int):int id-ref=9784 @bci=2
1690686 │ │ └── directly calls com.oracle.svm.jni.JNIGeneratedMethodSupport.rethrowPendingException():void id=19739 @bci=-6->-6
1690687 │ │ └── directly calls com.oracle.svm.jni.JNIGeneratedMethodSupport.getAndClearPendingException():java.lang.Throwable id-ref=3369 @bci=0
Does it make any sense?
By the way, if I revert to GraalVM 20.x and the old maven-client-plugin instead of gluonfx-maven-plugin, the app works on Android. Maybe it's really a GraalVM issue?
That last issue is very relevant. Once JNI_OnLoad_javajpeg is found (and required), the other functions are probably linked into the executable, so you don't have to add them to the android-specific implementations. Hence, we need to provide an option to pull in JNI_OnLoad_javajpeg.
In general, when some native functions are required, they have to be provided. Dummy methods don't really provide the implementation, so they are not a solution (but an easy hack, admitted). The problem here comes from the java.desktop module which has com.sun.imageio.plugins.jpeg.JPEGImageReader and which will pull in a bunch of native functions. There are actually 2 scenario's:
- JPEGImageReader is required, and in that case the native functions are required as well. Dummy implementations will fail.
- JPEGImageReader is not required. In that case, it shouldn't show up in the analysis.
From the treedump, it seems that JPEGImageReader is required, but if the dummy methods are sufficient, it is very unlikely that they are ever invoked. So JPEGImageReader should not be required. Can you find out why JPEGImageReader is in the tree?
The slightly more elegant approach (instead of adding those methods to the launcher) is to use a separate file with empty methods. That is something we can do in Substrate: check for files in a given path, and compile them as well.
Fixed in metadata
accidentally closed this
I ran into this issue as well and was also missing several other methods. I haven't figured out where they are coming from yet, but replacing them with stubs worked and none of them were ever called. For anyone in a similar situation, the missing stubs can be generated with
nm target/gluonfx/aarch64-android/lib*.so | \
grep "U Java_" | \
awk '{print "void " $2 "() {\n fprintf(stderr, \"We should never reach here ("$2")\\n\");\n}\n"}'
I didn't see any way to add custom sources to the build step, so I ended up modifying the jar's native/android/launcher.c file.
Full list of missing references
nm target/gluonfx/aarch64-android/lib*.so | grep "U Java_" | awk '{print $2}'
Java_com_sun_imageio_plugins_jpeg_JPEGImageReader_clearNativeReadAbortFlag Java_com_sun_imageio_plugins_jpeg_JPEGImageReader_disposeReader Java_com_sun_imageio_plugins_jpeg_JPEGImageReader_initJPEGImageReader Java_com_sun_imageio_plugins_jpeg_JPEGImageReader_initReaderIDs Java_com_sun_imageio_plugins_jpeg_JPEGImageReader_readImage Java_com_sun_imageio_plugins_jpeg_JPEGImageReader_readImageHeader Java_com_sun_imageio_plugins_jpeg_JPEGImageReader_resetLibraryState Java_com_sun_imageio_plugins_jpeg_JPEGImageReader_resetReader Java_com_sun_imageio_plugins_jpeg_JPEGImageReader_setOutColorSpace Java_com_sun_imageio_plugins_jpeg_JPEGImageReader_setSource Java_com_sun_imageio_plugins_jpeg_JPEGImageWriter_disposeWriter Java_com_sun_imageio_plugins_jpeg_JPEGImageWriter_initJPEGImageWriter Java_com_sun_imageio_plugins_jpeg_JPEGImageWriter_initWriterIDs Java_com_sun_imageio_plugins_jpeg_JPEGImageWriter_resetWriter Java_com_sun_imageio_plugins_jpeg_JPEGImageWriter_setDest Java_com_sun_imageio_plugins_jpeg_JPEGImageWriter_writeImage Java_com_sun_imageio_plugins_jpeg_JPEGImageWriter_writeTables Java_java_awt_image_BufferedImage_initIDs Java_java_awt_image_ColorModel_initIDs Java_java_awt_image_IndexColorModel_initIDs Java_java_awt_image_Raster_initIDs Java_java_awt_image_SampleModel_initIDs Java_java_awt_image_SinglePixelPackedSampleModel_initIDs Java_sun_awt_FontDescriptor_initIDs Java_sun_awt_PlatformFont_initIDs Java_sun_awt_image_BufImgSurfaceData_initIDs Java_sun_awt_image_BufImgSurfaceData_initRaster Java_sun_awt_image_ByteComponentRaster_initIDs Java_sun_awt_image_BytePackedRaster_initIDs Java_sun_awt_image_IntegerComponentRaster_initIDs Java_sun_awt_image_ShortComponentRaster_initIDs Java_sun_font_StrikeCache_getGlyphCacheDescription Java_sun_font_SunFontManager_initIDs Java_sun_java2d_DefaultDisposerRecord_invokeNativeDispose Java_sun_java2d_Disposer_initIDs Java_sun_java2d_SurfaceData_initIDs Java_sun_java2d_SurfaceData_isOpaqueGray Java_sun_java2d_cmm_lcms_LCMS_colorConvert Java_sun_java2d_cmm_lcms_LCMS_createNativeTransform Java_sun_java2d_cmm_lcms_LCMS_getProfileDataNative Java_sun_java2d_cmm_lcms_LCMS_getProfileID Java_sun_java2d_cmm_lcms_LCMS_getTagNative Java_sun_java2d_cmm_lcms_LCMS_initLCMS Java_sun_java2d_cmm_lcms_LCMS_loadProfileNative Java_sun_java2d_loops_Blit_Blit Java_sun_java2d_loops_GraphicsPrimitiveMgr_initIDs Java_sun_java2d_loops_GraphicsPrimitiveMgr_registerNativeLoops Java_sun_java2d_loops_MaskBlit_MaskBlit Java_sun_java2d_pipe_Region_initIDs Java_sun_java2d_pipe_ShapeSpanIterator_initIDs Java_sun_java2d_pipe_SpanClipRenderer_initIDs
The slightly more elegant approach (instead of adding those methods to the launcher) is to use a separate file with empty methods. That is something we can do in Substrate: check for files in a given path, and compile them as well.
+1
@ennerf After https://github.com/gluonhq/substrate/pull/1103, you can also create a c file with all the missing methods that you need, like:
missing_symbols.c:
#include <stdlib.h>
void JNI_OnLoad_javajpeg() {
fprintf(stderr, "We should never reach here (JNI_OnLoad_javajpeg)\n");
}
...
then you can compile for Android:
$ANDROID_NDK/toolchains/llvm/prebuilt/linux-x86_64/bin/clang -c -target aarch64-linux-android -I. missing_symbols.c
and finally you can use linkerArgs to include it to the link process:
<plugin>
<groupId>com.gluonhq</groupId>
<artifactId>gluonfx-maven-plugin</artifactId>
<version>${gluonfx.maven.version}</version>
<configuration>
<linkerArgs>
<arg>/path/to/missing_symbols.o</arg>
</linkerArgs>
...
For the record, I ran into the same issue as #377 . The problem was, that I had ScenicView in the classpath. Maybe this hint helps someone else running into the same issue.
It looks like JAX-B pulls in a lot of dependencies as well https://github.com/oracle/graal/issues/3225
Same issue here! Using graalvm-svm-java17-linux-gluon-22.1.0.1-Final on Ubuntu 22 LTS
https://stackoverflow.com/questions/76393159/dlopen-failed-cannot-locate-symbol-jni-onload-javajpeg
I have the same problem with a basic FXGL demo application: https://github.com/FDelporte/JavaFXGameSnakeApp
In the Google Play Store reports, several identical error messages are shown:
java.lang.UnsatisfiedLinkError: dlopen failed:
cannot locate symbol "JNI_OnLoad_javajpeg" referenced by
"/data/app/~~mfcP9UKk5v7YB8oaYdyKAQ==/be.webtechie.emojisnakegameapp-xf_z_-5-dleGGUxh5ExT9A==/split_config.arm64_v8a.apk!/lib/arm64-v8a/libsubstrate.so"...