From 75561cf00f45b548cb3101c75d889d55918cc6f4 Mon Sep 17 00:00:00 2001 From: IzzelAliz Date: Mon, 22 Jun 2020 15:16:38 +0800 Subject: [PATCH] ASM event executor. --- .../mixin/bukkit/JavaPluginLoaderMixin.java | 241 +++++++++++++++++- .../mixin/bukkit/PluginClassLoaderMixin.java | 9 +- 2 files changed, 242 insertions(+), 8 deletions(-) diff --git a/arclight-common/src/main/java/io/izzel/arclight/common/mixin/bukkit/JavaPluginLoaderMixin.java b/arclight-common/src/main/java/io/izzel/arclight/common/mixin/bukkit/JavaPluginLoaderMixin.java index d9a3b761..33100cba 100644 --- a/arclight-common/src/main/java/io/izzel/arclight/common/mixin/bukkit/JavaPluginLoaderMixin.java +++ b/arclight-common/src/main/java/io/izzel/arclight/common/mixin/bukkit/JavaPluginLoaderMixin.java @@ -1,20 +1,253 @@ 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 org.apache.commons.lang3.Validate; +import org.bukkit.Server; +import org.bukkit.Warning; +import org.bukkit.event.Event; +import org.bukkit.event.EventHandler; +import org.bukkit.event.Listener; +import org.bukkit.plugin.AuthorNagException; +import org.bukkit.plugin.EventExecutor; +import org.bukkit.plugin.Plugin; +import org.bukkit.plugin.RegisteredListener; 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.Final; import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Overwrite; +import org.spongepowered.asm.mixin.Shadow; 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.Arrays; +import java.util.HashMap; +import java.util.HashSet; import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.logging.Level; @Mixin(value = JavaPluginLoader.class, remap = false) -public interface JavaPluginLoaderMixin extends JavaPluginLoaderBridge { +public abstract class JavaPluginLoaderMixin implements JavaPluginLoaderBridge { // @formatter:off - @Invoker("getClassByName") Class bridge$getClassByName(final String name); - @Invoker("setClass") void bridge$setClass(final String name, final Class clazz); - @Accessor("loaders") List bridge$getLoaders(); + @Shadow @Final Server server; + @Invoker("getClassByName") public abstract Class bridge$getClassByName(final String name); + @Invoker("setClass") public abstract void bridge$setClass(final String name, final Class clazz); + @Accessor("loaders") public abstract List bridge$getLoaders(); // @formatter:on + + private static final AtomicInteger COUNTER = new AtomicInteger(); + private static final Cache> EXECUTOR_CACHE = CacheBuilder.newBuilder().build(); + + /** + * @author IzzelAliz + * @reason use asm event executor + */ + @Overwrite + @NotNull + public Map, Set> createRegisteredListeners(@NotNull Listener listener, @NotNull Plugin plugin) { + Validate.notNull(plugin, "Plugin can not be null"); + Validate.notNull(listener, "Listener can not be null"); + + boolean useTimings = server.getPluginManager().useTimings(); + Map, Set> ret = new HashMap<>(); + Set methods; + try { + Method[] publicMethods = listener.getClass().getMethods(); + Method[] privateMethods = listener.getClass().getDeclaredMethods(); + methods = new HashSet<>(publicMethods.length + privateMethods.length, 1.0f); + for (Method method : publicMethods) { + methods.add(method); + } + for (Method method : privateMethods) { + methods.add(method); + } + } catch (NoClassDefFoundError e) { + plugin.getLogger().severe("Plugin " + plugin.getDescription().getFullName() + " has failed to register events for " + listener.getClass() + " because " + e.getMessage() + " does not exist."); + return ret; + } + + for (final Method method : methods) { + final EventHandler eh = method.getAnnotation(EventHandler.class); + if (eh == null) continue; + // Do not register bridge or synthetic methods to avoid event duplication + // Fixes SPIGOT-893 + if (method.isBridge() || method.isSynthetic()) { + continue; + } + final Class checkClass; + if (method.getParameterTypes().length != 1 || !Event.class.isAssignableFrom(checkClass = method.getParameterTypes()[0])) { + plugin.getLogger().severe(plugin.getDescription().getFullName() + " attempted to register an invalid EventHandler method signature \"" + method.toGenericString() + "\" in " + listener.getClass()); + continue; + } + final Class eventClass = checkClass.asSubclass(Event.class); + method.setAccessible(true); + Set eventSet = ret.get(eventClass); + if (eventSet == null) { + eventSet = new HashSet<>(); + ret.put(eventClass, eventSet); + } + + for (Class clazz = eventClass; Event.class.isAssignableFrom(clazz); clazz = clazz.getSuperclass()) { + // This loop checks for extending deprecated events + if (clazz.getAnnotation(Deprecated.class) != null) { + Warning warning = clazz.getAnnotation(Warning.class); + Warning.WarningState warningState = server.getWarningState(); + if (!warningState.printFor(warning)) { + break; + } + plugin.getLogger().log( + Level.WARNING, + String.format( + "\"%s\" has registered a listener for %s on method \"%s\", but the event is Deprecated. \"%s\"; please notify the authors %s.", + plugin.getDescription().getFullName(), + clazz.getName(), + method.toGenericString(), + (warning != null && warning.reason().length() != 0) ? warning.reason() : "Server performance will be affected", + Arrays.toString(plugin.getDescription().getAuthors().toArray())), + warningState == Warning.WarningState.ON ? new AuthorNagException(null) : null); + break; + } + } + + // final CustomTimingsHandler timings = new CustomTimingsHandler("Plugin: " + plugin.getDescription().getFullName() + " Event: " + listener.getClass().getName() + "::" + method.getName() + "(" + eventClass.getSimpleName() + ")", pluginParentTimer); // Spigot + + try { + Class executorClass = createExecutor(method, eventClass); + Constructor constructor = executorClass.getDeclaredConstructor(); + constructor.setAccessible(true); + EventExecutor executor = constructor.newInstance(); + eventSet.add(new RegisteredListener(listener, executor, eh.priority(), plugin, eh.ignoreCancelled())); + } catch (Throwable t) { + t.printStackTrace(); + } + } + return ret; + } + + @SuppressWarnings("unchecked") + private Class createExecutor(Method method, Class 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) Unsafe.defineAnonymousClass(method.getDeclaringClass(), cv.toByteArray(), null); + }); + } + + private void createConstructor(ClassVisitor cv) { + MethodVisitor mv = cv.visitMethod( + Opcodes.ACC_PRIVATE, + "", + "()V", + null, null); + mv.visitCode(); + mv.visitVarInsn(Opcodes.ALOAD, 0); + mv.visitMethodInsn(Opcodes.INVOKESPECIAL, Type.getInternalName(Object.class), "", "()V", false); + mv.visitInsn(Opcodes.RETURN); + mv.visitMaxs(-1, -1); + mv.visitEnd(); + } + + private void createImpl(Method method, Class 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("Ljava/lang/invoke/LambdaForm$Hidden;", 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).(event); + // TYPE.(event); + int invokeCode; + if (Modifier.isStatic(method.getModifiers())) { + invokeCode = Opcodes.INVOKESTATIC; + } else if (method.getDeclaringClass().isInterface()) { + invokeCode = Opcodes.INVOKEINTERFACE; + } else if (Modifier.isPrivate(method.getModifiers())) { + invokeCode = Opcodes.INVOKESPECIAL; + } 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", "", "(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(); + } } diff --git a/arclight-common/src/main/java/io/izzel/arclight/common/mixin/bukkit/PluginClassLoaderMixin.java b/arclight-common/src/main/java/io/izzel/arclight/common/mixin/bukkit/PluginClassLoaderMixin.java index 43139ab1..af44bc1e 100644 --- a/arclight-common/src/main/java/io/izzel/arclight/common/mixin/bukkit/PluginClassLoaderMixin.java +++ b/arclight-common/src/main/java/io/izzel/arclight/common/mixin/bukkit/PluginClassLoaderMixin.java @@ -1,6 +1,9 @@ package io.izzel.arclight.common.mixin.bukkit; import com.google.common.io.ByteStreams; +import io.izzel.arclight.common.bridge.bukkit.JavaPluginLoaderBridge; +import io.izzel.arclight.common.mod.util.remapper.ArclightRemapper; +import io.izzel.arclight.common.mod.util.remapper.PluginRemapper; import org.bukkit.Bukkit; import org.bukkit.plugin.PluginDescriptionFile; import org.bukkit.plugin.java.JavaPluginLoader; @@ -8,8 +11,6 @@ import org.spongepowered.asm.mixin.Final; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.Overwrite; import org.spongepowered.asm.mixin.Shadow; -import io.izzel.arclight.common.mod.util.remapper.ArclightRemapper; -import io.izzel.arclight.common.mod.util.remapper.PluginRemapper; import java.io.IOException; import java.io.InputStream; @@ -53,7 +54,7 @@ public class PluginClassLoaderMixin extends URLClassLoader { if (result == null) { if (checkGlobal) { - result = ((JavaPluginLoaderMixin) (Object) loader).bridge$getClassByName(name); + result = ((JavaPluginLoaderBridge) (Object) loader).bridge$getClassByName(name); } if (result == null) { @@ -104,7 +105,7 @@ public class PluginClassLoaderMixin extends URLClassLoader { } if (result != null) { - ((JavaPluginLoaderMixin) (Object) loader).bridge$setClass(name, result); + ((JavaPluginLoaderBridge) (Object) loader).bridge$setClass(name, result); } }