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