Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,9 @@ build/
/.settings/
.idea
*.log

# Java 25 migration: local test run dirs and IDE module files
run_j17_test/
run_j25_test/
run_prod/
*.iml
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
package io.izzel.arclight.common.mixin.bukkit;

import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import io.izzel.arclight.api.Unsafe;
import io.izzel.arclight.common.bridge.bukkit.JavaPluginLoaderBridge;
import io.izzel.arclight.common.bridge.bukkit.PluginClassLoaderBridge;
import io.izzel.arclight.common.mod.server.ArclightServer;
import io.izzel.arclight.common.mod.util.EventExecutorGenerator;
import io.izzel.arclight.i18n.ArclightConfig;
import org.apache.commons.lang3.Validate;
import org.bukkit.Server;
Expand All @@ -16,24 +14,14 @@
import org.bukkit.plugin.*;
import org.bukkit.plugin.java.JavaPluginLoader;
import org.jetbrains.annotations.NotNull;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.Label;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.Type;
import org.spongepowered.asm.mixin.*;
import org.spongepowered.asm.mixin.gen.Accessor;
import org.spongepowered.asm.mixin.gen.Invoker;

import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.net.URLClassLoader;
import java.util.*;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.logging.Level;

@Mixin(value = JavaPluginLoader.class, remap = false)
Expand All @@ -46,15 +34,6 @@ public abstract class JavaPluginLoaderMixin implements JavaPluginLoaderBridge {
@Accessor("loaders") public abstract<T extends URLClassLoader & PluginClassLoaderBridge> List<T> arclight$getLoaders();
// @formatter:on

private static final AtomicInteger COUNTER = new AtomicInteger();
private static final Cache<Method, Class<? extends EventExecutor>> EXECUTOR_CACHE = CacheBuilder.newBuilder()
.expireAfterAccess(1, TimeUnit.HOURS)
.build();
private static final String HIDDEN_FORM =
Float.parseFloat(System.getProperty("java.class.version")) < 57
? "Ljava/lang/invoke/LambdaForm$Hidden;"
: "Ljdk/internal/vm/annotation/Hidden;";

/**
* @author InitAuther97
* @reason Support plugin class loader isolation
Expand Down Expand Up @@ -156,7 +135,7 @@ public Map<Class<? extends Event>, Set<RegisteredListener>> createRegisteredList
// final CustomTimingsHandler timings = new CustomTimingsHandler("Plugin: " + plugin.getDescription().getFullName() + " Event: " + listener.getClass().getName() + "::" + method.getName() + "(" + eventClass.getSimpleName() + ")", pluginParentTimer); // Spigot

try {
Class<? extends EventExecutor> executorClass = createExecutor(method, eventClass);
Class<? extends EventExecutor> executorClass = EventExecutorGenerator.createExecutor(method, eventClass);
Constructor<? extends EventExecutor> constructor = executorClass.getDeclaredConstructor();
constructor.setAccessible(true);
EventExecutor executor = constructor.newInstance();
Expand All @@ -168,110 +147,4 @@ public Map<Class<? extends Event>, Set<RegisteredListener>> createRegisteredList
return ret;
}

@SuppressWarnings("unchecked")
private Class<? extends EventExecutor> createExecutor(Method method, Class<? extends Event> eventClass) throws ExecutionException {
return EXECUTOR_CACHE.get(method, () -> {
ClassWriter cv = new ClassWriter(ClassWriter.COMPUTE_MAXS);
cv.visit(Opcodes.V1_8,
Opcodes.ACC_SUPER + Opcodes.ACC_SYNTHETIC + Opcodes.ACC_FINAL,
Type.getInternalName(method.getDeclaringClass()) + "$$arclight$" + COUNTER.getAndIncrement(),
null,
Type.getInternalName(Object.class),
new String[]{Type.getInternalName(EventExecutor.class)}
);
cv.visitOuterClass(Type.getInternalName(method.getDeclaringClass()), null, null);
createConstructor(cv);
createImpl(method, eventClass, cv);
cv.visitEnd();
return (Class<? extends EventExecutor>) Unsafe.defineAnonymousClass(method.getDeclaringClass(), cv.toByteArray(), null);
});
}

private void createConstructor(ClassVisitor cv) {
MethodVisitor mv = cv.visitMethod(
Opcodes.ACC_PRIVATE,
"<init>",
"()V",
null, null);
mv.visitCode();
mv.visitVarInsn(Opcodes.ALOAD, 0);
mv.visitMethodInsn(Opcodes.INVOKESPECIAL, Type.getInternalName(Object.class), "<init>", "()V", false);
mv.visitInsn(Opcodes.RETURN);
mv.visitMaxs(-1, -1);
mv.visitEnd();
}

private void createImpl(Method method, Class<? extends Event> eventClass, ClassVisitor cv) {
String ownerType = Type.getInternalName(method.getDeclaringClass());
MethodVisitor mv = cv.visitMethod(
Opcodes.ACC_PUBLIC,
"execute",
Type.getMethodDescriptor(Type.VOID_TYPE, Type.getType(Listener.class), Type.getType(Event.class)),
null, null
);
mv.visitAnnotation(HIDDEN_FORM, true);

Label label0 = new Label();
Label label1 = new Label();
Label label2 = new Label();
mv.visitTryCatchBlock(label0, label1, label2, "java/lang/Throwable");
Label label3 = new Label();
Label label4 = new Label();
// try {
mv.visitTryCatchBlock(label3, label4, label2, "java/lang/Throwable");
// if (!(event instanceof TYPE))
mv.visitLabel(label0);
mv.visitVarInsn(Opcodes.ALOAD, 2);
mv.visitTypeInsn(Opcodes.INSTANCEOF, Type.getInternalName(eventClass));
mv.visitJumpInsn(Opcodes.IFNE, label3);
// return;
mv.visitLabel(label1);
mv.visitInsn(Opcodes.RETURN);
mv.visitLabel(label3);
mv.visitFrame(Opcodes.F_SAME, 0, null, 0, null);
// ((TYPE) listener).<method>(event);
// TYPE.<method>(event);
int invokeCode;
if (Modifier.isStatic(method.getModifiers())) {
invokeCode = Opcodes.INVOKESTATIC;
} else if (method.getDeclaringClass().isInterface()) {
invokeCode = Opcodes.INVOKEINTERFACE;
} else {
invokeCode = Opcodes.INVOKEVIRTUAL;
}
if (invokeCode != Opcodes.INVOKESTATIC) {
mv.visitVarInsn(Opcodes.ALOAD, 1);
mv.visitTypeInsn(Opcodes.CHECKCAST, ownerType);
}
mv.visitVarInsn(Opcodes.ALOAD, 2);
mv.visitTypeInsn(Opcodes.CHECKCAST, Type.getInternalName(eventClass));
mv.visitMethodInsn(invokeCode, ownerType, method.getName(), Type.getMethodDescriptor(method), invokeCode == Opcodes.INVOKEINTERFACE);
int retSize = Type.getType(method.getReturnType()).getSize();
if (retSize > 0) {
mv.visitInsn(Opcodes.POP + retSize - 1);
}
mv.visitLabel(label4);
// } catch (Throwable t) {
Label label5 = new Label();
mv.visitJumpInsn(Opcodes.GOTO, label5);
mv.visitLabel(label2);
mv.visitFrame(Opcodes.F_SAME1, 0, null, 1, new Object[]{"java/lang/Throwable"});
mv.visitVarInsn(Opcodes.ASTORE, 3);
// throw new EventException(t);
Label label6 = new Label();
mv.visitLabel(label6);
mv.visitTypeInsn(Opcodes.NEW, "org/bukkit/event/EventException");
mv.visitInsn(Opcodes.DUP);
mv.visitVarInsn(Opcodes.ALOAD, 3);
mv.visitMethodInsn(Opcodes.INVOKESPECIAL, "org/bukkit/event/EventException", "<init>", "(Ljava/lang/Throwable;)V", false);
mv.visitInsn(Opcodes.ATHROW);
mv.visitLabel(label5);
mv.visitFrame(Opcodes.F_SAME, 0, null, 0, null);
mv.visitInsn(Opcodes.RETURN);
// }
Label label7 = new Label();
mv.visitLabel(label7);
mv.visitMaxs(-1, -1);
mv.visitEnd();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
package io.izzel.arclight.common.mod.util;

import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import io.izzel.arclight.api.Unsafe;
import org.bukkit.event.Event;
import org.bukkit.event.Listener;
import org.bukkit.plugin.EventExecutor;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.Label;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.Type;

import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;

/**
* ASM generation of plugin {@link EventExecutor}s, extracted out of
* {@code JavaPluginLoaderMixin} so that Mixin never has to resolve the
* org.objectweb.asm.* types referenced here. On Java 25 the Mixin bytecode
* provider cannot load org.objectweb.asm.ClassVisitor metadata, which crashed
* the preprocessor when these methods lived inside the mixin. Keeping them in a
* normal class avoids that resolution entirely; the ASM classes still load and
* run fine at runtime.
*/
public final class EventExecutorGenerator {

private EventExecutorGenerator() {
}

private static final AtomicInteger COUNTER = new AtomicInteger();
private static final Cache<Method, Class<? extends EventExecutor>> EXECUTOR_CACHE = CacheBuilder.newBuilder()
.expireAfterAccess(1, TimeUnit.HOURS)
.build();
private static final String HIDDEN_FORM =
Float.parseFloat(System.getProperty("java.class.version")) < 57
? "Ljava/lang/invoke/LambdaForm$Hidden;"
: "Ljdk/internal/vm/annotation/Hidden;";

@SuppressWarnings("unchecked")
public static Class<? extends EventExecutor> createExecutor(Method method, Class<? extends Event> eventClass) throws ExecutionException {
return EXECUTOR_CACHE.get(method, () -> {
ClassWriter cv = new ClassWriter(ClassWriter.COMPUTE_MAXS);
cv.visit(Opcodes.V1_8,
Opcodes.ACC_SUPER + Opcodes.ACC_SYNTHETIC + Opcodes.ACC_FINAL,
Type.getInternalName(method.getDeclaringClass()) + "$$arclight$" + COUNTER.getAndIncrement(),
null,
Type.getInternalName(Object.class),
new String[]{Type.getInternalName(EventExecutor.class)}
);
cv.visitOuterClass(Type.getInternalName(method.getDeclaringClass()), null, null);
createConstructor(cv);
createImpl(method, eventClass, cv);
cv.visitEnd();
return (Class<? extends EventExecutor>) Unsafe.defineAnonymousClass(method.getDeclaringClass(), cv.toByteArray(), null);
});
}

private static void createConstructor(ClassVisitor cv) {
MethodVisitor mv = cv.visitMethod(
Opcodes.ACC_PRIVATE,
"<init>",
"()V",
null, null);
mv.visitCode();
mv.visitVarInsn(Opcodes.ALOAD, 0);
mv.visitMethodInsn(Opcodes.INVOKESPECIAL, Type.getInternalName(Object.class), "<init>", "()V", false);
mv.visitInsn(Opcodes.RETURN);
mv.visitMaxs(-1, -1);
mv.visitEnd();
}

private static void createImpl(Method method, Class<? extends Event> eventClass, ClassVisitor cv) {
String ownerType = Type.getInternalName(method.getDeclaringClass());
MethodVisitor mv = cv.visitMethod(
Opcodes.ACC_PUBLIC,
"execute",
Type.getMethodDescriptor(Type.VOID_TYPE, Type.getType(Listener.class), Type.getType(Event.class)),
null, null
);
mv.visitAnnotation(HIDDEN_FORM, true);

Label label0 = new Label();
Label label1 = new Label();
Label label2 = new Label();
mv.visitTryCatchBlock(label0, label1, label2, "java/lang/Throwable");
Label label3 = new Label();
Label label4 = new Label();
// try {
mv.visitTryCatchBlock(label3, label4, label2, "java/lang/Throwable");
// if (!(event instanceof TYPE))
mv.visitLabel(label0);
mv.visitVarInsn(Opcodes.ALOAD, 2);
mv.visitTypeInsn(Opcodes.INSTANCEOF, Type.getInternalName(eventClass));
mv.visitJumpInsn(Opcodes.IFNE, label3);
// return;
mv.visitLabel(label1);
mv.visitInsn(Opcodes.RETURN);
mv.visitLabel(label3);
mv.visitFrame(Opcodes.F_SAME, 0, null, 0, null);
// ((TYPE) listener).<method>(event);
// TYPE.<method>(event);
int invokeCode;
if (Modifier.isStatic(method.getModifiers())) {
invokeCode = Opcodes.INVOKESTATIC;
} else if (method.getDeclaringClass().isInterface()) {
invokeCode = Opcodes.INVOKEINTERFACE;
} else {
invokeCode = Opcodes.INVOKEVIRTUAL;
}
if (invokeCode != Opcodes.INVOKESTATIC) {
mv.visitVarInsn(Opcodes.ALOAD, 1);
mv.visitTypeInsn(Opcodes.CHECKCAST, ownerType);
}
mv.visitVarInsn(Opcodes.ALOAD, 2);
mv.visitTypeInsn(Opcodes.CHECKCAST, Type.getInternalName(eventClass));
mv.visitMethodInsn(invokeCode, ownerType, method.getName(), Type.getMethodDescriptor(method), invokeCode == Opcodes.INVOKEINTERFACE);
int retSize = Type.getType(method.getReturnType()).getSize();
if (retSize > 0) {
mv.visitInsn(Opcodes.POP + retSize - 1);
}
mv.visitLabel(label4);
// } catch (Throwable t) {
Label label5 = new Label();
mv.visitJumpInsn(Opcodes.GOTO, label5);
mv.visitLabel(label2);
mv.visitFrame(Opcodes.F_SAME1, 0, null, 1, new Object[]{"java/lang/Throwable"});
mv.visitVarInsn(Opcodes.ASTORE, 3);
// throw new EventException(t);
Label label6 = new Label();
mv.visitLabel(label6);
mv.visitTypeInsn(Opcodes.NEW, "org/bukkit/event/EventException");
mv.visitInsn(Opcodes.DUP);
mv.visitVarInsn(Opcodes.ALOAD, 3);
mv.visitMethodInsn(Opcodes.INVOKESPECIAL, "org/bukkit/event/EventException", "<init>", "(Ljava/lang/Throwable;)V", false);
mv.visitInsn(Opcodes.ATHROW);
mv.visitLabel(label5);
mv.visitFrame(Opcodes.F_SAME, 0, null, 0, null);
mv.visitInsn(Opcodes.RETURN);
// }
Label label7 = new Label();
mv.visitLabel(label7);
mv.visitMaxs(-1, -1);
mv.visitEnd();
}
}
4 changes: 3 additions & 1 deletion arclight-forge/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,9 @@ dependencies {
}
embed 'io.izzel.arclight:mixin-tools:1.0.1'
embed "io.izzel:tools:$toolsVersion"
embed "io.izzel.arclight:arclight-api:$apiVersion"
// Java 25: patched arclight-api (Unsafe routed to jdk.internal.misc.Unsafe, StackWalker
// caller-class instead of SecurityManager). See ~/.arclight-proxy/arclight-api-fork.
embed files("libs/arclight-api-${apiVersion}-j25.jar")
embed 'commons-lang:commons-lang:2.6@jar'
embed(project(':i18n-config')) {
transitive = false
Expand Down
Binary file added arclight-forge/libs/arclight-api-1.5.4-j25.jar
Binary file not shown.
25 changes: 25 additions & 0 deletions arclight-forge/patches/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# Vendored patches — Java 25 runtime compatibility

`io.izzel.arclight:arclight-api:1.5.4` is consumed as a binary dependency (its source is not
published). To run on Java 25 it needed patching, so the patched class source is kept here for
review/reproducibility and the rebuilt jar is vendored at
`arclight-forge/libs/arclight-api-1.5.4-j25.jar` (referenced by `arclight-forge/build.gradle`).

## `arclight-api/io/izzel/arclight/api/Unsafe.java`
Patched from the decompiled 1.5.4 class. Changes vs upstream:
- `ensureClassInitialized` / `shouldBeInitialized` are routed to `jdk.internal.misc.Unsafe`
(the `sun.misc.Unsafe` versions were removed in JDK 22+), resolved through the trusted IMPL_LOOKUP.
- The caller-class provider always uses `StackWalker` (the `SecurityManager` constructor throws on
JDK 24+, JEP 486).
- The dead `sun.misc.Unsafe.defineAnonymousClass` fallback branch throws instead (gone since JDK 17).
- Restored the `(Object)` cast on the last `defineAnonymousClass` invokeExact argument (a cast the
decompiler had dropped; without it, plugin event-executor generation threw WrongMethodTypeException).

### Rebuilding the jar
```bash
# compile only Unsafe.java against asm, then swap its classes into a copy of the original jar
javac -XDignore.symbol.file -cp asm-9.x.jar -d out \
patches/arclight-api/io/izzel/arclight/api/Unsafe.java
# (extract original arclight-api-1.5.4.jar, replace io/izzel/arclight/api/Unsafe*.class with the
# freshly compiled ones, re-jar as arclight-api-1.5.4-j25.jar)
```
Loading