From 844bc6c46a18d5fad58566293864910a211dba4f Mon Sep 17 00:00:00 2001
From: Techcable <Techcable@outlook.com>
Date: Thu, 3 Mar 2016 13:20:33 -0700
Subject: [PATCH] Use ASM for event executors.

Uses method handles for private or static methods.
---
 paper-api/build.gradle.kts                    |  3 +
 .../executor/MethodHandleEventExecutor.java   | 46 +++++++++++++
 .../StaticMethodHandleEventExecutor.java      | 44 +++++++++++++
 .../asm/ASMEventExecutorGenerator.java        | 59 +++++++++++++++++
 .../event/executor/asm/ClassDefiner.java      | 35 ++++++++++
 .../event/executor/asm/SafeClassDefiner.java  | 66 +++++++++++++++++++
 .../java/org/bukkit/plugin/EventExecutor.java | 66 +++++++++++++++++++
 7 files changed, 319 insertions(+)
 create mode 100644 paper-api/src/main/java/com/destroystokyo/paper/event/executor/MethodHandleEventExecutor.java
 create mode 100644 paper-api/src/main/java/com/destroystokyo/paper/event/executor/StaticMethodHandleEventExecutor.java
 create mode 100644 paper-api/src/main/java/com/destroystokyo/paper/event/executor/asm/ASMEventExecutorGenerator.java
 create mode 100644 paper-api/src/main/java/com/destroystokyo/paper/event/executor/asm/ClassDefiner.java
 create mode 100644 paper-api/src/main/java/com/destroystokyo/paper/event/executor/asm/SafeClassDefiner.java

diff --git a/paper-api/build.gradle.kts b/paper-api/build.gradle.kts
index ed5d58d756..43086f7d20 100644
--- a/paper-api/build.gradle.kts
+++ b/paper-api/build.gradle.kts
@@ -61,6 +61,9 @@ dependencies {
     apiAndDocs("net.kyori:adventure-text-serializer-legacy")
     apiAndDocs("net.kyori:adventure-text-serializer-plain")
     apiAndDocs("net.kyori:adventure-text-logger-slf4j")
+
+    implementation("org.ow2.asm:asm:9.7.1")
+    implementation("org.ow2.asm:asm-commons:9.7.1")
     // Paper end
 
     compileOnly("org.apache.maven:maven-resolver-provider:3.9.6")
diff --git a/paper-api/src/main/java/com/destroystokyo/paper/event/executor/MethodHandleEventExecutor.java b/paper-api/src/main/java/com/destroystokyo/paper/event/executor/MethodHandleEventExecutor.java
new file mode 100644
index 0000000000..5a702481d2
--- /dev/null
+++ b/paper-api/src/main/java/com/destroystokyo/paper/event/executor/MethodHandleEventExecutor.java
@@ -0,0 +1,46 @@
+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;
+
+    public MethodHandleEventExecutor(final Class<? extends Event> eventClass, final MethodHandle handle) {
+        this.eventClass = eventClass;
+        this.handle = handle;
+    }
+
+    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);
+        }
+    }
+
+    @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);
+        }
+    }
+}
diff --git a/paper-api/src/main/java/com/destroystokyo/paper/event/executor/StaticMethodHandleEventExecutor.java b/paper-api/src/main/java/com/destroystokyo/paper/event/executor/StaticMethodHandleEventExecutor.java
new file mode 100644
index 0000000000..bbdb5b472d
--- /dev/null
+++ b/paper-api/src/main/java/com/destroystokyo/paper/event/executor/StaticMethodHandleEventExecutor.java
@@ -0,0 +1,44 @@
+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;
+
+    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);
+        }
+    }
+
+    @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);
+        }
+    }
+}
diff --git a/paper-api/src/main/java/com/destroystokyo/paper/event/executor/asm/ASMEventExecutorGenerator.java b/paper-api/src/main/java/com/destroystokyo/paper/event/executor/asm/ASMEventExecutorGenerator.java
new file mode 100644
index 0000000000..abfcb6e838
--- /dev/null
+++ b/paper-api/src/main/java/com/destroystokyo/paper/event/executor/asm/ASMEventExecutorGenerator.java
@@ -0,0 +1,59 @@
+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;
+    }
+}
diff --git a/paper-api/src/main/java/com/destroystokyo/paper/event/executor/asm/ClassDefiner.java b/paper-api/src/main/java/com/destroystokyo/paper/event/executor/asm/ClassDefiner.java
new file mode 100644
index 0000000000..581561fbd3
--- /dev/null
+++ b/paper-api/src/main/java/com/destroystokyo/paper/event/executor/asm/ClassDefiner.java
@@ -0,0 +1,35 @@
+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;
+    }
+
+}
diff --git a/paper-api/src/main/java/com/destroystokyo/paper/event/executor/asm/SafeClassDefiner.java b/paper-api/src/main/java/com/destroystokyo/paper/event/executor/asm/SafeClassDefiner.java
new file mode 100644
index 0000000000..48bcc72293
--- /dev/null
+++ b/paper-api/src/main/java/com/destroystokyo/paper/event/executor/asm/SafeClassDefiner.java
@@ -0,0 +1,66 @@
+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;
+                }
+            }
+        }
+    }
+}
diff --git a/paper-api/src/main/java/org/bukkit/plugin/EventExecutor.java b/paper-api/src/main/java/org/bukkit/plugin/EventExecutor.java
index a850f0780d..9026e108cc 100644
--- a/paper-api/src/main/java/org/bukkit/plugin/EventExecutor.java
+++ b/paper-api/src/main/java/org/bukkit/plugin/EventExecutor.java
@@ -5,9 +5,75 @@ import org.bukkit.event.EventException;
 import org.bukkit.event.Listener;
 import org.jetbrains.annotations.NotNull;
 
