-
-
Notifications
You must be signed in to change notification settings - Fork 66
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Create the 'CrucibleEventBus', a clean way to register ForgeEvents on…
… bukkit plugins without having to create a dummy mod.
- Loading branch information
Showing
5 changed files
with
321 additions
and
1 deletion.
There are no files selected for viewing
16 changes: 16 additions & 0 deletions
16
patches/cpw/mods/fml/common/eventhandler/ASMEventHandler.java.patch
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
--- ../src-base/minecraft/cpw/mods/fml/common/eventhandler/ASMEventHandler.java | ||
+++ ../src-work/minecraft/cpw/mods/fml/common/eventhandler/ASMEventHandler.java | ||
@@ -36,6 +36,13 @@ | ||
readable = "ASM: " + target + " " + method.getName() + Type.getMethodDescriptor(method); | ||
} | ||
|
||
+ public ASMEventHandler(IEventListener handler, Object target, Method method, ModContainer owner, String readable) { | ||
+ this.handler = handler; | ||
+ this.subInfo = method.getAnnotation(SubscribeEvent.class); | ||
+ this.owner = owner; | ||
+ this.readable = readable; | ||
+ } | ||
+ | ||
@Override | ||
public void invoke(Event event) | ||
{ |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
119 changes: 119 additions & 0 deletions
119
src/main/java/io/github/crucible/api/CrucibleEventBus.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,119 @@ | ||
package io.github.crucible.api; | ||
|
||
import cpw.mods.fml.common.FMLCommonHandler; | ||
import cpw.mods.fml.common.eventhandler.*; | ||
import io.github.crucible.CrucibleModContainer; | ||
import io.github.crucible.eventfactory.PluginClassLoaderFactory; | ||
import net.minecraftforge.common.MinecraftForge; | ||
import org.bukkit.plugin.Plugin; | ||
import org.objectweb.asm.Type; | ||
|
||
import java.lang.reflect.Method; | ||
import java.lang.reflect.Modifier; | ||
import java.util.Arrays; | ||
import java.util.HashSet; | ||
import java.util.Optional; | ||
import java.util.Set; | ||
import java.util.concurrent.ConcurrentHashMap; | ||
|
||
public class CrucibleEventBus { | ||
|
||
/** | ||
* Register an object to the specific forge event bus! | ||
* | ||
* | ||
* @param plugin The plugin that is registering the event, can be 'null' | ||
* | ||
* @param bus The event bus to register to: | ||
* - You can use {@link MinecraftForge#EVENT_BUS} | ||
* - You can use {@link FMLCommonHandler#bus()} | ||
* - Any other bus. | ||
* | ||
* @param target Either a {@link Class} or an arbitrary object. | ||
* The object maybe have methods annotated with {@link SubscribeEvent} | ||
* The class maybe have static methods annotated with {@link SubscribeEvent} | ||
*/ | ||
@SuppressWarnings("unchecked") | ||
public static void register(Plugin plugin, EventBus bus, Object target) { | ||
ConcurrentHashMap<Object, ?> listeners = bus.getListeners(); | ||
if (!listeners.containsKey(target)) { | ||
if (target.getClass() == Class.class) { | ||
registerClass(plugin, (Class<?>) target, bus); | ||
} else { | ||
registerObject(plugin, target, bus); | ||
} | ||
} | ||
} | ||
|
||
private static void registerClass(Plugin plugin, final Class<?> clazz, EventBus bus) { | ||
Arrays.stream(clazz.getMethods()). | ||
filter(m -> Modifier.isStatic(m.getModifiers())). | ||
filter(m -> m.isAnnotationPresent(SubscribeEvent.class)). | ||
forEach(m -> registerListener(plugin, clazz, m, m, bus)); | ||
} | ||
|
||
private static void registerObject(Plugin plugin, final Object obj, EventBus bus) { | ||
final HashSet<Class<?>> classes = new HashSet<>(); | ||
typesFor(obj.getClass(), classes); | ||
Arrays.stream(obj.getClass().getMethods()). | ||
filter(m -> !Modifier.isStatic(m.getModifiers())). | ||
forEach(m -> classes.stream(). | ||
map(c -> getDeclMethod(c, m)). | ||
filter(rm -> rm.isPresent() && rm.get().isAnnotationPresent(SubscribeEvent.class)). | ||
findFirst(). | ||
ifPresent(rm -> registerListener(plugin, obj, m, rm.get(), bus))); | ||
} | ||
|
||
private static void registerListener(Plugin plugin, final Object target, final Method method, final Method real, EventBus bus) { | ||
Class<?>[] parameterTypes = method.getParameterTypes(); | ||
if (parameterTypes.length != 1) { | ||
throw new IllegalArgumentException( | ||
"Method " + method + " has @SubscribeEvent annotation. " + | ||
"It has " + parameterTypes.length + " arguments, " + | ||
"but event handler methods require a single argument only." | ||
); | ||
} | ||
|
||
Class<?> eventType = parameterTypes[0]; | ||
|
||
if (!Event.class.isAssignableFrom(eventType)) { | ||
throw new IllegalArgumentException( | ||
"Method " + method + " has @SubscribeEvent annotation, " + | ||
"but takes an argument that is not an Event subtype : " + eventType); | ||
} | ||
|
||
register(plugin, eventType, target, real, bus); | ||
} | ||
|
||
private static void register(Plugin plugin, Class<?> eventType, Object target, Method method, EventBus bus) { | ||
String pluginName = (plugin != null ? plugin.getName() : "null"); | ||
try { | ||
PluginClassLoaderFactory factory = new PluginClassLoaderFactory(); | ||
IEventListener iEventListener = factory.create(method, target); | ||
String readable = "ASM_CRUCIBLE: (Plugin='" + pluginName + "') " + target + " " + method.getName() + Type.getMethodDescriptor(method); | ||
ASMEventHandler asm = new ASMEventHandler(iEventListener, target, method, CrucibleModContainer.instance, readable); | ||
bus.crucible_register(asm, eventType, target, method, CrucibleModContainer.instance); | ||
} catch (Throwable e) { | ||
CrucibleModContainer.logger.error("Error registering event handler for the plugin ({}): {} {}", pluginName, eventType, method, e); | ||
} | ||
} | ||
|
||
// ----------------------------------------------------------------------------------------------------------------- | ||
// The following methods are utilities that come from isnide the EventBus on modern forge. | ||
// ----------------------------------------------------------------------------------------------------------------- | ||
|
||
private static void typesFor(final Class<?> clz, final Set<Class<?>> visited) { | ||
if (clz.getSuperclass() == null) return; | ||
typesFor(clz.getSuperclass(),visited); | ||
Arrays.stream(clz.getInterfaces()).forEach(i->typesFor(i, visited)); | ||
visited.add(clz); | ||
} | ||
|
||
private static Optional<Method> getDeclMethod(final Class<?> clz, final Method in) { | ||
try { | ||
return Optional.of(clz.getDeclaredMethod(in.getName(), in.getParameterTypes())); | ||
} catch (NoSuchMethodException nse) { | ||
return Optional.empty(); | ||
} | ||
} | ||
} |
88 changes: 88 additions & 0 deletions
88
src/main/java/io/github/crucible/eventfactory/PluginClassLoaderFactory.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,88 @@ | ||
package io.github.crucible.eventfactory; | ||
|
||
import cpw.mods.fml.common.eventhandler.Event; | ||
import cpw.mods.fml.common.eventhandler.IEventListener; | ||
import io.github.crucible.unsafe.CrucibleUnsafe; | ||
import org.objectweb.asm.ClassWriter; | ||
import org.objectweb.asm.MethodVisitor; | ||
import org.objectweb.asm.Type; | ||
|
||
import java.lang.reflect.InvocationTargetException; | ||
import java.lang.reflect.Method; | ||
import java.lang.reflect.Modifier; | ||
|
||
import static org.objectweb.asm.Opcodes.*; | ||
|
||
public class PluginClassLoaderFactory { | ||
private static final String HANDLER_DESC = Type.getInternalName(IEventListener.class); | ||
private static final String HANDLER_FUNC_DESC = Type.getMethodDescriptor(Type.VOID_TYPE, Type.getType(Event.class)); | ||
|
||
public IEventListener create(Method method, Object target) throws InstantiationException, IllegalAccessException, IllegalArgumentException, InvocationTargetException, NoSuchMethodException, SecurityException { | ||
Class<?> cls = createWrapper(method); | ||
if (Modifier.isStatic(method.getModifiers())) | ||
return (IEventListener)cls.getDeclaredConstructor().newInstance(); | ||
else | ||
return (IEventListener)cls.getConstructor(Object.class).newInstance(target); | ||
} | ||
|
||
public Class<?> createWrapper(Method callback) { | ||
|
||
ClassWriter cw = new ClassWriter(0); | ||
MethodVisitor mv; | ||
|
||
boolean isStatic = Modifier.isStatic(callback.getModifiers()); | ||
String name = getUniqueName(callback); | ||
String desc = name.replace('.', '/'); | ||
String instType = Type.getInternalName(callback.getDeclaringClass()); | ||
String eventType = Type.getInternalName(callback.getParameterTypes()[0]); | ||
|
||
cw.visit(V1_6, ACC_PUBLIC | ACC_SUPER, desc, null, "java/lang/Object", new String[]{HANDLER_DESC}); | ||
|
||
cw.visitSource(".dynamic", null); | ||
{ | ||
if (!isStatic) | ||
cw.visitField(ACC_PUBLIC, "instance", "Ljava/lang/Object;", null, null).visitEnd(); | ||
} | ||
{ | ||
mv = cw.visitMethod(ACC_PUBLIC, "<init>", isStatic ? "()V" : "(Ljava/lang/Object;)V", null, null); | ||
mv.visitCode(); | ||
mv.visitVarInsn(ALOAD, 0); | ||
mv.visitMethodInsn(INVOKESPECIAL, "java/lang/Object", "<init>", "()V", false); | ||
if (!isStatic) { | ||
mv.visitVarInsn(ALOAD, 0); | ||
mv.visitVarInsn(ALOAD, 1); | ||
mv.visitFieldInsn(PUTFIELD, desc, "instance", "Ljava/lang/Object;"); | ||
} | ||
mv.visitInsn(RETURN); | ||
mv.visitMaxs(2, 2); | ||
mv.visitEnd(); | ||
} | ||
{ | ||
mv = cw.visitMethod(ACC_PUBLIC, "invoke", HANDLER_FUNC_DESC, null, null); | ||
mv.visitCode(); | ||
mv.visitVarInsn(ALOAD, 0); | ||
if (!isStatic) { | ||
mv.visitFieldInsn(GETFIELD, desc, "instance", "Ljava/lang/Object;"); | ||
mv.visitTypeInsn(CHECKCAST, instType); | ||
} | ||
mv.visitVarInsn(ALOAD, 1); | ||
mv.visitTypeInsn(CHECKCAST, eventType); | ||
mv.visitMethodInsn(isStatic ? INVOKESTATIC : INVOKEVIRTUAL, instType, callback.getName(), Type.getMethodDescriptor(callback), false); | ||
mv.visitInsn(RETURN); | ||
mv.visitMaxs(2, 2); | ||
mv.visitEnd(); | ||
} | ||
cw.visitEnd(); | ||
byte[] bytes = cw.toByteArray(); | ||
return CrucibleUnsafe.defineClass(name, bytes, 0, bytes.length, callback.getDeclaringClass().getClassLoader(), callback.getDeclaringClass().getProtectionDomain()); | ||
} | ||
|
||
public String getUniqueName(Method callback) { | ||
return String.format("%s.__%s_%s_%s", | ||
callback.getDeclaringClass().getPackage().getName(), | ||
callback.getDeclaringClass().getSimpleName(), | ||
callback.getName(), | ||
callback.getParameterTypes()[0].getSimpleName() | ||
); | ||
} | ||
} |
66 changes: 66 additions & 0 deletions
66
src/main/java/io/github/crucible/unsafe/CrucibleUnsafe.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,66 @@ | ||
package io.github.crucible.unsafe; | ||
|
||
import java.lang.invoke.MethodHandle; | ||
import java.lang.invoke.MethodHandles; | ||
import java.lang.reflect.Field; | ||
import java.lang.reflect.Method; | ||
import java.security.ProtectionDomain; | ||
|
||
@SuppressWarnings({ "restriction", "sunapi" }) | ||
public class CrucibleUnsafe { | ||
|
||
private static final sun.misc.Unsafe unsafe; | ||
private static MethodHandles.Lookup lookup; | ||
private static MethodHandle defineClass; | ||
|
||
static { | ||
try { | ||
Field theUnsafe = sun.misc.Unsafe.class.getDeclaredField("theUnsafe"); | ||
theUnsafe.setAccessible(true); | ||
unsafe = (sun.misc.Unsafe) theUnsafe.get(null); | ||
|
||
try { | ||
MethodHandles.Lookup.class.getDeclaredMethod("ensureInitialized", MethodHandles.Lookup.class) | ||
.invoke(MethodHandles.lookup(), MethodHandles.Lookup.class); | ||
Field field = MethodHandles.Lookup.class.getDeclaredField("IMPL_LOOKUP"); | ||
Object base = unsafe.staticFieldBase(field); | ||
long offset = unsafe.staticFieldOffset(field); | ||
lookup = (MethodHandles.Lookup) unsafe.getObject(base, offset); | ||
MethodHandle mh; | ||
try { | ||
Method sunMisc = unsafe.getClass().getMethod("defineClass", String.class, byte[].class, int.class, int.class, ClassLoader.class, ProtectionDomain.class); | ||
mh = lookup.unreflect(sunMisc).bindTo(unsafe); | ||
} catch (Exception e) { | ||
Class<?> jdkInternalUnsafe = Class.forName("jdk.internal.misc.Unsafe"); | ||
Field internalUnsafeField = jdkInternalUnsafe.getDeclaredField("theUnsafe"); | ||
Object internalUnsafe = unsafe.getObject(unsafe.staticFieldBase(internalUnsafeField), unsafe.staticFieldOffset(internalUnsafeField)); | ||
Method internalDefineClass = jdkInternalUnsafe.getMethod("defineClass", String.class, byte[].class, int.class, int.class, ClassLoader.class, ProtectionDomain.class); | ||
mh = lookup.unreflect(internalDefineClass).bindTo(internalUnsafe); | ||
} | ||
defineClass = mh; | ||
}catch (Exception ignored){ | ||
//This will probably fail on older java versions, but whatever, we don't need it on older versions | ||
} | ||
} catch (Exception e) { | ||
throw new RuntimeException(e); | ||
} | ||
} | ||
|
||
public static Class<?> defineClass(String s, byte[] bytes, int i, int i1, ClassLoader classLoader, ProtectionDomain protectionDomain) { | ||
try { | ||
if (defineClass == null){ | ||
return unsafe.defineClass(s, bytes, i, i1, classLoader, protectionDomain); | ||
}else { | ||
return (Class<?>) defineClass.invokeExact(s, bytes, i, i1, classLoader, protectionDomain); | ||
} | ||
} catch (Throwable throwable) { | ||
throwException(throwable); | ||
return null; | ||
} | ||
} | ||
|
||
public static void throwException(Throwable throwable) { | ||
unsafe.throwException(throwable); | ||
} | ||
|
||
} |