Use hidden classes for event executors (#11848)
Static final MethodHandles perform similar to direct calls. Additionally, hidden classes simplify logic around ClassLoaders as they can be defined weakly coupled to their defining class loader. All variants of methods (static, private, non-void) can be covered by this mechanism.
This commit is contained in:
@@ -1,54 +0,0 @@
|
||||
package com.destroystokyo.paper.event.executor;
|
||||
|
||||
import com.destroystokyo.paper.util.SneakyThrow;
|
||||
import java.lang.invoke.MethodHandle;
|
||||
import java.lang.invoke.MethodHandles;
|
||||
import java.lang.reflect.Method;
|
||||
import org.bukkit.event.Event;
|
||||
import org.bukkit.event.EventException;
|
||||
import org.bukkit.event.Listener;
|
||||
import org.bukkit.plugin.EventExecutor;
|
||||
import org.jetbrains.annotations.ApiStatus;
|
||||
import org.jspecify.annotations.NullMarked;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
|
||||
@ApiStatus.Internal
|
||||
@NullMarked
|
||||
public class MethodHandleEventExecutor implements EventExecutor {
|
||||
|
||||
private final Class<? extends Event> eventClass;
|
||||
private final MethodHandle handle;
|
||||
private final @Nullable Method method;
|
||||
|
||||
public MethodHandleEventExecutor(final Class<? extends Event> eventClass, final MethodHandle handle) {
|
||||
this.eventClass = eventClass;
|
||||
this.handle = handle;
|
||||
this.method = null;
|
||||
}
|
||||
|
||||
public MethodHandleEventExecutor(final Class<? extends Event> eventClass, final Method m) {
|
||||
this.eventClass = eventClass;
|
||||
try {
|
||||
m.setAccessible(true);
|
||||
this.handle = MethodHandles.lookup().unreflect(m);
|
||||
} catch (final IllegalAccessException e) {
|
||||
throw new AssertionError("Unable to set accessible", e);
|
||||
}
|
||||
this.method = m;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void execute(final Listener listener, final Event event) throws EventException {
|
||||
if (!this.eventClass.isInstance(event)) return;
|
||||
try {
|
||||
this.handle.invoke(listener, event);
|
||||
} catch (final Throwable t) {
|
||||
SneakyThrow.sneaky(t);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "MethodHandleEventExecutor['" + this.method + "']";
|
||||
}
|
||||
}
|
||||
@@ -1,51 +0,0 @@
|
||||
package com.destroystokyo.paper.event.executor;
|
||||
|
||||
import com.destroystokyo.paper.util.SneakyThrow;
|
||||
import com.google.common.base.Preconditions;
|
||||
import java.lang.invoke.MethodHandle;
|
||||
import java.lang.invoke.MethodHandles;
|
||||
import java.lang.reflect.Method;
|
||||
import java.lang.reflect.Modifier;
|
||||
import org.bukkit.event.Event;
|
||||
import org.bukkit.event.EventException;
|
||||
import org.bukkit.event.Listener;
|
||||
import org.bukkit.plugin.EventExecutor;
|
||||
import org.jetbrains.annotations.ApiStatus;
|
||||
import org.jspecify.annotations.NullMarked;
|
||||
|
||||
@ApiStatus.Internal
|
||||
@NullMarked
|
||||
public class StaticMethodHandleEventExecutor implements EventExecutor {
|
||||
|
||||
private final Class<? extends Event> eventClass;
|
||||
private final MethodHandle handle;
|
||||
private final Method method;
|
||||
|
||||
public StaticMethodHandleEventExecutor(final Class<? extends Event> eventClass, final Method m) {
|
||||
Preconditions.checkArgument(Modifier.isStatic(m.getModifiers()), "Not a static method: %s", m);
|
||||
Preconditions.checkArgument(eventClass != null, "eventClass is null");
|
||||
this.eventClass = eventClass;
|
||||
try {
|
||||
m.setAccessible(true);
|
||||
this.handle = MethodHandles.lookup().unreflect(m);
|
||||
} catch (final IllegalAccessException e) {
|
||||
throw new AssertionError("Unable to set accessible", e);
|
||||
}
|
||||
this.method = m;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void execute(final Listener listener, final Event event) throws EventException {
|
||||
if (!this.eventClass.isInstance(event)) return;
|
||||
try {
|
||||
this.handle.invoke(event);
|
||||
} catch (final Throwable throwable) {
|
||||
SneakyThrow.sneaky(throwable);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "StaticMethodHandleEventExecutor['" + this.method + "']";
|
||||
}
|
||||
}
|
||||
@@ -1,59 +0,0 @@
|
||||
package com.destroystokyo.paper.event.executor.asm;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import org.bukkit.plugin.EventExecutor;
|
||||
import org.jetbrains.annotations.ApiStatus;
|
||||
import org.jspecify.annotations.NullMarked;
|
||||
import org.objectweb.asm.ClassWriter;
|
||||
import org.objectweb.asm.Type;
|
||||
import org.objectweb.asm.commons.GeneratorAdapter;
|
||||
|
||||
import static org.objectweb.asm.Opcodes.ACC_PUBLIC;
|
||||
import static org.objectweb.asm.Opcodes.INVOKEINTERFACE;
|
||||
import static org.objectweb.asm.Opcodes.INVOKESPECIAL;
|
||||
import static org.objectweb.asm.Opcodes.INVOKEVIRTUAL;
|
||||
import static org.objectweb.asm.Opcodes.V1_8;
|
||||
|
||||
@ApiStatus.Internal
|
||||
@NullMarked
|
||||
public final class ASMEventExecutorGenerator {
|
||||
|
||||
private static final String EXECUTE_DESCRIPTOR = "(Lorg/bukkit/event/Listener;Lorg/bukkit/event/Event;)V";
|
||||
|
||||
public static byte[] generateEventExecutor(final Method m, final String name) {
|
||||
final ClassWriter writer = new ClassWriter(ClassWriter.COMPUTE_FRAMES | ClassWriter.COMPUTE_MAXS);
|
||||
writer.visit(V1_8, ACC_PUBLIC, name.replace('.', '/'), null, Type.getInternalName(Object.class), new String[]{Type.getInternalName(EventExecutor.class)});
|
||||
// Generate constructor
|
||||
GeneratorAdapter methodGenerator = new GeneratorAdapter(writer.visitMethod(ACC_PUBLIC, "<init>", "()V", null, null), ACC_PUBLIC, "<init>", "()V");
|
||||
methodGenerator.loadThis();
|
||||
methodGenerator.visitMethodInsn(INVOKESPECIAL, Type.getInternalName(Object.class), "<init>", "()V", false); // Invoke the super class (Object) constructor
|
||||
methodGenerator.returnValue();
|
||||
methodGenerator.endMethod();
|
||||
// Generate the execute method
|
||||
methodGenerator = new GeneratorAdapter(writer.visitMethod(ACC_PUBLIC, "execute", EXECUTE_DESCRIPTOR, null, null), ACC_PUBLIC, "execute", EXECUTE_DESCRIPTOR);
|
||||
methodGenerator.loadArg(0);
|
||||
methodGenerator.checkCast(Type.getType(m.getDeclaringClass()));
|
||||
methodGenerator.loadArg(1);
|
||||
methodGenerator.checkCast(Type.getType(m.getParameterTypes()[0]));
|
||||
methodGenerator.visitMethodInsn(m.getDeclaringClass().isInterface() ? INVOKEINTERFACE : INVOKEVIRTUAL, Type.getInternalName(m.getDeclaringClass()), m.getName(), Type.getMethodDescriptor(m), m.getDeclaringClass().isInterface());
|
||||
// The only purpose of this switch statement is to generate the correct pop instruction, should the event handler method return something other than void.
|
||||
// Non-void event handlers will be unsupported in a future release.
|
||||
switch (Type.getType(m.getReturnType()).getSize()) {
|
||||
// case 0 is omitted because the only type that has size 0 is void - no pop instruction needed.
|
||||
case 1 -> methodGenerator.pop(); // handles reference types and most primitives
|
||||
case 2 -> methodGenerator.pop2(); // handles long and double
|
||||
}
|
||||
methodGenerator.returnValue();
|
||||
methodGenerator.endMethod();
|
||||
writer.visitEnd();
|
||||
return writer.toByteArray();
|
||||
}
|
||||
|
||||
public static AtomicInteger NEXT_ID = new AtomicInteger(1);
|
||||
|
||||
public static String generateName() {
|
||||
final int id = NEXT_ID.getAndIncrement();
|
||||
return "com.destroystokyo.paper.event.executor.asm.generated.GeneratedEventExecutor" + id;
|
||||
}
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
package com.destroystokyo.paper.event.executor.asm;
|
||||
|
||||
import org.jetbrains.annotations.ApiStatus;
|
||||
import org.jspecify.annotations.NullMarked;
|
||||
|
||||
@ApiStatus.Internal
|
||||
@NullMarked
|
||||
public interface ClassDefiner {
|
||||
|
||||
/**
|
||||
* Returns if the defined classes can bypass access checks
|
||||
*
|
||||
* @return if classes bypass access checks
|
||||
*/
|
||||
default boolean isBypassAccessChecks() {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Define a class
|
||||
*
|
||||
* @param parentLoader the parent classloader
|
||||
* @param name the name of the class
|
||||
* @param data the class data to load
|
||||
* @return the defined class
|
||||
* @throws ClassFormatError if the class data is invalid
|
||||
* @throws NullPointerException if any of the arguments are null
|
||||
*/
|
||||
Class<?> defineClass(ClassLoader parentLoader, String name, byte[] data);
|
||||
|
||||
static ClassDefiner getInstance() {
|
||||
return SafeClassDefiner.INSTANCE;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,66 +0,0 @@
|
||||
package com.destroystokyo.paper.event.executor.asm;
|
||||
|
||||
import com.google.common.base.Preconditions;
|
||||
import com.google.common.collect.MapMaker;
|
||||
import java.util.concurrent.ConcurrentMap;
|
||||
import org.jetbrains.annotations.ApiStatus;
|
||||
import org.jspecify.annotations.NullMarked;
|
||||
|
||||
@ApiStatus.Internal
|
||||
@NullMarked
|
||||
public class SafeClassDefiner implements ClassDefiner {
|
||||
|
||||
/* default */ static final SafeClassDefiner INSTANCE = new SafeClassDefiner();
|
||||
|
||||
private SafeClassDefiner() {
|
||||
}
|
||||
|
||||
private final ConcurrentMap<ClassLoader, GeneratedClassLoader> loaders = new MapMaker().weakKeys().makeMap();
|
||||
|
||||
@Override
|
||||
public Class<?> defineClass(final ClassLoader parentLoader, final String name, final byte[] data) {
|
||||
final GeneratedClassLoader loader = this.loaders.computeIfAbsent(parentLoader, GeneratedClassLoader::new);
|
||||
synchronized (loader.getClassLoadingLock(name)) {
|
||||
Preconditions.checkState(!loader.hasClass(name), "%s already defined", name);
|
||||
final Class<?> c = loader.define(name, data);
|
||||
assert c.getName().equals(name);
|
||||
return c;
|
||||
}
|
||||
}
|
||||
|
||||
private static class GeneratedClassLoader extends ClassLoader {
|
||||
|
||||
static {
|
||||
ClassLoader.registerAsParallelCapable();
|
||||
}
|
||||
|
||||
protected GeneratedClassLoader(final ClassLoader parent) {
|
||||
super(parent);
|
||||
}
|
||||
|
||||
private Class<?> define(final String name, final byte[] data) {
|
||||
synchronized (this.getClassLoadingLock(name)) {
|
||||
assert !this.hasClass(name);
|
||||
final Class<?> c = this.defineClass(name, data, 0, data.length);
|
||||
this.resolveClass(c);
|
||||
return c;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getClassLoadingLock(final String name) {
|
||||
return super.getClassLoadingLock(name);
|
||||
}
|
||||
|
||||
public boolean hasClass(final String name) {
|
||||
synchronized (this.getClassLoadingLock(name)) {
|
||||
try {
|
||||
Class.forName(name);
|
||||
return true;
|
||||
} catch (final ClassNotFoundException e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user