feat: initial, low quality and untested implementation of method transformer

This commit is contained in:
zaaarf 2023-02-03 17:45:28 +01:00
parent 7b201195a1
commit 26e52da7d8
No known key found for this signature in database
GPG key ID: 82240E075E31FA4C
4 changed files with 232 additions and 8 deletions

View file

@ -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<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) {
//if(!isEmpty && shouldHandle(classType))
// LOGGER.info("CLAZZ >>> {}", classType.getClassName());
//return NAY;
throw new IllegalStateException("Outdated ModLauncher"); //mixin does it
}
@Override
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;
}
}
}

View file

@ -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;
}
}

View file

@ -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";
}

View file

@ -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();
}