Skip to content

Commit fdd020e

Browse files
committed
Create the 'CrucibleEventBus', a clean way to register ForgeEvents on bukkit plugins without having to create a dummy mod.
1 parent eccfcc8 commit fdd020e

File tree

5 files changed

+321
-1
lines changed

5 files changed

+321
-1
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
--- ../src-base/minecraft/cpw/mods/fml/common/eventhandler/ASMEventHandler.java
2+
+++ ../src-work/minecraft/cpw/mods/fml/common/eventhandler/ASMEventHandler.java
3+
@@ -36,6 +36,13 @@
4+
readable = "ASM: " + target + " " + method.getName() + Type.getMethodDescriptor(method);
5+
}
6+
7+
+ public ASMEventHandler(IEventListener handler, Object target, Method method, ModContainer owner, String readable) {
8+
+ this.handler = handler;
9+
+ this.subInfo = method.getAnnotation(SubscribeEvent.class);
10+
+ this.owner = owner;
11+
+ this.readable = readable;
12+
+ }
13+
+
14+
@Override
15+
public void invoke(Event event)
16+
{

patches/cpw/mods/fml/common/eventhandler/EventBus.java.patch

+32-1
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,29 @@
1111
import org.apache.logging.log4j.Level;
1212

1313
import com.google.common.base.Preconditions;
14-
@@ -131,19 +135,44 @@
14+
@@ -118,6 +122,21 @@
15+
}
16+
}
17+
18+
+ public void crucible_register(ASMEventHandler listener, Class<?> eventType, Object target, Method method, ModContainer owner) throws Exception {
19+
+ Constructor<?> ctr = eventType.getConstructor();
20+
+ ctr.setAccessible(true);
21+
+ Event event = (Event)ctr.newInstance();
22+
+ event.getListenerList().register(busID, listener.getPriority(), listener);
23+
+
24+
+ ArrayList<IEventListener> others = listeners.get(target);
25+
+ if (others == null)
26+
+ {
27+
+ others = new ArrayList<IEventListener>();
28+
+ listeners.put(target, others);
29+
+ }
30+
+ others.add(listener);
31+
+ }
32+
+
33+
public void unregister(Object object)
34+
{
35+
ArrayList<IEventListener> list = listeners.remove(object);
36+
@@ -131,19 +150,44 @@
1537

1638
public boolean post(Event event)
1739
{
@@ -66,3 +88,12 @@
6688
}
6789
return (event.isCancelable() ? event.isCanceled() : false);
6890
}
91+
@@ -158,4 +202,8 @@
92+
FMLLog.log(Level.ERROR, "%d: %s", x, listeners[x]);
93+
}
94+
}
95+
+
96+
+ public ConcurrentHashMap<Object, ArrayList<IEventListener>> getListeners() {
97+
+ return listeners;
98+
+ }
99+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
package io.github.crucible.api;
2+
3+
import cpw.mods.fml.common.FMLCommonHandler;
4+
import cpw.mods.fml.common.eventhandler.*;
5+
import io.github.crucible.CrucibleModContainer;
6+
import io.github.crucible.eventfactory.PluginClassLoaderFactory;
7+
import net.minecraftforge.common.MinecraftForge;
8+
import org.bukkit.plugin.Plugin;
9+
import org.objectweb.asm.Type;
10+
11+
import java.lang.reflect.Method;
12+
import java.lang.reflect.Modifier;
13+
import java.util.Arrays;
14+
import java.util.HashSet;
15+
import java.util.Optional;
16+
import java.util.Set;
17+
import java.util.concurrent.ConcurrentHashMap;
18+
19+
public class CrucibleEventBus {
20+
21+
/**
22+
* Register an object to the specific forge event bus!
23+
*
24+
*
25+
* @param plugin The plugin that is registering the event, can be 'null'
26+
*
27+
* @param bus The event bus to register to:
28+
* - You can use {@link MinecraftForge#EVENT_BUS}
29+
* - You can use {@link FMLCommonHandler#bus()}
30+
* - Any other bus.
31+
*
32+
* @param target Either a {@link Class} or an arbitrary object.
33+
* The object maybe have methods annotated with {@link SubscribeEvent}
34+
* The class maybe have static methods annotated with {@link SubscribeEvent}
35+
*/
36+
@SuppressWarnings("unchecked")
37+
public static void register(Plugin plugin, EventBus bus, Object target) {
38+
ConcurrentHashMap<Object, ?> listeners = bus.getListeners();
39+
if (!listeners.containsKey(target)) {
40+
if (target.getClass() == Class.class) {
41+
registerClass(plugin, (Class<?>) target, bus);
42+
} else {
43+
registerObject(plugin, target, bus);
44+
}
45+
}
46+
}
47+
48+
private static void registerClass(Plugin plugin, final Class<?> clazz, EventBus bus) {
49+
Arrays.stream(clazz.getMethods()).
50+
filter(m -> Modifier.isStatic(m.getModifiers())).
51+
filter(m -> m.isAnnotationPresent(SubscribeEvent.class)).
52+
forEach(m -> registerListener(plugin, clazz, m, m, bus));
53+
}
54+
55+
private static void registerObject(Plugin plugin, final Object obj, EventBus bus) {
56+
final HashSet<Class<?>> classes = new HashSet<>();
57+
typesFor(obj.getClass(), classes);
58+
Arrays.stream(obj.getClass().getMethods()).
59+
filter(m -> !Modifier.isStatic(m.getModifiers())).
60+
forEach(m -> classes.stream().
61+
map(c -> getDeclMethod(c, m)).
62+
filter(rm -> rm.isPresent() && rm.get().isAnnotationPresent(SubscribeEvent.class)).
63+
findFirst().
64+
ifPresent(rm -> registerListener(plugin, obj, m, rm.get(), bus)));
65+
}
66+
67+
private static void registerListener(Plugin plugin, final Object target, final Method method, final Method real, EventBus bus) {
68+
Class<?>[] parameterTypes = method.getParameterTypes();
69+
if (parameterTypes.length != 1) {
70+
throw new IllegalArgumentException(
71+
"Method " + method + " has @SubscribeEvent annotation. " +
72+
"It has " + parameterTypes.length + " arguments, " +
73+
"but event handler methods require a single argument only."
74+
);
75+
}
76+
77+
Class<?> eventType = parameterTypes[0];
78+
79+
if (!Event.class.isAssignableFrom(eventType)) {
80+
throw new IllegalArgumentException(
81+
"Method " + method + " has @SubscribeEvent annotation, " +
82+
"but takes an argument that is not an Event subtype : " + eventType);
83+
}
84+
85+
register(plugin, eventType, target, real, bus);
86+
}
87+
88+
private static void register(Plugin plugin, Class<?> eventType, Object target, Method method, EventBus bus) {
89+
String pluginName = (plugin != null ? plugin.getName() : "null");
90+
try {
91+
PluginClassLoaderFactory factory = new PluginClassLoaderFactory();
92+
IEventListener iEventListener = factory.create(method, target);
93+
String readable = "ASM_CRUCIBLE: (Plugin='" + pluginName + "') " + target + " " + method.getName() + Type.getMethodDescriptor(method);
94+
ASMEventHandler asm = new ASMEventHandler(iEventListener, target, method, CrucibleModContainer.instance, readable);
95+
bus.crucible_register(asm, eventType, target, method, CrucibleModContainer.instance);
96+
} catch (Throwable e) {
97+
CrucibleModContainer.logger.error("Error registering event handler for the plugin ({}): {} {}", pluginName, eventType, method, e);
98+
}
99+
}
100+
101+
// -----------------------------------------------------------------------------------------------------------------
102+
// The following methods are utilities that come from isnide the EventBus on modern forge.
103+
// -----------------------------------------------------------------------------------------------------------------
104+
105+
private static void typesFor(final Class<?> clz, final Set<Class<?>> visited) {
106+
if (clz.getSuperclass() == null) return;
107+
typesFor(clz.getSuperclass(),visited);
108+
Arrays.stream(clz.getInterfaces()).forEach(i->typesFor(i, visited));
109+
visited.add(clz);
110+
}
111+
112+
private static Optional<Method> getDeclMethod(final Class<?> clz, final Method in) {
113+
try {
114+
return Optional.of(clz.getDeclaredMethod(in.getName(), in.getParameterTypes()));
115+
} catch (NoSuchMethodException nse) {
116+
return Optional.empty();
117+
}
118+
}
119+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
package io.github.crucible.eventfactory;
2+
3+
import cpw.mods.fml.common.eventhandler.Event;
4+
import cpw.mods.fml.common.eventhandler.IEventListener;
5+
import io.github.crucible.unsafe.CrucibleUnsafe;
6+
import org.objectweb.asm.ClassWriter;
7+
import org.objectweb.asm.MethodVisitor;
8+
import org.objectweb.asm.Type;
9+
10+
import java.lang.reflect.InvocationTargetException;
11+
import java.lang.reflect.Method;
12+
import java.lang.reflect.Modifier;
13+
14+
import static org.objectweb.asm.Opcodes.*;
15+
16+
public class PluginClassLoaderFactory {
17+
private static final String HANDLER_DESC = Type.getInternalName(IEventListener.class);
18+
private static final String HANDLER_FUNC_DESC = Type.getMethodDescriptor(Type.VOID_TYPE, Type.getType(Event.class));
19+
20+
public IEventListener create(Method method, Object target) throws InstantiationException, IllegalAccessException, IllegalArgumentException, InvocationTargetException, NoSuchMethodException, SecurityException {
21+
Class<?> cls = createWrapper(method);
22+
if (Modifier.isStatic(method.getModifiers()))
23+
return (IEventListener)cls.getDeclaredConstructor().newInstance();
24+
else
25+
return (IEventListener)cls.getConstructor(Object.class).newInstance(target);
26+
}
27+
28+
public Class<?> createWrapper(Method callback) {
29+
30+
ClassWriter cw = new ClassWriter(0);
31+
MethodVisitor mv;
32+
33+
boolean isStatic = Modifier.isStatic(callback.getModifiers());
34+
String name = getUniqueName(callback);
35+
String desc = name.replace('.', '/');
36+
String instType = Type.getInternalName(callback.getDeclaringClass());
37+
String eventType = Type.getInternalName(callback.getParameterTypes()[0]);
38+
39+
cw.visit(V1_6, ACC_PUBLIC | ACC_SUPER, desc, null, "java/lang/Object", new String[]{HANDLER_DESC});
40+
41+
cw.visitSource(".dynamic", null);
42+
{
43+
if (!isStatic)
44+
cw.visitField(ACC_PUBLIC, "instance", "Ljava/lang/Object;", null, null).visitEnd();
45+
}
46+
{
47+
mv = cw.visitMethod(ACC_PUBLIC, "<init>", isStatic ? "()V" : "(Ljava/lang/Object;)V", null, null);
48+
mv.visitCode();
49+
mv.visitVarInsn(ALOAD, 0);
50+
mv.visitMethodInsn(INVOKESPECIAL, "java/lang/Object", "<init>", "()V", false);
51+
if (!isStatic) {
52+
mv.visitVarInsn(ALOAD, 0);
53+
mv.visitVarInsn(ALOAD, 1);
54+
mv.visitFieldInsn(PUTFIELD, desc, "instance", "Ljava/lang/Object;");
55+
}
56+
mv.visitInsn(RETURN);
57+
mv.visitMaxs(2, 2);
58+
mv.visitEnd();
59+
}
60+
{
61+
mv = cw.visitMethod(ACC_PUBLIC, "invoke", HANDLER_FUNC_DESC, null, null);
62+
mv.visitCode();
63+
mv.visitVarInsn(ALOAD, 0);
64+
if (!isStatic) {
65+
mv.visitFieldInsn(GETFIELD, desc, "instance", "Ljava/lang/Object;");
66+
mv.visitTypeInsn(CHECKCAST, instType);
67+
}
68+
mv.visitVarInsn(ALOAD, 1);
69+
mv.visitTypeInsn(CHECKCAST, eventType);
70+
mv.visitMethodInsn(isStatic ? INVOKESTATIC : INVOKEVIRTUAL, instType, callback.getName(), Type.getMethodDescriptor(callback), false);
71+
mv.visitInsn(RETURN);
72+
mv.visitMaxs(2, 2);
73+
mv.visitEnd();
74+
}
75+
cw.visitEnd();
76+
byte[] bytes = cw.toByteArray();
77+
return CrucibleUnsafe.defineClass(name, bytes, 0, bytes.length, callback.getDeclaringClass().getClassLoader(), callback.getDeclaringClass().getProtectionDomain());
78+
}
79+
80+
public String getUniqueName(Method callback) {
81+
return String.format("%s.__%s_%s_%s",
82+
callback.getDeclaringClass().getPackage().getName(),
83+
callback.getDeclaringClass().getSimpleName(),
84+
callback.getName(),
85+
callback.getParameterTypes()[0].getSimpleName()
86+
);
87+
}
88+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
package io.github.crucible.unsafe;
2+
3+
import java.lang.invoke.MethodHandle;
4+
import java.lang.invoke.MethodHandles;
5+
import java.lang.reflect.Field;
6+
import java.lang.reflect.Method;
7+
import java.security.ProtectionDomain;
8+
9+
@SuppressWarnings({ "restriction", "sunapi" })
10+
public class CrucibleUnsafe {
11+
12+
private static final sun.misc.Unsafe unsafe;
13+
private static MethodHandles.Lookup lookup;
14+
private static MethodHandle defineClass;
15+
16+
static {
17+
try {
18+
Field theUnsafe = sun.misc.Unsafe.class.getDeclaredField("theUnsafe");
19+
theUnsafe.setAccessible(true);
20+
unsafe = (sun.misc.Unsafe) theUnsafe.get(null);
21+
22+
try {
23+
MethodHandles.Lookup.class.getDeclaredMethod("ensureInitialized", MethodHandles.Lookup.class)
24+
.invoke(MethodHandles.lookup(), MethodHandles.Lookup.class);
25+
Field field = MethodHandles.Lookup.class.getDeclaredField("IMPL_LOOKUP");
26+
Object base = unsafe.staticFieldBase(field);
27+
long offset = unsafe.staticFieldOffset(field);
28+
lookup = (MethodHandles.Lookup) unsafe.getObject(base, offset);
29+
MethodHandle mh;
30+
try {
31+
Method sunMisc = unsafe.getClass().getMethod("defineClass", String.class, byte[].class, int.class, int.class, ClassLoader.class, ProtectionDomain.class);
32+
mh = lookup.unreflect(sunMisc).bindTo(unsafe);
33+
} catch (Exception e) {
34+
Class<?> jdkInternalUnsafe = Class.forName("jdk.internal.misc.Unsafe");
35+
Field internalUnsafeField = jdkInternalUnsafe.getDeclaredField("theUnsafe");
36+
Object internalUnsafe = unsafe.getObject(unsafe.staticFieldBase(internalUnsafeField), unsafe.staticFieldOffset(internalUnsafeField));
37+
Method internalDefineClass = jdkInternalUnsafe.getMethod("defineClass", String.class, byte[].class, int.class, int.class, ClassLoader.class, ProtectionDomain.class);
38+
mh = lookup.unreflect(internalDefineClass).bindTo(internalUnsafe);
39+
}
40+
defineClass = mh;
41+
}catch (Exception ignored){
42+
//This will probably fail on older java versions, but whatever, we don't need it on older versions
43+
}
44+
} catch (Exception e) {
45+
throw new RuntimeException(e);
46+
}
47+
}
48+
49+
public static Class<?> defineClass(String s, byte[] bytes, int i, int i1, ClassLoader classLoader, ProtectionDomain protectionDomain) {
50+
try {
51+
if (defineClass == null){
52+
return unsafe.defineClass(s, bytes, i, i1, classLoader, protectionDomain);
53+
}else {
54+
return (Class<?>) defineClass.invokeExact(s, bytes, i, i1, classLoader, protectionDomain);
55+
}
56+
} catch (Throwable throwable) {
57+
throwException(throwable);
58+
return null;
59+
}
60+
}
61+
62+
public static void throwException(Throwable throwable) {
63+
unsafe.throwException(throwable);
64+
}
65+
66+
}

0 commit comments

Comments
 (0)