From 26e52da7d82da3a729e74a8522265cd99ca544c3 Mon Sep 17 00:00:00 2001 From: zaaarf Date: Fri, 3 Feb 2023 17:45:28 +0100 Subject: feat: initial, low quality and untested implementation of method transformer --- src/main/java/bscv/asm/BoSCoVicinoLoader.java | 173 ++++++++++++++++++++- src/main/java/bscv/asm/api/AnnotationChecker.java | 37 +++++ src/main/java/bscv/asm/api/annotations/Inject.java | 18 +++ src/main/java/bscv/asm/api/annotations/Patch.java | 12 ++ 4 files changed, 232 insertions(+), 8 deletions(-) create mode 100644 src/main/java/bscv/asm/api/AnnotationChecker.java create mode 100644 src/main/java/bscv/asm/api/annotations/Inject.java create mode 100644 src/main/java/bscv/asm/api/annotations/Patch.java 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 YAY = EnumSet.of(Phase.BEFORE); + private static final EnumSet NAY = EnumSet.noneOf(Phase.class); + + private static final HashMultimap> 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 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 handlesClass(Type classType, final boolean isEmpty) { - LOGGER.info(String.format("CLAZZ >>> %s", classType.getClassName())); - return EnumSet.noneOf(Phase.class); + public EnumSet 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 classMethodNames = classNode.methods + .stream() + .map(m -> m.name) + .collect(Collectors.toSet()); + Set classMethodDescriptors = classNode.methods + .stream() + .map(m -> m.desc) + .collect(Collectors.toSet()); + Set> relevantPatches = patchClasses.get(classType.getDescriptor()); + HashMultimap patches = HashMultimap.create(); + relevantPatches + .forEach(p -> { + Set 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 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 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 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(); +} -- cgit v1.2.3-56-ga3b1