diff --git a/paper-api/build.gradle.kts b/paper-api/build.gradle.kts index 3ea140477c..d3bdc0b51b 100644 --- a/paper-api/build.gradle.kts +++ b/paper-api/build.gradle.kts @@ -68,9 +68,6 @@ dependencies { 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") - api("org.apache.maven:maven-resolver-provider:3.9.6") // make API dependency for Paper Plugins compileOnly("org.apache.maven.resolver:maven-resolver-connector-basic:1.9.18") compileOnly("org.apache.maven.resolver:maven-resolver-transport-http:1.9.18") 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 deleted file mode 100644 index 2a169d2f6f..0000000000 --- a/paper-api/src/main/java/com/destroystokyo/paper/event/executor/MethodHandleEventExecutor.java +++ /dev/null @@ -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 eventClass; - private final MethodHandle handle; - private final @Nullable Method method; - - public MethodHandleEventExecutor(final Class eventClass, final MethodHandle handle) { - this.eventClass = eventClass; - this.handle = handle; - this.method = null; - } - - public MethodHandleEventExecutor(final Class 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 + "']"; - } -} 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 deleted file mode 100644 index e98962b6c6..0000000000 --- a/paper-api/src/main/java/com/destroystokyo/paper/event/executor/StaticMethodHandleEventExecutor.java +++ /dev/null @@ -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 eventClass; - private final MethodHandle handle; - private final Method method; - - public StaticMethodHandleEventExecutor(final Class 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 + "']"; - } -} 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 deleted file mode 100644 index abfcb6e838..0000000000 --- a/paper-api/src/main/java/com/destroystokyo/paper/event/executor/asm/ASMEventExecutorGenerator.java +++ /dev/null @@ -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, "", "()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", 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 deleted file mode 100644 index 581561fbd3..0000000000 --- a/paper-api/src/main/java/com/destroystokyo/paper/event/executor/asm/ClassDefiner.java +++ /dev/null @@ -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; - } - -} 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 deleted file mode 100644 index 48bcc72293..0000000000 --- a/paper-api/src/main/java/com/destroystokyo/paper/event/executor/asm/SafeClassDefiner.java +++ /dev/null @@ -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 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/io/papermc/paper/event/executor/EventExecutorFactory.java b/paper-api/src/main/java/io/papermc/paper/event/executor/EventExecutorFactory.java new file mode 100644 index 0000000000..2e3e21c3e9 --- /dev/null +++ b/paper-api/src/main/java/io/papermc/paper/event/executor/EventExecutorFactory.java @@ -0,0 +1,74 @@ +package io.papermc.paper.event.executor; + +import org.bukkit.event.Event; +import org.bukkit.event.Listener; +import org.bukkit.plugin.EventExecutor; +import org.jetbrains.annotations.ApiStatus; +import org.jspecify.annotations.NullMarked; +import java.io.IOException; +import java.io.InputStream; +import java.lang.constant.ConstantDescs; +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; +import java.lang.invoke.MethodType; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.List; +import java.util.Objects; + +@ApiStatus.Internal +@NullMarked +public final class EventExecutorFactory { + private static final byte[] TEMPLATE_CLASS_BYTES; + + static { + try (final InputStream is = EventExecutorFactory.class.getResourceAsStream("MethodHandleEventExecutorTemplate.class")) { + TEMPLATE_CLASS_BYTES = Objects.requireNonNull(is, "template class is missing").readAllBytes(); + } catch (IOException e) { + throw new AssertionError(e); + } + } + + private EventExecutorFactory() { + + } + + /** + * {@return an {@link EventExecutor} implemented by a hidden class calling a method handle} + * + * @param method the method to be invoked by the created event executor + * @param eventClass the class of the event to handle + */ + public static EventExecutor create(final Method method, final Class eventClass) { + final List classData = List.of(method, eventClass); + try { + final MethodHandles.Lookup newClass = MethodHandles.lookup().defineHiddenClassWithClassData(TEMPLATE_CLASS_BYTES, classData, true); + return newClass.lookupClass().asSubclass(EventExecutor.class).getDeclaredConstructor().newInstance(); + } catch (ReflectiveOperationException e) { + throw new AssertionError(e); + } + } + + record ClassData(Method method, MethodHandle methodHandle, Class eventClass) { + + } + + /** + * Extracts the class data and creates an adjusted MethodHandle directly usable by the lookup class. + * The logic is kept here to minimize memory usage per created class. + */ + static ClassData classData(final MethodHandles.Lookup lookup) { + try { + final Method method = MethodHandles.classDataAt(lookup, ConstantDescs.DEFAULT_NAME, Method.class, 0); + MethodHandle mh = lookup.unreflect(method); + if (Modifier.isStatic(method.getModifiers())) { + mh = MethodHandles.dropArguments(mh, 0, Listener.class); + } + mh = mh.asType(MethodType.methodType(void.class, Listener.class, Event.class)); + final Class eventClass = MethodHandles.classDataAt(lookup, ConstantDescs.DEFAULT_NAME, Class.class, 1); + return new ClassData(method, mh, eventClass.asSubclass(Event.class)); + } catch (ReflectiveOperationException e) { + throw new AssertionError(e); + } + } +} diff --git a/paper-api/src/main/java/io/papermc/paper/event/executor/MethodHandleEventExecutorTemplate.java b/paper-api/src/main/java/io/papermc/paper/event/executor/MethodHandleEventExecutorTemplate.java new file mode 100644 index 0000000000..b28fcfed96 --- /dev/null +++ b/paper-api/src/main/java/io/papermc/paper/event/executor/MethodHandleEventExecutorTemplate.java @@ -0,0 +1,56 @@ +package io.papermc.paper.event.executor; + +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.ApiStatus; +import org.jspecify.annotations.NullMarked; +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; +import java.lang.reflect.Method; + +/** + * This class is designed to be used as hidden class template. + * Initializing the class directly will fail due to missing {@code classData}. + * Instead, {@link java.lang.invoke.MethodHandles.Lookup#defineHiddenClassWithClassData(byte[], Object, boolean, MethodHandles.Lookup.ClassOption...)} + * must be used, with the {@code classData} object being a list consisting of two elements: + *
    + *
  1. A {@link Method} representing the event handler method
  2. + *
  3. A {@link Class} representing the event type
  4. + *
