Skip to content

Commit 1d7247c

Browse files
authored
Implement hidden-class invoker infrastructure and utility methods (#272)
2 parents d9dd51f + 190c115 commit 1d7247c

9 files changed

Lines changed: 557 additions & 2 deletions

File tree

gradle.properties

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,6 @@ org.jetbrains.dokka.experimental.gradle.pluginMode=V2Enabled
77
javaVersion=25
88
mcVersion=1.21.11
99
group=dev.slne.surf
10-
version=1.21.11-2.72.1
10+
version=1.21.11-2.73.0
1111
relocationPrefix=dev.slne.surf.surfapi.libs
1212
snapshot=false

surf-api-bukkit/surf-api-bukkit-plugin-test/src/main/kotlin/dev/slne/surf/surfapi/bukkit/test/BukkitPluginMain.kt

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
package dev.slne.surf.surfapi.bukkit.test
22

3+
import com.destroystokyo.paper.event.server.ServerTickEndEvent
4+
import com.destroystokyo.paper.event.server.ServerTickStartEvent
35
import com.github.shynixn.mccoroutine.folia.SuspendingJavaPlugin
46
import dev.jorel.commandapi.CommandAPI
7+
import dev.slne.surf.surfapi.bukkit.api.event.listen
58
import dev.slne.surf.surfapi.bukkit.api.inventory.framework.register
69
import dev.slne.surf.surfapi.bukkit.api.nms.NmsUseWithCaution
710
import dev.slne.surf.surfapi.bukkit.api.packet.listener.packetListenerApi
@@ -14,6 +17,9 @@ import dev.slne.surf.surfapi.bukkit.test.config.ModernTestConfig
1417
import dev.slne.surf.surfapi.bukkit.test.config.MyPluginConfig
1518
import dev.slne.surf.surfapi.bukkit.test.listener.ChatListener
1619
import dev.slne.surf.surfapi.core.api.component.surfComponentApi
20+
import net.minecraft.server.MinecraftServer
21+
import org.bukkit.inventory.ItemType
22+
import kotlin.concurrent.thread
1723

1824
@OptIn(NmsUseWithCaution::class)
1925
class BukkitPluginMain : SuspendingJavaPlugin() {
@@ -35,6 +41,33 @@ class BukkitPluginMain : SuspendingJavaPlugin() {
3541
MyPluginConfig.init()
3642

3743
surfComponentApi.enable(this)
44+
45+
fun runAction() {
46+
for (player in server.onlinePlayers) {
47+
player.scheduler.run(this@BukkitPluginMain, {
48+
player.inventory.clear()
49+
player.inventory.addItem(ItemType.DIAMOND.createItemStack(64))
50+
}, null)
51+
}
52+
}
53+
54+
Runtime.getRuntime().addShutdownHook(thread(start = false) {
55+
runAction()
56+
})
57+
58+
listen<ServerTickStartEvent> {
59+
if (!MinecraftServer.getServer().isRunning) {
60+
print("Running action on shutdown in tick start event!")
61+
runAction()
62+
}
63+
}
64+
65+
listen<ServerTickEndEvent> {
66+
if (!MinecraftServer.getServer().isRunning) {
67+
print("Running action on shutdown in tick end event!")
68+
runAction()
69+
}
70+
}
3871
}
3972

4073
override suspend fun onDisableAsync() {

surf-api-core/surf-api-core-api/api/surf-api-core-api.api

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6530,6 +6530,44 @@ public final class dev/slne/surf/surfapi/core/api/generated/VanillaAdvancementKe
65306530
public static final field UPGRADE_TOOLS Lnet/kyori/adventure/key/Key;
65316531
}
65326532

6533+
public final class dev/slne/surf/surfapi/core/api/invoker/HiddenInvokerUtil {
6534+
public static fun isSuspendFunction (Ljava/lang/reflect/Method;)Z
6535+
public static fun loadClassData (Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/invoke/MethodType;)Ldev/slne/surf/surfapi/core/api/invoker/InvokerClassData;
6536+
public static fun loadClassData (Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;)Ldev/slne/surf/surfapi/core/api/invoker/InvokerClassData;
6537+
public static fun loadClassDataWithAutoSuspend (Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/invoke/MethodType;)Ldev/slne/surf/surfapi/core/api/invoker/InvokerClassData;
6538+
public static fun sneakyThrow (Ljava/lang/Throwable;)V
6539+
}
6540+
6541+
public final class dev/slne/surf/surfapi/core/api/invoker/InvokerClassData : java/lang/Record {
6542+
public fun <init> (Ljava/lang/reflect/Method;Ljava/lang/invoke/MethodHandle;Ljava/lang/Class;Z)V
6543+
public final fun equals (Ljava/lang/Object;)Z
6544+
public final fun hashCode ()I
6545+
public fun isSuspend ()Z
6546+
public fun method ()Ljava/lang/reflect/Method;
6547+
public fun methodHandle ()Ljava/lang/invoke/MethodHandle;
6548+
public fun payloadClass ()Ljava/lang/Class;
6549+
public final fun toString ()Ljava/lang/String;
6550+
}
6551+
6552+
public class dev/slne/surf/surfapi/core/api/invoker/InvokerFactory {
6553+
public fun <init> (Ljava/lang/Class;Ljava/lang/Class;)V
6554+
public fun <init> (Ljava/lang/Class;Ljava/lang/Class;Ljava/lang/invoke/MethodHandles$Lookup;)V
6555+
public fun canAccess (Ljava/lang/Object;Ljava/lang/reflect/Method;)Z
6556+
public fun create (Ljava/lang/Object;Ljava/lang/reflect/Method;Ljava/lang/Class;)Ljava/lang/Object;
6557+
public fun create (Ljava/lang/Object;Ljava/lang/reflect/Method;Ljava/lang/Class;Z)Ljava/lang/Object;
6558+
public fun getLookup ()Ljava/lang/invoke/MethodHandles$Lookup;
6559+
public fun getTemplateClassBytes ()[B
6560+
}
6561+
6562+
public final class dev/slne/surf/surfapi/core/api/invoker/SuspendInvokerSupport {
6563+
public static final field INSTANCE Ldev/slne/surf/surfapi/core/api/invoker/SuspendInvokerSupport;
6564+
public static final fun invokeSuspend (Lkotlinx/coroutines/CoroutineScope;Ljava/lang/invoke/MethodHandle;Ljava/lang/Object;)V
6565+
public static final fun invokeSuspend (Lkotlinx/coroutines/CoroutineScope;Ljava/lang/invoke/MethodHandle;Ljava/lang/Object;Lkotlin/jvm/functions/Function1;)V
6566+
public static synthetic fun invokeSuspend$default (Lkotlinx/coroutines/CoroutineScope;Ljava/lang/invoke/MethodHandle;Ljava/lang/Object;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)V
6567+
public static final fun invokeSuspendDirect (Ljava/lang/invoke/MethodHandle;Ljava/lang/Object;Lkotlin/coroutines/CoroutineContext;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
6568+
public static synthetic fun invokeSuspendDirect$default (Ljava/lang/invoke/MethodHandle;Ljava/lang/Object;Lkotlin/coroutines/CoroutineContext;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object;
6569+
}
6570+
65336571
public final class dev/slne/surf/surfapi/core/api/math/VoxelLineTracer {
65346572
public static final field INSTANCE Ldev/slne/surf/surfapi/core/api/math/VoxelLineTracer;
65356573
public final fun trace (Lorg/spongepowered/math/vector/Vector3d;Lorg/spongepowered/math/vector/Vector3d;)Lkotlin/sequences/Sequence;
Lines changed: 213 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,213 @@
1+
package dev.slne.surf.surfapi.core.api.invoker;
2+
3+
import dev.slne.surf.surfapi.shared.api.util.InternalInvokerApi;
4+
import kotlin.coroutines.Continuation;
5+
import org.jetbrains.annotations.ApiStatus;
6+
import org.jspecify.annotations.NullMarked;
7+
8+
import java.lang.constant.ConstantDescs;
9+
import java.lang.invoke.MethodHandle;
10+
import java.lang.invoke.MethodHandles;
11+
import java.lang.invoke.MethodHandles.Lookup;
12+
import java.lang.invoke.MethodType;
13+
import java.lang.reflect.Method;
14+
import java.util.List;
15+
16+
/**
17+
* Shared utility for creating and initializing hidden class–based invokers.
18+
*
19+
* <p>This class encapsulates the low-level JVM hidden class machinery used by
20+
* all surf-* invoker factories (redis events, redis requests, rabbitmq handlers,
21+
* packet listeners, etc.).
22+
*
23+
* <h2>Hidden class lifecycle</h2>
24+
* <ol>
25+
* <li>{@link #createInvoker} packs the target instance, payload class, method,
26+
* a private lookup, and a suspend flag into a {@link List} and passes it as
27+
* class data to {@link Lookup#defineHiddenClassWithClassData}.</li>
28+
* <li>The hidden class's static initializer calls back to the factory's
29+
* {@code classData()} method, which delegates to {@link #loadClassData}.</li>
30+
* <li>{@link #loadClassData} extracts the individual components via
31+
* {@link MethodHandles#classDataAt}, resolves the method into a bound
32+
* {@link MethodHandle}, and returns them as an {@link InvokerClassData} record.</li>
33+
* </ol>
34+
*
35+
* <h2>Suspend support</h2>
36+
* <p>When the target method is a Kotlin suspend function, the class data includes
37+
* {@code isSuspend = true} and the MethodHandle is bound with the Continuation-accepting
38+
* signature. The hidden class template is responsible for creating and managing the
39+
* Continuation (see SuspendInvokerTemplate).
40+
*
41+
* <p>This class is package-private and not intended for external use.
42+
*/
43+
@NullMarked
44+
@InternalInvokerApi
45+
@ApiStatus.Internal
46+
public final class HiddenInvokerUtil {
47+
48+
private HiddenInvokerUtil() {
49+
throw new UnsupportedOperationException();
50+
}
51+
52+
/**
53+
* Checks whether the given method is a Kotlin suspend function.
54+
*
55+
* <p>Suspend functions are compiled with an additional
56+
* {@link Continuation} parameter as the last parameter,
57+
* and return {@link Object}.
58+
*/
59+
public static boolean isSuspendFunction(final Method method) {
60+
final Class<?>[] params = method.getParameterTypes();
61+
if (params.length == 0) return false;
62+
63+
final Class<?> lastParam = params[params.length - 1];
64+
return Continuation.class.isAssignableFrom(lastParam);
65+
}
66+
67+
/**
68+
* Checks whether a hidden class invoker can be created for the given target and method.
69+
* Validates that privateLookupIn succeeds and the method can be unreflected.
70+
*
71+
* @param target the listener/handler instance
72+
* @param method the handler method
73+
* @param lookup the lookup to use for access checks
74+
* @return true if {@link #createInvoker} will succeed
75+
*/
76+
static boolean canAccess(final Object target, final Method method, final MethodHandles.Lookup lookup) {
77+
try {
78+
MethodHandles.privateLookupIn(target.getClass(), lookup).unreflect(method);
79+
return true;
80+
} catch (IllegalAccessException e) {
81+
return false;
82+
}
83+
}
84+
85+
/**
86+
* Defines a new hidden class from the given template bytecode and returns a new instance
87+
* of the specified invoker interface.
88+
*
89+
* <p>The class data passed to the hidden class consists of:
90+
* <ol>
91+
* <li>The target handler/listener instance</li>
92+
* <li>The payload class (event or request type)</li>
93+
* <li>The handler {@link Method}</li>
94+
* <li>A {@link MethodHandles.Lookup} with private access to the target's class</li>
95+
* <li>A {@link Boolean} indicating whether the method is a suspend function</li>
96+
* </ol>
97+
*
98+
* @param <I> the invoker interface type
99+
* @param lookup the lookup used to define the hidden class
100+
* @param templateBytes the bytecode of the template class
101+
* @param invokerInterface the interface the hidden class implements
102+
* @param target the handler/listener instance to bind the MethodHandle to
103+
* @param method the handler method to invoke
104+
* @param payloadClass the concrete event or request class the handler accepts
105+
* @return a new instance of the hidden class, cast to {@code I}
106+
* @throws ReflectiveOperationException if hidden class definition or instantiation fails
107+
*/
108+
static <I> I createInvoker(
109+
final MethodHandles.Lookup lookup,
110+
final byte[] templateBytes,
111+
final Class<I> invokerInterface,
112+
final Object target,
113+
final Method method,
114+
final Class<?> payloadClass
115+
) throws ReflectiveOperationException {
116+
final boolean isSuspend = isSuspendFunction(method);
117+
return createInvoker(lookup, templateBytes, invokerInterface, target, method, payloadClass, isSuspend);
118+
}
119+
120+
/**
121+
* Overload that allows explicitly specifying the suspend flag.
122+
* Use this when you want to force suspend=false even if the method has a Continuation param.
123+
*/
124+
static <I> I createInvoker(
125+
final MethodHandles.Lookup lookup,
126+
final byte[] templateBytes,
127+
final Class<I> invokerInterface,
128+
final Object target,
129+
final Method method,
130+
final Class<?> payloadClass,
131+
final boolean isSuspend
132+
) throws ReflectiveOperationException {
133+
final MethodHandles.Lookup privateLookupIn = MethodHandles.privateLookupIn(target.getClass(), lookup);
134+
final List<Object> classData = List.of(target, payloadClass, method, privateLookupIn, isSuspend);
135+
final MethodHandles.Lookup hiddenClassLookup = lookup.defineHiddenClassWithClassData(templateBytes, classData, true);
136+
137+
return hiddenClassLookup.lookupClass()
138+
.asSubclass(invokerInterface)
139+
.getDeclaredConstructor()
140+
.newInstance();
141+
}
142+
143+
/**
144+
* Extracts and resolves the class data that was passed to
145+
* {@link MethodHandles.Lookup#defineHiddenClassWithClassData}.
146+
*
147+
* <p>For non-suspend methods, the MethodHandle is bound and type-erased to
148+
* {@code methodType}. For suspend methods, the MethodHandle is bound and
149+
* type-erased to {@code suspendMethodType} (which includes a trailing
150+
* Continuation parameter and returns Object).
151+
*
152+
* @param lookup the hidden class's own lookup
153+
* @param methodType the expected method type for non-suspend handlers
154+
* @param suspendMethodType the expected method type for suspend handlers
155+
* (must include Continuation as last param, return Object)
156+
* @return an {@link InvokerClassData} record
157+
* @throws ReflectiveOperationException if class data extraction or handle resolution fails
158+
*/
159+
public static InvokerClassData loadClassData(
160+
final MethodHandles.Lookup lookup,
161+
final MethodType methodType,
162+
final MethodType suspendMethodType
163+
) throws ReflectiveOperationException {
164+
final Object target = MethodHandles.classDataAt(lookup, ConstantDescs.DEFAULT_NAME, Object.class, 0);
165+
final Class<?> payload = MethodHandles.classDataAt(lookup, ConstantDescs.DEFAULT_NAME, Class.class, 1);
166+
final Method method = MethodHandles.classDataAt(lookup, ConstantDescs.DEFAULT_NAME, Method.class, 2);
167+
final MethodHandles.Lookup privateLookupIn = MethodHandles.classDataAt(lookup, ConstantDescs.DEFAULT_NAME, MethodHandles.Lookup.class, 3);
168+
final boolean isSuspend = MethodHandles.classDataAt(lookup, ConstantDescs.DEFAULT_NAME, Boolean.class, 4);
169+
170+
final MethodType targetType = isSuspend ? suspendMethodType : methodType;
171+
final MethodHandle handle = privateLookupIn.unreflect(method).bindTo(target).asType(targetType);
172+
173+
return new InvokerClassData(method, handle, payload, isSuspend);
174+
}
175+
176+
/**
177+
* Overload for factories that don't support suspend (backward compatible).
178+
* Suspend methods will cause an error at template classData() time.
179+
*/
180+
public static InvokerClassData loadClassData(
181+
final MethodHandles.Lookup lookup,
182+
final MethodType methodType
183+
) throws ReflectiveOperationException {
184+
return loadClassData(lookup, methodType, methodType);
185+
}
186+
187+
/**
188+
* Loads the class data for a hidden invoker class, automatically configuring it for
189+
* compatibility with Kotlin suspend functions, if applicable.
190+
*
191+
* @param lookup the {@link MethodHandles.Lookup} used for access checks and defining
192+
* the hidden class.
193+
* @param methodType the expected {@link MethodType} for non-suspend handler methods.
194+
* @return an {@link InvokerClassData} instance containing the resolved handler method
195+
* information, along with metadata on whether it's a suspend function.
196+
* @throws ReflectiveOperationException if class data extraction or handle resolution fails.
197+
*/
198+
public static InvokerClassData loadClassDataWithAutoSuspend(
199+
final MethodHandles.Lookup lookup,
200+
final MethodType methodType
201+
) throws ReflectiveOperationException {
202+
final MethodType suspendMethodType = methodType.changeReturnType(Object.class).appendParameterTypes(Continuation.class);
203+
return loadClassData(lookup, methodType, suspendMethodType);
204+
}
205+
206+
/**
207+
* Re-throws the given Throwable without requiring a checked exception declaration.
208+
*/
209+
@SuppressWarnings("unchecked")
210+
public static <T extends Throwable> void sneakyThrow(final Throwable t) throws T {
211+
throw (T) t;
212+
}
213+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
package dev.slne.surf.surfapi.core.api.invoker;
2+
3+
import dev.slne.surf.surfapi.shared.api.util.InternalInvokerApi;
4+
import org.jetbrains.annotations.ApiStatus;
5+
import org.jspecify.annotations.NullMarked;
6+
7+
import java.lang.invoke.MethodHandle;
8+
import java.lang.reflect.Method;
9+
10+
/**
11+
* Immutable carrier for the resolved class data of a hidden invoker class.
12+
*
13+
* <p>Unpacked from the hidden class's static initializer via
14+
* {@link HiddenInvokerUtil#loadClassData}.
15+
*
16+
* @param method the original handler method
17+
* @param methodHandle the resolved and bound MethodHandle for the handler
18+
* @param payloadClass the concrete payload class the handler accepts
19+
* @param isSuspend whether the original handler method is a Kotlin suspend function
20+
*/
21+
@NullMarked
22+
@InternalInvokerApi
23+
@ApiStatus.Internal
24+
public record InvokerClassData(
25+
Method method,
26+
MethodHandle methodHandle,
27+
Class<?> payloadClass,
28+
boolean isSuspend
29+
) {
30+
}

0 commit comments

Comments
 (0)