Skip to content

Commit

Permalink
Java 18+ compatibility using ByteBuddy, Rewrite injection of CTCGraph…
Browse files Browse the repository at this point in the history
…icsEnvironment (#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
  • Loading branch information
janblom authored Feb 19, 2024
1 parent 95fe0c5 commit 36b0d98
Show file tree
Hide file tree
Showing 10 changed files with 143 additions and 26 deletions.
9 changes: 9 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,12 @@ target/

# OS X
.DS_Store

# jenv


# jenv
.java-version

# log files
*.log
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
4 changes: 2 additions & 2 deletions cacio-shared/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,8 @@
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<source>17</source>
<target>17</target>
<source>${cacio.java.version}</source>
<target>${cacio.java.version}</target>
<compilerArgs>
<arg>-XDignore.symbol.file=true</arg>
<arg>--add-exports=java.desktop/java.awt.peer=ALL-UNNAMED</arg>
Expand Down
15 changes: 13 additions & 2 deletions cacio-tta/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,17 @@
<artifactId>jide-oss</artifactId>
<version>3.6.18</version>
</dependency>
<!-- https://mvnrepository.com/artifact/net.bytebuddy/byte-buddy -->
<dependency>
<groupId>net.bytebuddy</groupId>
<artifactId>byte-buddy</artifactId>
<version>1.14.11</version>
</dependency>
<dependency>
<groupId>net.bytebuddy</groupId>
<artifactId>byte-buddy-agent</artifactId>
<version>1.14.11</version>
</dependency>
</dependencies>

<build>
Expand All @@ -54,8 +65,8 @@
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<source>17</source>
<target>17</target>
<source>${cacio.java.version}</source>
<target>${cacio.java.version}</target>
<compilerArgs>
<arg>-XDignore.symbol.file=true</arg>
<arg>--add-exports=java.desktop/java.awt.peer=ALL-UNNAMED</arg>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
@@ -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();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,27 +24,46 @@
*/
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;
import org.junit.platform.commons.util.AnnotationUtils;

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
private static final VarHandle MODIFIERS;

static {
try {
ByteBuddyAgent.install();

var lookup = MethodHandles.privateLookupIn(Field.class, MethodHandles.lookup());
MODIFIERS = lookup.findVarHandle(Field.class, "modifiers", int.class);
} catch (IllegalAccessException | NoSuchFieldException ex) {
Expand All @@ -54,42 +73,87 @@ 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();
}

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();
}
}

Expand All @@ -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));
}
}
}
9 changes: 7 additions & 2 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,10 @@
<module>cacio-tta</module>
</modules>

<properties>
<cacio.java.version>17</cacio.java.version>
</properties>

<distributionManagement>
<snapshotRepository>
<id>ossrh</id>
Expand Down Expand Up @@ -112,8 +116,8 @@
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<source>17</source>
<target>17</target>
<source>${cacio.java.version}</source>
<target>${cacio.java.version}</target>
<compilerArgs>
<arg>-XDignore.symbol.file=true</arg>
</compilerArgs>
Expand All @@ -130,6 +134,7 @@
<java.awt.headless>false</java.awt.headless>
</systemPropertyVariables>
<argLine>
-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
Expand Down

0 comments on commit 36b0d98

Please sign in to comment.