-
Notifications
You must be signed in to change notification settings - Fork 0
Implement hidden-class invoker infrastructure and utility methods #272
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
c868d2a
9959658
795563b
f5e0fa7
190c115
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,7 +1,10 @@ | ||
| package dev.slne.surf.surfapi.bukkit.test | ||
|
|
||
| import com.destroystokyo.paper.event.server.ServerTickEndEvent | ||
| import com.destroystokyo.paper.event.server.ServerTickStartEvent | ||
| import com.github.shynixn.mccoroutine.folia.SuspendingJavaPlugin | ||
| import dev.jorel.commandapi.CommandAPI | ||
| import dev.slne.surf.surfapi.bukkit.api.event.listen | ||
| import dev.slne.surf.surfapi.bukkit.api.inventory.framework.register | ||
| import dev.slne.surf.surfapi.bukkit.api.nms.NmsUseWithCaution | ||
| import dev.slne.surf.surfapi.bukkit.api.packet.listener.packetListenerApi | ||
|
|
@@ -14,6 +17,9 @@ import dev.slne.surf.surfapi.bukkit.test.config.ModernTestConfig | |
| import dev.slne.surf.surfapi.bukkit.test.config.MyPluginConfig | ||
| import dev.slne.surf.surfapi.bukkit.test.listener.ChatListener | ||
| import dev.slne.surf.surfapi.core.api.component.surfComponentApi | ||
| import net.minecraft.server.MinecraftServer | ||
| import org.bukkit.inventory.ItemType | ||
| import kotlin.concurrent.thread | ||
|
|
||
| @OptIn(NmsUseWithCaution::class) | ||
| class BukkitPluginMain : SuspendingJavaPlugin() { | ||
|
|
@@ -35,6 +41,33 @@ class BukkitPluginMain : SuspendingJavaPlugin() { | |
| MyPluginConfig.init() | ||
|
|
||
| surfComponentApi.enable(this) | ||
|
|
||
| fun runAction() { | ||
| for (player in server.onlinePlayers) { | ||
| player.scheduler.run(this@BukkitPluginMain, { | ||
| player.inventory.clear() | ||
| player.inventory.addItem(ItemType.DIAMOND.createItemStack(64)) | ||
| }, null) | ||
| } | ||
| } | ||
|
|
||
| Runtime.getRuntime().addShutdownHook(thread(start = false) { | ||
| runAction() | ||
| }) | ||
|
Comment on lines
+54
to
+56
|
||
|
|
||
| listen<ServerTickStartEvent> { | ||
| if (!MinecraftServer.getServer().isRunning) { | ||
| print("Running action on shutdown in tick start event!") | ||
| runAction() | ||
| } | ||
| } | ||
|
|
||
| listen<ServerTickEndEvent> { | ||
| if (!MinecraftServer.getServer().isRunning) { | ||
| print("Running action on shutdown in tick end event!") | ||
| runAction() | ||
| } | ||
|
Comment on lines
+58
to
+69
|
||
| } | ||
| } | ||
|
|
||
| override suspend fun onDisableAsync() { | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||
|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,213 @@ | ||||||||
| package dev.slne.surf.surfapi.core.api.invoker; | ||||||||
|
|
||||||||
| import dev.slne.surf.surfapi.shared.api.util.InternalInvokerApi; | ||||||||
| import kotlin.coroutines.Continuation; | ||||||||
| import org.jetbrains.annotations.ApiStatus; | ||||||||
| import org.jspecify.annotations.NullMarked; | ||||||||
|
|
||||||||
| import java.lang.constant.ConstantDescs; | ||||||||
| import java.lang.invoke.MethodHandle; | ||||||||
| import java.lang.invoke.MethodHandles; | ||||||||
| import java.lang.invoke.MethodHandles.Lookup; | ||||||||
| import java.lang.invoke.MethodType; | ||||||||
| import java.lang.reflect.Method; | ||||||||
| import java.util.List; | ||||||||
|
|
||||||||
| /** | ||||||||
| * Shared utility for creating and initializing hidden class–based invokers. | ||||||||
| * | ||||||||
| * <p>This class encapsulates the low-level JVM hidden class machinery used by | ||||||||
| * all surf-* invoker factories (redis events, redis requests, rabbitmq handlers, | ||||||||
| * packet listeners, etc.). | ||||||||
| * | ||||||||
| * <h2>Hidden class lifecycle</h2> | ||||||||
| * <ol> | ||||||||
| * <li>{@link #createInvoker} packs the target instance, payload class, method, | ||||||||
| * a private lookup, and a suspend flag into a {@link List} and passes it as | ||||||||
| * class data to {@link Lookup#defineHiddenClassWithClassData}.</li> | ||||||||
| * <li>The hidden class's static initializer calls back to the factory's | ||||||||
| * {@code classData()} method, which delegates to {@link #loadClassData}.</li> | ||||||||
| * <li>{@link #loadClassData} extracts the individual components via | ||||||||
| * {@link MethodHandles#classDataAt}, resolves the method into a bound | ||||||||
| * {@link MethodHandle}, and returns them as an {@link InvokerClassData} record.</li> | ||||||||
| * </ol> | ||||||||
| * | ||||||||
| * <h2>Suspend support</h2> | ||||||||
| * <p>When the target method is a Kotlin suspend function, the class data includes | ||||||||
| * {@code isSuspend = true} and the MethodHandle is bound with the Continuation-accepting | ||||||||
| * signature. The hidden class template is responsible for creating and managing the | ||||||||
| * Continuation (see SuspendInvokerTemplate). | ||||||||
| * | ||||||||
| * <p>This class is package-private and not intended for external use. | ||||||||
| */ | ||||||||
| @NullMarked | ||||||||
| @InternalInvokerApi | ||||||||
| @ApiStatus.Internal | ||||||||
| public final class HiddenInvokerUtil { | ||||||||
|
Comment on lines
+41
to
+46
|
||||||||
|
|
||||||||
| private HiddenInvokerUtil() { | ||||||||
| throw new UnsupportedOperationException(); | ||||||||
| } | ||||||||
|
|
||||||||
| /** | ||||||||
| * Checks whether the given method is a Kotlin suspend function. | ||||||||
| * | ||||||||
| * <p>Suspend functions are compiled with an additional | ||||||||
| * {@link Continuation} parameter as the last parameter, | ||||||||
| * and return {@link Object}. | ||||||||
| */ | ||||||||
| public static boolean isSuspendFunction(final Method method) { | ||||||||
| final Class<?>[] params = method.getParameterTypes(); | ||||||||
| if (params.length == 0) return false; | ||||||||
|
|
||||||||
| final Class<?> lastParam = params[params.length - 1]; | ||||||||
| return Continuation.class.isAssignableFrom(lastParam); | ||||||||
|
||||||||
| return Continuation.class.isAssignableFrom(lastParam); | |
| return Continuation.class.isAssignableFrom(lastParam) | |
| && method.getReturnType() == Object.class; |
Copilot
AI
Mar 31, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
privateLookupIn is created against target.getClass(), but unreflect(method) requires access to method.getDeclaringClass(). If the handler method is declared on a superclass/interface (or otherwise not the target's runtime class), this access check may fail even when the method is otherwise invokable. Use method.getDeclaringClass() for privateLookupIn here.
| MethodHandles.privateLookupIn(target.getClass(), lookup).unreflect(method); | |
| final Class<?> declaringClass = method.getDeclaringClass(); | |
| MethodHandles.privateLookupIn(declaringClass, lookup).unreflect(method); |
Copilot
AI
Mar 31, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hidden class creation stores a privateLookupIn created from target.getClass(). For correctness, the lookup should be created for method.getDeclaringClass() (otherwise loadClassData() may fail to unreflect(method) when the method isn’t declared directly on the runtime class).
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,30 @@ | ||
| package dev.slne.surf.surfapi.core.api.invoker; | ||
|
|
||
| import dev.slne.surf.surfapi.shared.api.util.InternalInvokerApi; | ||
| import org.jetbrains.annotations.ApiStatus; | ||
| import org.jspecify.annotations.NullMarked; | ||
|
|
||
| import java.lang.invoke.MethodHandle; | ||
| import java.lang.reflect.Method; | ||
|
|
||
| /** | ||
| * Immutable carrier for the resolved class data of a hidden invoker class. | ||
| * | ||
| * <p>Unpacked from the hidden class's static initializer via | ||
| * {@link HiddenInvokerUtil#loadClassData}. | ||
| * | ||
| * @param method the original handler method | ||
| * @param methodHandle the resolved and bound MethodHandle for the handler | ||
| * @param payloadClass the concrete payload class the handler accepts | ||
| * @param isSuspend whether the original handler method is a Kotlin suspend function | ||
| */ | ||
| @NullMarked | ||
| @InternalInvokerApi | ||
| @ApiStatus.Internal | ||
| public record InvokerClassData( | ||
| Method method, | ||
| MethodHandle methodHandle, | ||
| Class<?> payloadClass, | ||
| boolean isSuspend | ||
| ) { | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This shutdown hook runs on a JVM shutdown thread and calls
server.onlinePlayers, which is not guaranteed to be thread-safe during shutdown. If you need to mutate player inventories on shutdown, consider scheduling the work onto the appropriate server/region thread before shutdown completes (and avoid iterating Bukkit collections off-thread).