summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
author zaaarf <zaaarf@proton.me>2023-02-03 17:45:28 +0100
committer zaaarf <zaaarf@proton.me>2023-02-03 17:45:28 +0100
commit26e52da7d82da3a729e74a8522265cd99ca544c3 (patch)
tree5dcfa5371d8cbc207f1a5736e118b13737a8a1a9
parent7b201195a1435962a8d01bdd0b8c10c10eb0c324 (diff)
feat: initial, low quality and untested implementation of method transformer
-rw-r--r--src/main/java/bscv/asm/BoSCoVicinoLoader.java173
-rw-r--r--src/main/java/bscv/asm/api/AnnotationChecker.java37
-rw-r--r--src/main/java/bscv/asm/api/annotations/Inject.java18
-rw-r--r--src/main/java/bscv/asm/api/annotations/Patch.java12
4 files changed, 232 insertions, 8 deletions
diff --git a/src/main/java/bscv/asm/BoSCoVicinoLoader.java b/src/main/java/bscv/asm/BoSCoVicinoLoader.java
index a41e883..77d5f6e 100644
--- a/src/main/java/bscv/asm/BoSCoVicinoLoader.java
+++ b/src/main/java/bscv/asm/BoSCoVicinoLoader.java
@@ -1,32 +1,189 @@
package bscv.asm;
-import java.io.File;
import java.io.IOException;
-import java.util.EnumSet;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.util.*;
+import java.util.stream.Collectors;
+import bscv.asm.api.AnnotationChecker;
+import bscv.asm.api.annotations.Inject;
+import bscv.asm.api.annotations.Patch;
+import com.google.common.collect.HashMultimap;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
+import org.objectweb.asm.ClassReader;
import org.objectweb.asm.Type;
import cpw.mods.modlauncher.serviceapi.ILaunchPluginService;
+import org.objectweb.asm.tree.ClassNode;
+import org.objectweb.asm.tree.MethodNode;
public class BoSCoVicinoLoader implements ILaunchPluginService {
- public static Logger LOGGER = LogManager.getLogger("BSCV-ASM");
+ public static final String NAME = "boscovicino_asm"; //temp name
+ public static final Logger LOGGER = LogManager.getLogger("BoSCoVicinoASM");
+
+ private static final EnumSet<Phase> YAY = EnumSet.of(Phase.BEFORE);
+ private static final EnumSet<Phase> NAY = EnumSet.noneOf(Phase.class);
+
+ private static final HashMultimap<String, Class<?>> patchClasses = HashMultimap.create();
public BoSCoVicinoLoader() {
- LOGGER.info("BoSCoVicinoLoader instantiation");
+ LOGGER.info("BoSCoVicinoLoader instantiated successfully!");
}
@Override
public String name() {
- return "boscovicino_asm";
+ return NAME;
+ }
+
+ @Override
+ public EnumSet<Phase> handlesClass(Type classType, final boolean isEmpty) {
+ //if(!isEmpty && shouldHandle(classType))
+ // LOGGER.info("CLAZZ >>> {}", classType.getClassName());
+ //return NAY;
+ throw new IllegalStateException("Outdated ModLauncher"); //mixin does it
}
@Override
- public EnumSet<ILaunchPluginService.Phase> handlesClass(Type classType, final boolean isEmpty) {
- LOGGER.info(String.format("CLAZZ >>> %s", classType.getClassName()));
- return EnumSet.noneOf(Phase.class);
+ public EnumSet<Phase> handlesClass(Type classType, final boolean isEmpty, final String reason) {
+ if(isEmpty) return NAY;
+ String name = classType.getClassName();
+ if(name.startsWith("net.minecraft.") || name.indexOf('.') == -1)
+ return YAY;
+ else {
+ try {
+ if(hasAnnotation(name)) {
+ Class<?> patch = Class.forName(name);
+ patchClasses.put(Type.getDescriptor(patch.getAnnotation(Patch.class).value()), patch);
+ return YAY;
+ }
+ } catch(IOException | ClassNotFoundException e) {
+ LOGGER.debug("Could not load {}", name);
+ }
+ return NAY;
+ }
+ }
+
+ @Override
+ public int processClassWithFlags(Phase phase, ClassNode classNode, Type classType, String reason) {
+ if(patchClasses.containsKey(classType.getDescriptor())) {
+ //can have multiple patches working on same class
+ //each of these classes may attempt to patch a method one or more times
+ Set<String> classMethodNames = classNode.methods
+ .stream()
+ .map(m -> m.name)
+ .collect(Collectors.toSet());
+ Set<String> classMethodDescriptors = classNode.methods
+ .stream()
+ .map(m -> m.desc)
+ .collect(Collectors.toSet());
+ Set<Class<?>> relevantPatches = patchClasses.get(classType.getDescriptor());
+ HashMultimap<Object, Method> patches = HashMultimap.create();
+ relevantPatches
+ .forEach(p -> {
+ Set<Method> injectors =
+ getInjectorsInPatch(p)
+ .stream()
+ .filter(m -> {
+ Inject a = m.getAnnotation(Inject.class);
+ return
+ classMethodNames.contains(a.methodName())
+ && classMethodDescriptors.contains(descriptorBuilder(a.returnType(), a.parameters()));
+ })
+ .collect(Collectors.toSet());
+ if(!injectors.isEmpty()) {
+ try {
+ patches.putAll(p.newInstance(), injectors);
+ } catch(InstantiationException | IllegalAccessException e) {
+ throw new RuntimeException(e); //todo: better catch
+ }
+ }
+ });
+ if(patches.isEmpty())
+ return ComputeFlags.NO_REWRITE;
+ else {
+ boolean success = false;
+ for(Object p : patches.keys())
+ for(Method m : patches.get(p))
+ success = success || processInjector(classNode, p, m);
+ return success ? ComputeFlags.COMPUTE_FRAMES : ComputeFlags.NO_REWRITE; //todo: is this the right flag?
+ }
+ } else return ComputeFlags.NO_REWRITE;
+ }
+
+ private boolean hasAnnotation(String name) throws IOException {
+ ClassReader cr = new ClassReader(name);
+ AnnotationChecker ac = new AnnotationChecker(Patch.class);
+ cr.accept(ac, 0);
+ return ac.isAnnotationPresent();
+ }
+
+ private Set<Method> getInjectorsInPatch(Class<?> patch) {
+ return Arrays.stream(patch.getDeclaredMethods())
+ .filter(m -> m.isAnnotationPresent(Inject.class))
+ .collect(Collectors.toSet());
+ }
+
+ public static String descriptorBuilder(Class<?> returnType, Class<?>[] parameters) {
+ StringBuilder desc = new StringBuilder("(");
+ for(Class<?> p : parameters)
+ desc.append(Type.getDescriptor(p));
+ desc.append(")");
+ desc.append(Type.getDescriptor(returnType));
+ return desc.toString();
+ }
+
+ private boolean processInjector(ClassNode target, Object patch, Method injector) {
+ //get relevant method from target
+ Optional<MethodNode> targetMethod =
+ target.methods
+ .stream()
+ .filter(m -> {
+ Inject a = injector.getAnnotation(Inject.class);
+ return m.name.equals(a.methodName()) && m.desc.equals(descriptorBuilder(a.returnType(), a.parameters()));
+ })
+ .findAny(); //there can literally only be one so this is safe and more efficient than findFirst
+
+ try {
+ if(!targetMethod.isPresent())
+ throw new NoSuchMethodException();
+ injector.invoke(patch, targetMethod.get());
+
+ LOGGER.info(
+ "Completed transformation task {} for {}::{}",
+ injector.getAnnotation(Inject.class).description(),
+ target.name,
+ targetMethod.get().name
+ );
+
+ return true;
+
+ } catch (NoSuchMethodException | InvocationTargetException | IllegalAccessException e) {
+ if (e instanceof NoSuchMethodException) {
+ LOGGER.error(
+ "{} while attempting to find method {}::{} in task {}. This should never happen.",
+ e,
+ target.name,
+ injector.getAnnotation(Inject.class).methodName(),
+ injector.getAnnotation(Inject.class).description()
+ );
+ } else {
+ Throwable cause;
+ if(e instanceof InvocationTargetException)
+ cause = e.getCause();
+ else cause = e;
+ LOGGER.error(
+ "{} thrown from {}::{} for task with description {}",
+ cause,
+ target.name,
+ targetMethod.get().name,
+ injector.getAnnotation(Inject.class).description()
+ );
+ }
+ return false;
+ }
}
}
diff --git a/src/main/java/bscv/asm/api/AnnotationChecker.java b/src/main/java/bscv/asm/api/AnnotationChecker.java
new file mode 100644
index 0000000..a0abbda
--- /dev/null
+++ b/src/main/java/bscv/asm/api/AnnotationChecker.java
@@ -0,0 +1,37 @@
+package bscv.asm.api;
+
+import org.objectweb.asm.AnnotationVisitor;
+import org.objectweb.asm.ClassVisitor;
+import org.objectweb.asm.Opcodes;
+import org.objectweb.asm.Type;
+
+import java.lang.annotation.Annotation;
+
+/**
+ * ClassVisitor which checks whether a Class is annotated with
+ * a specific Annotation.
+ * @author Fraaz
+ */
+public class AnnotationChecker extends ClassVisitor {
+ private boolean annotationPresent;
+ private final String annotationDesc;
+
+ public AnnotationChecker(Class<? extends Annotation> a) {
+ super(Opcodes.ASM8); //hopefully lol
+ this.annotationPresent = false;
+ this.annotationDesc = Type.getDescriptor(a);
+ }
+
+ @Override
+ public AnnotationVisitor visitAnnotation(String desc, boolean visible) {
+ if (visible && desc.equals(this.annotationDesc))
+ this.annotationPresent = true;
+ return super.visitAnnotation(desc, visible);
+ //returning null would delete our annotation, but we don't want that
+ //so we jut delegate to superclass
+ }
+
+ public boolean isAnnotationPresent() {
+ return this.annotationPresent;
+ }
+}
diff --git a/src/main/java/bscv/asm/api/annotations/Inject.java b/src/main/java/bscv/asm/api/annotations/Inject.java
new file mode 100644
index 0000000..dcf5fcb
--- /dev/null
+++ b/src/main/java/bscv/asm/api/annotations/Inject.java
@@ -0,0 +1,18 @@
+package bscv.asm.api.annotations;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+@Target({ElementType.METHOD})
+@Retention(RetentionPolicy.RUNTIME)
+public @interface Inject {
+ String methodName();
+
+ Class<?> returnType();
+
+ Class<?>[] parameters();
+
+ String description() default "No description given";
+}
diff --git a/src/main/java/bscv/asm/api/annotations/Patch.java b/src/main/java/bscv/asm/api/annotations/Patch.java
new file mode 100644
index 0000000..4685845
--- /dev/null
+++ b/src/main/java/bscv/asm/api/annotations/Patch.java
@@ -0,0 +1,12 @@
+package bscv.asm.api.annotations;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+@Target(ElementType.TYPE)
+@Retention(RetentionPolicy.RUNTIME)
+public @interface Patch {
+ Class<?> value();
+}