+ * The method must take {@link Event} or a subtype of it as its single parameter. + * If the method is non-static, it also needs to reside in a class implementing {@link Listener}. + */ +@SuppressWarnings("unused") +@ApiStatus.Internal +@NullMarked +class MethodHandleEventExecutorTemplate implements EventExecutor { + private static final Method METHOD; + private static final MethodHandle HANDLE; + private static final Class EVENT_CLASS; + + static { + final MethodHandles.Lookup lookup = MethodHandles.lookup(); + final EventExecutorFactory.ClassData classData = EventExecutorFactory.classData(lookup); + METHOD = classData.method(); + HANDLE = classData.methodHandle(); + EVENT_CLASS = classData.eventClass(); + } + + @Override + public void execute(final Listener listener, final Event event) throws EventException { + if (!EVENT_CLASS.isInstance(event)) return; + try { + HANDLE.invokeExact(listener, event); + } catch (Throwable t) { + SneakyThrow.sneaky(t); + } + } + + @Override + public String toString() { + return "MethodHandleEventExecutorTemplate['" + METHOD + "']"; + } +} 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 60e086be70..5aa1c55460 100644 --- a/paper-api/src/main/java/org/bukkit/plugin/EventExecutor.java +++ b/paper-api/src/main/java/org/bukkit/plugin/EventExecutor.java @@ -6,16 +6,10 @@ import org.bukkit.event.Listener; import org.jetbrains.annotations.NotNull; // Paper start +import io.papermc.paper.event.executor.EventExecutorFactory; 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 @@ -26,69 +20,25 @@ 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 (m.getReturnType() != Void.TYPE) { final org.bukkit.plugin.java.JavaPlugin plugin = org.bukkit.plugin.java.JavaPlugin.getProvidingPlugin(m.getDeclaringClass()); org.bukkit.Bukkit.getLogger().warning("@EventHandler method " + m.getDeclaringClass().getName() + (Modifier.isStatic(m.getModifiers()) ? '.' : '#') + m.getName() + " returns non-void type " + m.getReturnType().getName() + ". This is unsupported behavior and will no longer work in a future version of Paper." + " This should be reported to the developers of " + plugin.getPluginMeta().getDisplayName() + " (" + String.join(",", plugin.getPluginMeta().getAuthors()) + ')'); } - 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 new EventExecutor() { - @Override - public void execute(@NotNull Listener listener, @NotNull Event event) throws EventException { - if (!eventClass.isInstance(event)) return; - asmExecutor.execute(listener, event); - } - - @Override - @NotNull - public String toString() { - return "ASMEventExecutor['" + m + "']"; - } - }; - } catch (InstantiationException | IllegalAccessException e) { - throw new AssertionError("Unable to initialize generated event executor", e); - } - } else { - return new MethodHandleEventExecutor(eventClass, m); + if (!m.trySetAccessible()) { + final org.bukkit.plugin.java.JavaPlugin plugin = org.bukkit.plugin.java.JavaPlugin.getProvidingPlugin(m.getDeclaringClass()); + throw new AssertionError( + "@EventHandler method " + m.getDeclaringClass().getName() + (Modifier.isStatic(m.getModifiers()) ? '.' : '#') + m.getName() + " is not accessible." + + " This should be reported to the developers of " + plugin.getDescription().getName() + " (" + String.join(",", plugin.getDescription().getAuthors()) + ')' + ); } + return EventExecutorFactory.create(m, eventClass); } // Paper end } diff --git a/paper-server/src/main/java/org/bukkit/craftbukkit/entity/CraftHumanEntity.java b/paper-server/src/main/java/org/bukkit/craftbukkit/entity/CraftHumanEntity.java index d142009c06..a1f42f860f 100644 --- a/paper-server/src/main/java/org/bukkit/craftbukkit/entity/CraftHumanEntity.java +++ b/paper-server/src/main/java/org/bukkit/craftbukkit/entity/CraftHumanEntity.java @@ -834,7 +834,7 @@ public class CraftHumanEntity extends CraftLivingEntity implements HumanEntity { @Override @Nullable - public Item dropItem(final @Nullable ItemStack itemStack, final boolean throwRandomly, final @Nullable Consumer entityOperation) { + public Item dropItem(final ItemStack itemStack, final boolean throwRandomly, final @Nullable Consumer entityOperation) { // This method implementation differs from the previous dropItem implementations, as it does not source // its itemstack from the players inventory. As such, we cannot reuse #internalDropItemFromInventory. Preconditions.checkArgument(itemStack != null, "Cannot drop a null itemstack");