feat: initial, low quality and untested implementation of method transformer
This commit is contained in:
parent
7b201195a1
commit
26e52da7d8
4 changed files with 232 additions and 8 deletions
|
@ -1,32 +1,189 @@
|
||||||
package bscv.asm;
|
package bscv.asm;
|
||||||
|
|
||||||
import java.io.File;
|
|
||||||
import java.io.IOException;
|
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.LogManager;
|
||||||
import org.apache.logging.log4j.Logger;
|
import org.apache.logging.log4j.Logger;
|
||||||
|
import org.objectweb.asm.ClassReader;
|
||||||
import org.objectweb.asm.Type;
|
import org.objectweb.asm.Type;
|
||||||
|
|
||||||
import cpw.mods.modlauncher.serviceapi.ILaunchPluginService;
|
import cpw.mods.modlauncher.serviceapi.ILaunchPluginService;
|
||||||
|
import org.objectweb.asm.tree.ClassNode;
|
||||||
|
import org.objectweb.asm.tree.MethodNode;
|
||||||
|
|
||||||
public class BoSCoVicinoLoader implements ILaunchPluginService {
|
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() {
|
public BoSCoVicinoLoader() {
|
||||||
LOGGER.info("BoSCoVicinoLoader instantiation");
|
LOGGER.info("BoSCoVicinoLoader instantiated successfully!");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String name() {
|
public String name() {
|
||||||
return "boscovicino_asm";
|
return NAME;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public EnumSet<ILaunchPluginService.Phase> handlesClass(Type classType, final boolean isEmpty) {
|
public EnumSet<Phase> handlesClass(Type classType, final boolean isEmpty) {
|
||||||
LOGGER.info(String.format("CLAZZ >>> %s", classType.getClassName()));
|
//if(!isEmpty && shouldHandle(classType))
|
||||||
return EnumSet.noneOf(Phase.class);
|
// 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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
37
src/main/java/bscv/asm/api/AnnotationChecker.java
Normal file
37
src/main/java/bscv/asm/api/AnnotationChecker.java
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
18
src/main/java/bscv/asm/api/annotations/Inject.java
Normal file
18
src/main/java/bscv/asm/api/annotations/Inject.java
Normal 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";
|
||||||
|
}
|
12
src/main/java/bscv/asm/api/annotations/Patch.java
Normal file
12
src/main/java/bscv/asm/api/annotations/Patch.java
Normal 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();
|
||||||
|
}
|
Loading…
Reference in a new issue