diff --git a/patches/cpw/mods/fml/common/eventhandler/ASMEventHandler.java.patch b/patches/cpw/mods/fml/common/eventhandler/ASMEventHandler.java.patch new file mode 100644 index 00000000..c76b7e63 --- /dev/null +++ b/patches/cpw/mods/fml/common/eventhandler/ASMEventHandler.java.patch @@ -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) + { diff --git a/patches/cpw/mods/fml/common/eventhandler/EventBus.java.patch b/patches/cpw/mods/fml/common/eventhandler/EventBus.java.patch index 9a6b4147..736ded36 100644 --- a/patches/cpw/mods/fml/common/eventhandler/EventBus.java.patch +++ b/patches/cpw/mods/fml/common/eventhandler/EventBus.java.patch @@ -11,7 +11,29 @@ import org.apache.logging.log4j.Level; import com.google.common.base.Preconditions; -@@ -131,19 +135,44 @@ +@@ -118,6 +122,21 @@ + } + } + ++ public void crucible_register(ASMEventHandler listener, Class eventType, Object target, Method method, ModContainer owner) throws Exception { ++ Constructor ctr = eventType.getConstructor(); ++ ctr.setAccessible(true); ++ Event event = (Event)ctr.newInstance(); ++ event.getListenerList().register(busID, listener.getPriority(), listener); ++ ++ ArrayList others = listeners.get(target); ++ if (others == null) ++ { ++ others = new ArrayList(); ++ listeners.put(target, others); ++ } ++ others.add(listener); ++ } ++ + public void unregister(Object object) + { + ArrayList list = listeners.remove(object); +@@ -131,19 +150,44 @@ public boolean post(Event event) { @@ -66,3 +88,12 @@ } return (event.isCancelable() ? event.isCanceled() : false); } +@@ -158,4 +202,8 @@ + FMLLog.log(Level.ERROR, "%d: %s", x, listeners[x]); + } + } ++ ++ public ConcurrentHashMap> getListeners() { ++ return listeners; ++ } + } diff --git a/src/main/java/io/github/crucible/api/CrucibleEventBus.java b/src/main/java/io/github/crucible/api/CrucibleEventBus.java new file mode 100644 index 00000000..4a71e133 --- /dev/null +++ b/src/main/java/io/github/crucible/api/CrucibleEventBus.java @@ -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 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> 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> 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 getDeclMethod(final Class clz, final Method in) { + try { + return Optional.of(clz.getDeclaredMethod(in.getName(), in.getParameterTypes())); + } catch (NoSuchMethodException nse) { + return Optional.empty(); + } + } +} diff --git a/src/main/java/io/github/crucible/eventfactory/PluginClassLoaderFactory.java b/src/main/java/io/github/crucible/eventfactory/PluginClassLoaderFactory.java new file mode 100644 index 00000000..577eec4a --- /dev/null +++ b/src/main/java/io/github/crucible/eventfactory/PluginClassLoaderFactory.java @@ -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, "", isStatic ? "()V" : "(Ljava/lang/Object;)V", null, null); + mv.visitCode(); + mv.visitVarInsn(ALOAD, 0); + mv.visitMethodInsn(INVOKESPECIAL, "java/lang/Object", "", "()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() + ); + } +} diff --git a/src/main/java/io/github/crucible/unsafe/CrucibleUnsafe.java b/src/main/java/io/github/crucible/unsafe/CrucibleUnsafe.java new file mode 100644 index 00000000..4c8c3217 --- /dev/null +++ b/src/main/java/io/github/crucible/unsafe/CrucibleUnsafe.java @@ -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); + } + +}