From 36b0d98c67d96975f1aff4da8554a301171cdaa1 Mon Sep 17 00:00:00 2001 From: janblom Date: Mon, 19 Feb 2024 20:27:47 +0100 Subject: [PATCH] Java 18+ compatibility using ByteBuddy, Rewrite injection of CTCGraphicsEnvironment (#16) * Avoid attempt to make java.awt.GraphicsEnvironment non final, as this breaks in Java 18+. Instead, use ByteBuddy to return CTCGraphicsEnvironment when sun.awt.PlatformGraphicsInfo.createGE() is called. * Ignore jenv and log files * Fix UnsupportedOperationException: inject CTCGraphicsEnvironment through sun.awt.PlatformGraphicsInfo.createGE() * Replace removing a final modifier, which is not possible in JDK 18+, by ByteBuddy interceptors. * Ensure that there is only one instance of CTCGraphicsEnvironment in use * Keep constructor public, no need to change the API * Make changing the Java version easier * Update CacioExtension.java * Update README.md --- .gitignore | 9 ++ README.md | 2 + cacio-shared/pom.xml | 4 +- cacio-tta/pom.xml | 15 ++- .../cacio/ctc/CTCGraphicsEnvironment.java | 4 + .../cacio/ctc/CTCSurfaceManagerFactory.java | 2 +- .../cacio/ctc/CTCVolatileSurfaceManager.java | 2 +- .../cacio/ctc/junit/CTCInterceptor.java | 14 +++ .../cacio/ctc/junit/CacioExtension.java | 108 +++++++++++++++--- pom.xml | 9 +- 10 files changed, 143 insertions(+), 26 deletions(-) create mode 100644 cacio-tta/src/main/java/com/github/caciocavallosilano/cacio/ctc/junit/CTCInterceptor.java diff --git a/.gitignore b/.gitignore index 3034361..7d5b006 100644 --- a/.gitignore +++ b/.gitignore @@ -17,3 +17,12 @@ target/ # OS X .DS_Store + +# jenv + + +# jenv +.java-version + +# log files +*.log diff --git a/README.md b/README.md index 3e7072c..d9347ea 100644 --- a/README.md +++ b/README.md @@ -132,6 +132,8 @@ This is because Java only allows to set the toolkit once, and it cannot be unloa The `add-exports` and `add-opens` jvm args are required with Java 17, since these are internal packages that aren't exported, these can't be added to a `module-info.java` file. +With Java 18+, you may also want to add a argument to suppress warnings about an agent (ByteBuddyAgent) being loaded: `-XX:+EnableDynamicAgentLoading` . + You can change the resolution of the virtual screen by setting the `cacio.managed.screensize` system property. For example: diff --git a/cacio-shared/pom.xml b/cacio-shared/pom.xml index 1db89e5..bd69d1f 100644 --- a/cacio-shared/pom.xml +++ b/cacio-shared/pom.xml @@ -21,8 +21,8 @@ maven-compiler-plugin 3.8.1 - 17 - 17 + ${cacio.java.version} + ${cacio.java.version} -XDignore.symbol.file=true --add-exports=java.desktop/java.awt.peer=ALL-UNNAMED diff --git a/cacio-tta/pom.xml b/cacio-tta/pom.xml index d1067c2..3ecf9d7 100644 --- a/cacio-tta/pom.xml +++ b/cacio-tta/pom.xml @@ -45,6 +45,17 @@ jide-oss 3.6.18 + + + net.bytebuddy + byte-buddy + 1.14.11 + + + net.bytebuddy + byte-buddy-agent + 1.14.11 + @@ -54,8 +65,8 @@ maven-compiler-plugin 3.8.1 - 17 - 17 + ${cacio.java.version} + ${cacio.java.version} -XDignore.symbol.file=true --add-exports=java.desktop/java.awt.peer=ALL-UNNAMED diff --git a/cacio-tta/src/main/java/com/github/caciocavallosilano/cacio/ctc/CTCGraphicsEnvironment.java b/cacio-tta/src/main/java/com/github/caciocavallosilano/cacio/ctc/CTCGraphicsEnvironment.java index 24e8c8c..5cdfbf7 100644 --- a/cacio-tta/src/main/java/com/github/caciocavallosilano/cacio/ctc/CTCGraphicsEnvironment.java +++ b/cacio-tta/src/main/java/com/github/caciocavallosilano/cacio/ctc/CTCGraphicsEnvironment.java @@ -31,10 +31,14 @@ public class CTCGraphicsEnvironment extends SunGraphicsEnvironment { + private static final CTCGraphicsEnvironment INSTANCE = new CTCGraphicsEnvironment(); public CTCGraphicsEnvironment() { SurfaceManagerFactory.setInstance(new CTCSurfaceManagerFactory()); } + public static CTCGraphicsEnvironment getInstance() { + return INSTANCE; + } @Override protected int getNumScreens() { return 1; diff --git a/cacio-tta/src/main/java/com/github/caciocavallosilano/cacio/ctc/CTCSurfaceManagerFactory.java b/cacio-tta/src/main/java/com/github/caciocavallosilano/cacio/ctc/CTCSurfaceManagerFactory.java index b06387c..7bc20a4 100644 --- a/cacio-tta/src/main/java/com/github/caciocavallosilano/cacio/ctc/CTCSurfaceManagerFactory.java +++ b/cacio-tta/src/main/java/com/github/caciocavallosilano/cacio/ctc/CTCSurfaceManagerFactory.java @@ -28,7 +28,7 @@ import sun.awt.image.VolatileSurfaceManager; import sun.java2d.SurfaceManagerFactory; -class CTCSurfaceManagerFactory extends SurfaceManagerFactory { +public class CTCSurfaceManagerFactory extends SurfaceManagerFactory { @Override public VolatileSurfaceManager createVolatileManager(SunVolatileImage image, diff --git a/cacio-tta/src/main/java/com/github/caciocavallosilano/cacio/ctc/CTCVolatileSurfaceManager.java b/cacio-tta/src/main/java/com/github/caciocavallosilano/cacio/ctc/CTCVolatileSurfaceManager.java index 236dc57..81e36bf 100644 --- a/cacio-tta/src/main/java/com/github/caciocavallosilano/cacio/ctc/CTCVolatileSurfaceManager.java +++ b/cacio-tta/src/main/java/com/github/caciocavallosilano/cacio/ctc/CTCVolatileSurfaceManager.java @@ -28,7 +28,7 @@ import sun.awt.image.VolatileSurfaceManager; import sun.java2d.SurfaceData; -class CTCVolatileSurfaceManager extends VolatileSurfaceManager { +public class CTCVolatileSurfaceManager extends VolatileSurfaceManager { protected CTCVolatileSurfaceManager(SunVolatileImage vImg, Object context) { super(vImg, context); diff --git a/cacio-tta/src/main/java/com/github/caciocavallosilano/cacio/ctc/junit/CTCInterceptor.java b/cacio-tta/src/main/java/com/github/caciocavallosilano/cacio/ctc/junit/CTCInterceptor.java new file mode 100644 index 0000000..c2464a9 --- /dev/null +++ b/cacio-tta/src/main/java/com/github/caciocavallosilano/cacio/ctc/junit/CTCInterceptor.java @@ -0,0 +1,14 @@ +package com.github.caciocavallosilano.cacio.ctc.junit; + +import com.github.caciocavallosilano.cacio.ctc.CTCGraphicsEnvironment; +import net.bytebuddy.implementation.bind.annotation.*; + +import java.awt.*; + + +public class CTCInterceptor { + @RuntimeType + public static GraphicsEnvironment intercept() { + return CTCGraphicsEnvironment.getInstance(); + } +} diff --git a/cacio-tta/src/main/java/com/github/caciocavallosilano/cacio/ctc/junit/CacioExtension.java b/cacio-tta/src/main/java/com/github/caciocavallosilano/cacio/ctc/junit/CacioExtension.java index 8974a41..ad5c44c 100644 --- a/cacio-tta/src/main/java/com/github/caciocavallosilano/cacio/ctc/junit/CacioExtension.java +++ b/cacio-tta/src/main/java/com/github/caciocavallosilano/cacio/ctc/junit/CacioExtension.java @@ -24,8 +24,19 @@ */ package com.github.caciocavallosilano.cacio.ctc.junit; -import com.github.caciocavallosilano.cacio.ctc.CTCGraphicsEnvironment; -import com.github.caciocavallosilano.cacio.ctc.CTCToolkit; +import com.github.caciocavallosilano.cacio.ctc.*; +import com.github.caciocavallosilano.cacio.peer.PlatformWindowFactory; +import com.github.caciocavallosilano.cacio.peer.managed.FullScreenWindowFactory; +import net.bytebuddy.ByteBuddy; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.dynamic.ClassFileLocator; +import net.bytebuddy.dynamic.loading.ClassInjector; +import net.bytebuddy.dynamic.loading.ClassReloadingStrategy; +import net.bytebuddy.implementation.MethodDelegation; +import net.bytebuddy.implementation.bind.annotation.*; +import net.bytebuddy.matcher.ElementMatchers; +import net.bytebuddy.pool.TypePool; +import net.bytebuddy.agent.ByteBuddyAgent; import org.junit.jupiter.api.extension.ConditionEvaluationResult; import org.junit.jupiter.api.extension.ExecutionCondition; import org.junit.jupiter.api.extension.ExtensionContext; @@ -33,11 +44,17 @@ import javax.swing.plaf.metal.MetalLookAndFeel; import java.awt.*; +import java.io.IOException; import java.lang.invoke.MethodHandles; import java.lang.invoke.VarHandle; import java.lang.reflect.AnnotatedElement; import java.lang.reflect.Field; -import java.lang.reflect.Modifier; +import java.lang.reflect.Method; +import java.util.Map; + + +import static net.bytebuddy.matcher.ElementMatchers.nameStartsWith; + public class CacioExtension implements ExecutionCondition { // https://stackoverflow.com/a/56043252/1050369 @@ -45,6 +62,8 @@ public class CacioExtension implements ExecutionCondition { static { try { + ByteBuddyAgent.install(); + var lookup = MethodHandles.privateLookupIn(Field.class, MethodHandles.lookup()); MODIFIERS = lookup.findVarHandle(Field.class, "modifiers", int.class); } catch (IllegalAccessException | NoSuchFieldException ex) { @@ -54,31 +73,23 @@ public class CacioExtension implements ExecutionCondition { static { try { + injectCTCGraphicsEnvironment(); + Field toolkit = Toolkit.class.getDeclaredField("toolkit"); toolkit.setAccessible(true); toolkit.set(null, new CTCToolkit()); Field defaultHeadlessField = java.awt.GraphicsEnvironment.class.getDeclaredField("defaultHeadless"); defaultHeadlessField.setAccessible(true); - defaultHeadlessField.set(null, Boolean.TRUE); + defaultHeadlessField.set(null, Boolean.FALSE); Field headlessField = java.awt.GraphicsEnvironment.class.getDeclaredField("headless"); headlessField.setAccessible(true); - headlessField.set(null, Boolean.TRUE); - - Class geCls = Class.forName("java.awt.GraphicsEnvironment$LocalGE"); - Field ge = geCls.getDeclaredField("INSTANCE"); - ge.setAccessible(true); - defaultHeadlessField.set(null, Boolean.FALSE); headlessField.set(null, Boolean.FALSE); - makeNonFinal(ge); - Class smfCls = Class.forName("sun.java2d.SurfaceManagerFactory"); Field smf = smfCls.getDeclaredField("instance"); smf.setAccessible(true); smf.set(null, null); - - ge.set(null, new CTCGraphicsEnvironment()); } catch (Exception e) { e.printStackTrace(); } @@ -86,10 +97,63 @@ public class CacioExtension implements ExecutionCondition { System.setProperty("swing.defaultlaf", MetalLookAndFeel.class.getName()); } - public static void makeNonFinal(Field field) { - int mods = field.getModifiers(); - if (Modifier.isFinal(mods)) { - MODIFIERS.set(field, mods & ~Modifier.FINAL); + public static void injectCTCGraphicsEnvironment() throws ClassNotFoundException, IOException { + /* + * ByteBuddy is used to intercept the methods that return the graphics environment in use + * (java.awt.GraphicsEnvironment.getLocalGraphicsEnvironment() and + * sun.awt.PlatformGraphicsInfo.createGE()) + * + * Since java.awt.GraphicsEnvironment is loaded by the bootstrap class loader, + * all classes used by CTCGraphicsEnvironment also need to be available to the bootstrap class loader, + * as that class loader also loads the CTCInterceptor class, which will instantiate CTCGraphicsEnvironment. + */ + injectClassIntoBootstrapClassLoader( + CTCInterceptor.class, + CTCGraphicsEnvironment.class, + CTCSurfaceManagerFactory.class, + CTCGraphicsConfiguration.class, + PlatformWindowFactory.class, + FullScreenWindowFactory.class, + CTCGraphicsDevice.class, + CTCVolatileSurfaceManager.class); + + ByteBuddy byteBuddy = new ByteBuddy(); + + byteBuddy + .redefine( + TypePool.Default.ofSystemLoader().describe("java.awt.GraphicsEnvironment").resolve(), + ClassFileLocator.ForClassLoader.ofSystemLoader()) + .method(ElementMatchers.named("getLocalGraphicsEnvironment")) + .intercept( + MethodDelegation.to(CTCInterceptor.class)) + .make() + .load( + Object.class.getClassLoader(), + ClassReloadingStrategy.fromInstalledAgent()); + + TypeDescription platformGraphicInfosType; + platformGraphicInfosType = TypePool.Default.ofSystemLoader().describe("sun.awt.PlatformGraphicsInfo").resolve(); + ClassFileLocator locator = ClassFileLocator.ForClassLoader.ofSystemLoader(); + + byteBuddy + .redefine( + platformGraphicInfosType, + locator) + .method( + nameStartsWith("createGE")) + .intercept( + MethodDelegation.to(GraphicsEnvironmentInterceptor.class)) + .make() + .load( + Thread.currentThread().getContextClassLoader(), + ClassReloadingStrategy.fromInstalledAgent()); + + } + + public static class GraphicsEnvironmentInterceptor { + @RuntimeType + public static Object intercept(@Origin Method method, @AllArguments final Object[] args) throws Exception { + return CTCGraphicsEnvironment.getInstance(); } } @@ -100,4 +164,12 @@ public final ConditionEvaluationResult evaluateExecutionCondition(ExtensionConte .map(annotation -> ConditionEvaluationResult.enabled("@GUITest is present")) .orElse(ConditionEvaluationResult.enabled("@GUITest is not present")); } + + private static void injectClassIntoBootstrapClassLoader(Class... classes) throws IOException { + for (Class clazz: classes) { + final byte[] buffer = clazz.getClassLoader().getResourceAsStream(clazz.getName().replace('.', '/').concat(".class")).readAllBytes(); + ClassInjector.UsingUnsafe injector = new ClassInjector.UsingUnsafe(null); + injector.injectRaw(Map.of(clazz.getName(), buffer)); + } + } } diff --git a/pom.xml b/pom.xml index 6319fa8..5c65a0a 100644 --- a/pom.xml +++ b/pom.xml @@ -40,6 +40,10 @@ cacio-tta + + 17 + + ossrh @@ -112,8 +116,8 @@ maven-compiler-plugin 3.8.1 - 17 - 17 + ${cacio.java.version} + ${cacio.java.version} -XDignore.symbol.file=true @@ -130,6 +134,7 @@ false + -XX:+EnableDynamicAgentLoading --add-exports=java.desktop/java.awt=ALL-UNNAMED --add-exports=java.desktop/java.awt.peer=ALL-UNNAMED --add-exports=java.desktop/sun.awt.image=ALL-UNNAMED