From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 From: Techcable Date: Thu, 3 Mar 2016 13:20:33 -0700 Subject: [PATCH] Use ASM for event executors. Uses method handles for private or static methods. diff --git a/build.gradle.kts b/build.gradle.kts index 6f31679c2944fae3b37847c683abb0d2e5791449..733fed7b0a7259a831e976d443b907830703bda6 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -38,6 +38,9 @@ dependencies { apiAndDocs("net.kyori:adventure-text-serializer-gson") apiAndDocs("net.kyori:adventure-text-serializer-legacy") apiAndDocs("net.kyori:adventure-text-serializer-plain") + + implementation("org.ow2.asm:asm:9.2") + implementation("org.ow2.asm:asm-commons:9.2") // Paper end compileOnly("org.apache.maven:maven-resolver-provider:3.8.5") diff --git a/src/main/java/com/destroystokyo/paper/event/executor/MethodHandleEventExecutor.java b/src/main/java/com/destroystokyo/paper/event/executor/MethodHandleEventExecutor.java new file mode 100644 index 0000000000000000000000000000000000000000..5b28e9b1daba7834af67dbc193dd656bedd9a994 --- /dev/null +++ b/src/main/java/com/destroystokyo/paper/event/executor/MethodHandleEventExecutor.java @@ -0,0 +1,42 @@ +package com.destroystokyo.paper.event.executor; + +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; +import java.lang.reflect.Method; + +import com.destroystokyo.paper.util.SneakyThrow; +import org.bukkit.event.Event; +import org.bukkit.event.EventException; +import org.bukkit.event.Listener; +import org.bukkit.plugin.EventExecutor; +import org.jetbrains.annotations.NotNull; + +public class MethodHandleEventExecutor implements EventExecutor { + private final Class eventClass; + private final MethodHandle handle; + + public MethodHandleEventExecutor(@NotNull Class eventClass, @NotNull MethodHandle handle) { + this.eventClass = eventClass; + this.handle = handle; + } + + public MethodHandleEventExecutor(@NotNull Class eventClass, @NotNull Method m) { + this.eventClass = eventClass; + try { + m.setAccessible(true); + this.handle = MethodHandles.lookup().unreflect(m); + } catch (IllegalAccessException e) { + throw new AssertionError("Unable to set accessible", e); + } + } + + @Override + public void execute(@NotNull Listener listener, @NotNull Event event) throws EventException { + if (!eventClass.isInstance(event)) return; + try { + handle.invoke(listener, event); + } catch (Throwable t) { + SneakyThrow.sneaky(t); + } + } +} diff --git a/src/main/java/com/destroystokyo/paper/event/executor/StaticMethodHandleEventExecutor.java b/src/main/java/com/destroystokyo/paper/event/executor/StaticMethodHandleEventExecutor.java new file mode 100644 index 0000000000000000000000000000000000000000..c83672427324bd068ed52916f700b68446a226f6 --- /dev/null +++ b/src/main/java/com/destroystokyo/paper/event/executor/StaticMethodHandleEventExecutor.java @@ -0,0 +1,43 @@ +package com.destroystokyo.paper.event.executor; + +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; + +import com.destroystokyo.paper.util.SneakyThrow; +import com.google.common.base.Preconditions; + +import org.bukkit.Bukkit; +import org.bukkit.event.Event; +import org.bukkit.event.EventException; +import org.bukkit.event.Listener; +import org.bukkit.plugin.EventExecutor; +import org.jetbrains.annotations.NotNull; + +public class StaticMethodHandleEventExecutor implements EventExecutor { + private final Class eventClass; + private final MethodHandle handle; + + public StaticMethodHandleEventExecutor(@NotNull Class eventClass, @NotNull 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 (IllegalAccessException e) { + throw new AssertionError("Unable to set accessible", e); + } + } + + @Override + public void execute(@NotNull Listener listener, @NotNull Event event) throws EventException { + if (!eventClass.isInstance(event)) return; + try { + handle.invoke(event); + } catch (Throwable throwable) { + SneakyThrow.sneaky(throwable); + } + } +} diff --git a/src/main/java/com/destroystokyo/paper/event/executor/asm/ASMEventExecutorGenerator.java b/src/main/java/com/destroystokyo/paper/event/executor/asm/ASMEventExecutorGenerator.java new file mode 100644 index 0000000000000000000000000000000000000000..b8d5c13980858dc27fb5383726b7ebcaf14adcb8 --- /dev/null +++ b/src/main/java/com/destroystokyo/paper/event/executor/asm/ASMEventExecutorGenerator.java @@ -0,0 +1,51 @@ +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.NotNull; +import org.objectweb.asm.ClassWriter; +import org.objectweb.asm.Type; +import org.objectweb.asm.commons.GeneratorAdapter; + +import static org.objectweb.asm.Opcodes.*; + +public class ASMEventExecutorGenerator { + @NotNull + public static byte[] generateEventExecutor(@NotNull Method m, @NotNull String name) { + 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, "", "()V", null, null), ACC_PUBLIC, "", "()V"); + methodGenerator.loadThis(); + methodGenerator.visitMethodInsn(INVOKESPECIAL, Type.getInternalName(Object.class), "", "()V", false); // Invoke the super class (Object) constructor + methodGenerator.returnValue(); + methodGenerator.endMethod(); + // Generate the execute method + methodGenerator = new GeneratorAdapter(writer.visitMethod(ACC_PUBLIC, "execute", "(Lorg/bukkit/event/Listener;Lorg/bukkit/event/Event;)V", null, null), ACC_PUBLIC, "execute", "(Lorg/bukkit/event/Listener;Lorg/bukkit/event/Listener;)V"); + 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); + @NotNull + public static String generateName() { + int id = NEXT_ID.getAndIncrement(); + return "com.destroystokyo.paper.event.executor.asm.generated.GeneratedEventExecutor" + id; + } +} diff --git a/src/main/java/com/destroystokyo/paper/event/executor/asm/ClassDefiner.java b/src/main/java/com/destroystokyo/paper/event/executor/asm/ClassDefiner.java new file mode 100644 index 0000000000000000000000000000000000000000..f79685b48bb581277a6891927988b6f7a4389dc4 --- /dev/null +++ b/src/main/java/com/destroystokyo/paper/event/executor/asm/ClassDefiner.java @@ -0,0 +1,34 @@ +package com.destroystokyo.paper.event.executor.asm; + +import org.jetbrains.annotations.NotNull; + +public interface ClassDefiner { + + /** + * Returns if the defined classes can bypass access checks + * + * @return if classes bypass access checks + */ + public 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 + */ + @NotNull + public Class defineClass(@NotNull ClassLoader parentLoader, @NotNull String name, @NotNull byte[] data); + + @NotNull + public static ClassDefiner getInstance() { + return SafeClassDefiner.INSTANCE; + } + +} diff --git a/src/main/java/com/destroystokyo/paper/event/executor/asm/SafeClassDefiner.java b/src/main/java/com/destroystokyo/paper/event/executor/asm/SafeClassDefiner.java new file mode 100644 index 0000000000000000000000000000000000000000..ac99477e9f2c08041aeff31abc1d1edee58d0a67 --- /dev/null +++ b/src/main/java/com/destroystokyo/paper/event/executor/asm/SafeClassDefiner.java @@ -0,0 +1,66 @@ +package com.destroystokyo.paper.event.executor.asm; + +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +import com.google.common.base.Preconditions; + +import com.google.common.collect.MapMaker; +import org.jetbrains.annotations.NotNull; +import org.objectweb.asm.Type; + +public class SafeClassDefiner implements ClassDefiner { + /* default */ static final SafeClassDefiner INSTANCE = new SafeClassDefiner(); + + private SafeClassDefiner() {} + + private final ConcurrentMap loaders = new MapMaker().weakKeys().makeMap(); + + @NotNull + @Override + public Class defineClass(@NotNull ClassLoader parentLoader, @NotNull String name, @NotNull byte[] data) { + GeneratedClassLoader loader = loaders.computeIfAbsent(parentLoader, GeneratedClassLoader::new); + synchronized (loader.getClassLoadingLock(name)) { + Preconditions.checkState(!loader.hasClass(name), "%s already defined", name); + Class c = loader.define(name, data); + assert c.getName().equals(name); + return c; + } + } + + private static class GeneratedClassLoader extends ClassLoader { + static { + ClassLoader.registerAsParallelCapable(); + } + + protected GeneratedClassLoader(@NotNull ClassLoader parent) { + super(parent); + } + + private Class define(@NotNull String name, byte[] data) { + synchronized (getClassLoadingLock(name)) { + assert !hasClass(name); + Class c = defineClass(name, data, 0, data.length); + resolveClass(c); + return c; + } + } + + @Override + @NotNull + public Object getClassLoadingLock(@NotNull String name) { + return super.getClassLoadingLock(name); + } + + public boolean hasClass(@NotNull String name) { + synchronized (getClassLoadingLock(name)) { + try { + Class.forName(name); + return true; + } catch (ClassNotFoundException e) { + return false; + } + } + } + } +} diff --git a/src/main/java/org/bukkit/plugin/EventExecutor.java b/src/main/java/org/bukkit/plugin/EventExecutor.java index a850f0780de05463fc0d3f9e15ff7f19d88b2aed..9026e108ccd3a88aee1267ee275137befa646455 100644 --- a/src/main/java/org/bukkit/plugin/EventExecutor.java +++ b/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> eventExecutorMap = new ConcurrentHashMap>() { + @NotNull + @Override + public Class computeIfAbsent(@NotNull Method key, @NotNull Function> mappingFunction) { + Class 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 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 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 } diff --git a/src/main/java/org/bukkit/plugin/java/JavaPluginLoader.java b/src/main/java/org/bukkit/plugin/java/JavaPluginLoader.java index 5eb24f38f158d43fb42836b83c108f808c89512e..5d74fab03a15d7099e5dacb780eade4cdc185797 100644 --- a/src/main/java/org/bukkit/plugin/java/JavaPluginLoader.java +++ b/src/main/java/org/bukkit/plugin/java/JavaPluginLoader.java @@ -291,21 +291,7 @@ public final class JavaPluginLoader implements PluginLoader { } } - EventExecutor executor = new co.aikar.timings.TimedEventExecutor(new EventExecutor() { // Paper - @Override - public void execute(@NotNull Listener listener, @NotNull Event event) throws EventException { // Paper - try { - if (!eventClass.isAssignableFrom(event.getClass())) { - return; - } - method.invoke(listener, event); - } catch (InvocationTargetException ex) { - throw new EventException(ex.getCause()); - } catch (Throwable t) { - throw new EventException(t); - } - } - }, plugin, method, eventClass); // Paper + EventExecutor executor = new co.aikar.timings.TimedEventExecutor(EventExecutor.create(method, eventClass), plugin, method, eventClass); // Paper // Paper (Yes.) - Use factory method `EventExecutor.create()` if (false) { // Spigot - RL handles useTimings check now eventSet.add(new TimedRegisteredListener(listener, executor, eh.priority(), plugin, eh.ignoreCancelled())); } else {