+// Paper start
+import java.lang.reflect.Method;
+import java.lang.reflect.Modifier;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+import java.util.function.Function;
+
+import com.destroystokyo.paper.event.executor.MethodHandleEventExecutor;
+import com.destroystokyo.paper.event.executor.StaticMethodHandleEventExecutor;
+import com.destroystokyo.paper.event.executor.asm.ASMEventExecutorGenerator;
+import com.destroystokyo.paper.event.executor.asm.ClassDefiner;
+import com.google.common.base.Preconditions;
+// Paper end
+
 /**
  * Interface which defines the class for event call backs to plugins
  */
 public interface EventExecutor {
     public void execute(@NotNull Listener listener, @NotNull Event event) throws EventException;
+
+    // Paper start
+    ConcurrentMap<Method, Class<? extends EventExecutor>> eventExecutorMap = new ConcurrentHashMap<Method, Class<? extends EventExecutor>>() {
+        @NotNull
+        @Override
+        public Class<? extends EventExecutor> computeIfAbsent(@NotNull Method key, @NotNull Function<? super Method, ? extends Class<? extends EventExecutor>> mappingFunction) {
+            Class<? extends EventExecutor> executorClass = get(key);
+            if (executorClass != null)
+                return executorClass;
+
+            //noinspection SynchronizationOnLocalVariableOrMethodParameter
+            synchronized (key) {
+                executorClass = get(key);
+                if (executorClass != null)
+                    return executorClass;
+
+                return super.computeIfAbsent(key, mappingFunction);
+            }
+        }
+    };
+
+    @NotNull
+    public static EventExecutor create(@NotNull Method m, @NotNull Class<? extends Event> eventClass) {
+        Preconditions.checkNotNull(m, "Null method");
+        Preconditions.checkArgument(m.getParameterCount() != 0, "Incorrect number of arguments %s", m.getParameterCount());
+        Preconditions.checkArgument(m.getParameterTypes()[0] == eventClass, "First parameter %s doesn't match event class %s", m.getParameterTypes()[0], eventClass);
+        ClassDefiner definer = ClassDefiner.getInstance();
+        if (Modifier.isStatic(m.getModifiers())) {
+            return new StaticMethodHandleEventExecutor(eventClass, m);
+        } else if (definer.isBypassAccessChecks() || Modifier.isPublic(m.getDeclaringClass().getModifiers()) && Modifier.isPublic(m.getModifiers())) {
+            // get the existing generated EventExecutor class for the Method or generate one
+            Class<? extends EventExecutor> executorClass = eventExecutorMap.computeIfAbsent(m, (__) -> {
+                String name = ASMEventExecutorGenerator.generateName();
+                byte[] classData = ASMEventExecutorGenerator.generateEventExecutor(m, name);
+                return definer.defineClass(m.getDeclaringClass().getClassLoader(), name, classData).asSubclass(EventExecutor.class);
+            });
+
+            try {
+                EventExecutor asmExecutor = executorClass.newInstance();
+                // Define a wrapper to conform to bukkit stupidity (passing in events that don't match and wrapper exception)
+                return (listener, event) -> {
+                    if (!eventClass.isInstance(event)) return;
+                    asmExecutor.execute(listener, event);
+                };
+            } catch (InstantiationException | IllegalAccessException e) {
+                throw new AssertionError("Unable to initialize generated event executor", e);
+            }
+        } else {
+            return new MethodHandleEventExecutor(eventClass, m);
+        }
+    }
+    // Paper end
 }