Skip to content

Commit

Permalink
Create the 'CrucibleEventBus', a clean way to register ForgeEvents on…
Browse files Browse the repository at this point in the history
… bukkit plugins without having to create a dummy mod.
  • Loading branch information
EverNife committed Oct 4, 2024
1 parent eccfcc8 commit fdd020e
Show file tree
Hide file tree
Showing 5 changed files with 321 additions and 1 deletion.
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)
{
33 changes: 32 additions & 1 deletion patches/cpw/mods/fml/common/eventhandler/EventBus.java.patch
Original file line number Diff line number Diff line change
Expand Up @@ -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<IEventListener> others = listeners.get(target);
+ if (others == null)
+ {
+ others = new ArrayList<IEventListener>();
+ listeners.put(target, others);
+ }
+ others.add(listener);
+ }
+
public void unregister(Object object)
{
ArrayList<IEventListener> list = listeners.remove(object);
@@ -131,19 +150,44 @@

public boolean post(Event event)
{
Expand Down Expand Up @@ -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<Object, ArrayList<IEventListener>> getListeners() {
+ return listeners;
+ }
}
119 changes: 119 additions & 0 deletions src/main/java/io/github/crucible/api/CrucibleEventBus.java
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();
}
}
}
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 src/main/java/io/github/crucible/unsafe/CrucibleUnsafe.java
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);
}

}

0 comments on commit fdd020e

Please sign in to comment.