Closed danielchemko closed 1 year ago
The issue is reproduced with a simple FXML sample on Linux as well.
The reason is that JavaFX Trampoline class checks that it is not defined on the bootstrap or platform classloader: https://github.com/openjdk/jfx/blob/a17a71458def91d206844b7d64e185af75a3c6e0/modules/javafx.base/src/main/java/com/sun/javafx/reflect/MethodUtil.java#L46
A simple Trampoline substitutor can workaround the problem in a project which uses FXML:
import com.oracle.svm.core.annotate.Substitute;
import com.oracle.svm.core.annotate.TargetClass;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.security.AccessController;
@TargetClass(className = "com.sun.javafx.reflect.Trampoline")
@Substitute
public final class com_sun_javafx_reflect_Trampoline {
@Substitute
private static void ensureInvocableMethod(Method m)
throws InvocationTargetException {
Class<?> clazz = m.getDeclaringClass();
if (clazz.equals(AccessController.class) ||
clazz.equals(Method.class) ||
clazz.getName().startsWith("java.lang.invoke."))
throw new InvocationTargetException(
new UnsupportedOperationException("invocation not supported"));
}
@Substitute
private static Object invoke(Method m, Object obj, Object[] params)
throws InvocationTargetException, IllegalAccessException {
ensureInvocableMethod(m);
return m.invoke(obj, params);
}
}
The substitutor can be just copied to the project and SVM dependency added to the pom.xml
(with the required version):
<dependency>
<groupId>org.graalvm.nativeimage</groupId>
<artifactId>svm</artifactId>
<version>23.0.0</version>
<scope>provided</scope>
</dependency>
The more rigorous fix can restrict the Trampolin.invoke(...)
method to call only methods on classes or modules used only by FXML.
For example, on my simple FXML project the -Djavafx.verbose=true
option prints:
java -Djavafx.verbose=true -jar target/fxmlsample-1.0-SNAPSHOT-jar-with-dependencies.jar
...
Calling main(String[]) method
com.sun.javafx.fxml.ModuleHelper : <clinit>
getModuleMethod = public java.lang.Module java.lang.Class.getModule()
getResourceAsStreamMethod = public java.io.InputStream java.lang.Module.getResourceAsStream(java.lang.String) throws java.io.IOException
thisModule = module javafx.fxml
methodModule = module javafx.graphics
m = public javafx.collections.ObservableList javafx.scene.layout.Pane.getChildren()
thisModule = module javafx.fxml
methodModule = module javafx.controls
m = public final void javafx.scene.control.Labeled.setText(java.lang.String)
thisModule = module javafx.fxml
methodModule = module javafx.controls
m = public final void javafx.scene.control.Labeled.setText(java.lang.String)
thisModule = module javafx.fxml
methodModule = module javafx.controls
m = public final void javafx.scene.control.Labeled.setText(java.lang.String)
thisModule = module javafx.fxml
methodModule = module javafx.graphics
m = public final void javafx.scene.layout.VBox.setSpacing(double)
The javafx.fxml
module calls only javafx.graphics
and javafx.controls
modules in my simple project.
The substitutor can be updated to allow to call methods only from these modules or even concrete methods.
Thanks @AlexanderScherbatiy, your workaround worked perfectly!
I imagine you've already done something like the same, but my next adventure was to catch all reflective calls to @FXML to get automatically added without needing to manually launch the code-path for each reflective call via the agent. This is why I wrote as a quick-fix:
import javafx.fxml.FXML;
import org.graalvm.nativeimage.hosted.Feature;
import org.graalvm.nativeimage.hosted.RuntimeReflection;
import java.util.Arrays;
class PreComputeFieldFeature implements Feature {
@Override
public void beforeAnalysis(BeforeAnalysisAccess access) {
access.registerSubtypeReachabilityHandler(this::iterateFields, Object.class);
}
// This method is invoked for every type that is reachable.
private void iterateFields(DuringAnalysisAccess access, Class<?> subtype) {
try {
Arrays.stream(subtype.getFields()).forEach(f -> {
if (f.getAnnotation(FXML.class) != null) {
System.out.println("Marking field accessed: [" + f.getDeclaringClass().getName() + "." + f.getName() + "]");
RuntimeReflection.register(f);
}
});
Arrays.stream(subtype.getDeclaredMethods()).forEach(m -> {
if (m.getAnnotation(FXML.class) != null) {
System.out.println("Marking method reflective: [" + m.getDeclaringClass().getName() + "." + m.getName() + "]");
RuntimeReflection.register(m);
}
});
} catch (NoClassDefFoundError ex) {
}
}
}
Then add --feature=package.to.PreComputeFieldFeature (or just add it to a jar META-INF)
I had very simple FXML example where I always needed to press the button to add the used @FXML
method into the reflect-config.json
file by the tracing agent.
Your PreComputeFieldFeature
is indeed very helpful to automatically register @FXML
fields and methods without manually touching all available paths.
This bug has been fixed in NIK 23.0.1. The workaround should no longer be needed.
Hello, I'm trying out NIK 20.0.1+10 (
bellsoft-liberica-vm-full-openjdk20-23.0.0
) on OSX (M1 Pro) for my simple JavaFX application and I've been trying to compile it into native-image. This will be my first attempt at native JavaFX, so apologies if there's something obvious I've missed.I have a few key JavaFX dependencies: javafx.controls, javafx.fxml. It seems like I'm getting hung up on the FXML reflection layer. Everything works fine in Java 17, 19, 20 running via JDK on my computer, and I've tried to follow https://www.graalvm.org/22.2/reference-manual/native-image/guides/use-native-image-maven-plugin/ on how to build native images using Maven tooling.
Run the application (with agent config capture)
PATH=/opt/java/bellsoft-liberica-vm-full-openjdk20-23.0.0/Contents/Home/bin:$PATH GRAALVM_HOME=/opt/java/bellsoft-liberica-vm-full-openjdk20-23.0.0/Contents/Home JAVA_HOME=/opt/java/bellsoft-liberica-vm-full-openjdk20-23.0.0/Contents/Home mvn -Pnative -Dagent exec:exec@java-agent
Build the native image (with agent config)
PATH=/opt/java/bellsoft-liberica-vm-full-openjdk20-23.0.0/Contents/Home/bin:$PATH GRAALVM_HOME=/opt/java/bellsoft-liberica-vm-full-openjdk20-23.0.0/Contents/Home JAVA_HOME=/opt/java/bellsoft-liberica-vm-full-openjdk20-23.0.0/Contents/Home mvn -Pnative -Dagent package -DskipTests
JavaFX startup code causing problems (Written in Kotlin, sorry)
I've tried several ways, but here are the results:
new FXMLLoader()
before failing to find JavaFX primitives likejavafx.scene.Button
(Even with--add-modules=java.desktop,javafx.controls,javafx.graphics,javafx.fxml
it doesn't seem to be detected as a core dependency most likely because of reflection).I'm not entirely sure how I could force the trampoline to be skipped from the system classloader or to have the compiler not skip over its insertion when it sees a copy within the platform classloader.