From b4ec1d7491e0ef1e9de9d25effc6e7c696d5f2e4 Mon Sep 17 00:00:00 2001 From: Chocohead Date: Mon, 29 Aug 2022 22:26:36 +0100 Subject: [PATCH 1/8] Add merged form of `@Inject` and `@ModifyVariable` Allows capturing (and modifying) locals like `@ModifyVariable`, but with the callback and multiple local handling of `@Inject` Still needs the docs finishing and remapping logic adding --- .../asm/mixin/injection/InjectWithLocals.java | 235 ++++++++++ .../callback/CallbackClassGenerator.java | 201 ++++++++ .../injection/callback/CallbackInjector.java | 46 +- .../callback/CallbackLocalInjector.java | 436 ++++++++++++++++++ .../callback/ModificationsCaught.java | 11 + .../struct/CallbackLocalInjectionInfo.java | 50 ++ .../mixin/injection/struct/InjectionInfo.java | 2 + .../mixin/transformer/DefaultExtensions.java | 2 + 8 files changed, 973 insertions(+), 10 deletions(-) create mode 100644 src/main/java/org/spongepowered/asm/mixin/injection/InjectWithLocals.java create mode 100644 src/main/java/org/spongepowered/asm/mixin/injection/callback/CallbackClassGenerator.java create mode 100644 src/main/java/org/spongepowered/asm/mixin/injection/callback/CallbackLocalInjector.java create mode 100644 src/main/java/org/spongepowered/asm/mixin/injection/callback/ModificationsCaught.java create mode 100644 src/main/java/org/spongepowered/asm/mixin/injection/struct/CallbackLocalInjectionInfo.java diff --git a/src/main/java/org/spongepowered/asm/mixin/injection/InjectWithLocals.java b/src/main/java/org/spongepowered/asm/mixin/injection/InjectWithLocals.java new file mode 100644 index 000000000..bd9cf19b9 --- /dev/null +++ b/src/main/java/org/spongepowered/asm/mixin/injection/InjectWithLocals.java @@ -0,0 +1,235 @@ +package org.spongepowered.asm.mixin.injection; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.spongepowered.asm.mixin.MixinEnvironment.Option; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; +import org.spongepowered.asm.mixin.injection.callback.LocalCapture; +import org.spongepowered.asm.mixin.injection.selectors.ITargetSelector; +import org.spongepowered.asm.mixin.injection.throwables.InjectionError; +import org.spongepowered.asm.mixin.injection.throwables.InvalidInjectionException; +import org.spongepowered.asm.util.ConstraintParser.Constraint; + +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +public @interface InjectWithLocals { + @Target({/* None */}) + @Retention(RetentionPolicy.RUNTIME) + @interface Local { + /** + * Gets the local variable ordinal by type. For example, if there are 3 + * {@link String} arguments in the local variable table, ordinal 0 specifies + * the first, 1 specifies the second, etc. Use ordinal when the + * index within the LVT is known. Takes precedence over {@link #index}. + * + * @return variable ordinal + */ + public int ordinal() default -1; + + /** + * Gets the absolute index of the local variable within the local variable + * table to capture. The local variable at the specified index must be of + * the same type as the capture. Takes precedence over {@link #name}. + * + * @return argument index to modify or -1 for automatic + */ + public int index() default -1; + + /** + * Gets the name of the variable to capture. Only used if the variable + * cannot be located via {@link #ordinal} or {@link #index}. + * + * @return possible names to capture, only useful when the LVT in the target + * method is known to be complete. + */ + public String[] name() default {}; + } + @Target(ElementType.PARAMETER) + @Retention(RetentionPolicy.RUNTIME) + @interface Modify { + } + + /** + * The identifier for this injector, can be retrieved via the + * {@link CallbackInfo#getId} accessor. If not specified, the ID defaults to + * the target method name. + * + * @return the injector id to use + */ + public String id() default ""; + + /** + * String representation of one or more + * {@link ITargetSelector target selectors} which identify the target + * methods. + * + * @return target method(s) for this injector + */ + public String[] method(); + + /** + * A {@link Slice} annotation which describes the method bisection used in + * the {@link #at} query for this injector. + * + * @return slice + */ + public Slice slice() default @Slice; + + /** + * An {@link At} annotation which describes the {@link InjectionPoint} in + * the target method. + * + * @return {@link At} which identifies the location to inject inside the + * target method. + */ + public At at(); + + /** + * Setting an injected callback to cancellable allows the injected + * callback to inject optional RETURN opcodes into the target method, the + * return behaviour can then be controlled from within the callback by + * interacting with the supplied {@link CallbackInfo} object. + * + * @return true if this injector should inject appropriate RETURN opcodes + * which allow it to be cancelled + */ + public boolean cancellable() default false; + + /** + * Specifies the local variable capture behaviour for this injector. + * + *

When capturing local variables in scope, the variables are appended to + * the callback invocation after the {@link CallbackInfo} argument.

+ * + *

Capturing local variables from the target scope requires careful + * planning because unlike other aspects of an injection (such as the target + * method name and signature), the local variable table is not safe + * from modification by other transformers which may be in use in the + * production environment. Even other injectors which target the same target + * method have the ability to modify the local variable table and thus it is + * in no way safe to assume that local variables in scope at development + * time will be so in production.

+ * + *

To provide some level of flexibility, especially where changes can be + * anticipated (for example a well-known mod makes changes which result in a + * particular structure for the local variable table) it is possible to + * provide overloads for the handler method which will become + * surrogate targets for the orphaned injector by annotating them with an + * {@link Surrogate} annotation.

+ * + *

You can improve the robustness of your local capture injection by only + * specifying locals up to the last variable you wish to use. For example if + * the target LVT contains <int, int, int, float, String> and + * you only need the float value, you can choose to omit the unused + * String and changes to the LVT beyond that point will not affect + * your injection.

+ * + *

It is also important to nominate the failure behaviour to follow when + * local capture fails and so all {@link LocalCapture} behaviours which + * specify a capture action imply a particular behaviour for handling + * failure. See the javadoc on the {@link LocalCapture} members for more + * details.

+ * + *

Determining what local variables are available to you and in what + * order can be somewhat tricky, and so a simple mechanism for enumerating + * available locals is provided. By setting locals to + * {@link LocalCapture#PRINT}, the injector writes the local capture state + * to STDERR instead of injecting the callback. Using the output thus + * obtained it is then a straightforward matter of altering the callback + * method signature to match the signature proposed by the Callback + * Injector.

+ * + * @return the desired local capture behaviour for this injector + */ + public LocalCapture behaviour(); + + public Local[] locals() default {}; + + /** + * By default, the annotation processor will attempt to locate an + * obfuscation mapping for all {@link Inject} methods since it is + * anticipated that in general the target of a {@link Inject} annotation + * will be an obfuscated method in the target class. However since it is + * possible to also apply mixins to non-obfuscated targets (or non- + * obfuscated methods in obfuscated targets, such as methods added by Forge) + * it may be necessary to suppress the compiler error which would otherwise + * be generated. Setting this value to false will cause the + * annotation processor to skip this annotation when attempting to build the + * obfuscation table for the mixin. + * + * @return True to instruct the annotation processor to search for + * obfuscation mappings for this annotation + */ + public boolean remap() default true; + + /** + * In general, injectors are intended to "fail soft" in that a failure to + * locate the injection point in the target method is not considered an + * error condition. Another transformer may have changed the method + * structure or any number of reasons may cause an injection to fail. This + * also makes it possible to define several injections to achieve the same + * task given expected mutation of the target class and the + * injectors which fail are simply ignored. + * + *

However, this behaviour is not always desirable. For example, if your + * application depends on a particular injection succeeding you may wish to + * detect the injection failure as an error condition. This argument is thus + * provided to allow you to stipulate a minimum number of successful + * injections for this callback handler. If the number of injections + * specified is not achieved then an {@link InjectionError} is thrown at + * application time. Use this option with care.

+ * + * @return Minimum required number of injected callbacks, default specified + * by the containing config + */ + public int require() default -1; + + /** + * Like {@link #require()} but only enabled if the + * {@link Option#DEBUG_INJECTORS mixin.debug.countInjections} option is set + * to true and defaults to 1. Use this option during debugging to + * perform simple checking of your injectors. Causes the injector to throw + * a {@link InvalidInjectionException} if the expected number of injections + * is not realised. + * + * @return Minimum number of expected callbacks, default 1 + */ + public int expect() default 1; + + /** + * Injection points are in general expected to match every candidate + * instruction in the target method or slice, except in cases where options + * such as {@link At#ordinal} are specified which naturally limit the number + * of results. + * + *

This option allows for sanity-checking to be performed on the results + * of an injection point by specifying a maximum allowed number of matches, + * similar to that afforded by {@link Group#max}. For example if your + * injection is expected to match 4 invocations of a target method, but + * instead matches 5, this can become a detectable tamper condition by + * setting this value to 4. + * + *

Setting any value 1 or greater is allowed. Values less than 1 or less + * than {@link #require} are ignored. {@link #require} supercedes this + * argument such that if allow is less than require the + * value of require is always used.

+ * + *

Note that this option is not a limit on the query behaviour of + * this injection point. It is only a sanity check used to ensure that the + * number of matches is not too high + * + * @return Maximum allowed number of injections for this + */ + public int allow() default -1; + + /** + * Returns constraints which must be validated for this injector to + * succeed. See {@link Constraint} for details of constraint formats. + * + * @return Constraints for this annotation + */ + public String constraints() default ""; +} \ No newline at end of file diff --git a/src/main/java/org/spongepowered/asm/mixin/injection/callback/CallbackClassGenerator.java b/src/main/java/org/spongepowered/asm/mixin/injection/callback/CallbackClassGenerator.java new file mode 100644 index 000000000..d004552f7 --- /dev/null +++ b/src/main/java/org/spongepowered/asm/mixin/injection/callback/CallbackClassGenerator.java @@ -0,0 +1,201 @@ +package org.spongepowered.asm.mixin.injection.callback; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; + +import org.objectweb.asm.ClassVisitor; +import org.objectweb.asm.MethodVisitor; +import org.objectweb.asm.Opcodes; +import org.objectweb.asm.Type; +import org.objectweb.asm.tree.ClassNode; +import org.objectweb.asm.util.CheckClassAdapter; + +import org.spongepowered.asm.logging.ILogger; +import org.spongepowered.asm.mixin.MixinEnvironment; +import org.spongepowered.asm.mixin.MixinEnvironment.Option; +import org.spongepowered.asm.mixin.extensibility.IMixinInfo; +import org.spongepowered.asm.mixin.transformer.SyntheticClassInfo; +import org.spongepowered.asm.mixin.transformer.ext.IClassGenerator; +import org.spongepowered.asm.service.ISyntheticClassInfo; +import org.spongepowered.asm.service.MixinService; +import org.spongepowered.asm.util.Constants; +import org.spongepowered.asm.util.IConsumer; + +public class CallbackClassGenerator implements IClassGenerator { + private static class Key { + private final Type returnType; + private boolean useReturn; + private Type[] locals; + + Key(Type returnType, boolean useReturn, Type[] locals) { + this.returnType = returnType; + this.useReturn = useReturn && !Type.VOID_TYPE.equals(returnType); + this.locals = locals; + } + + @Override + public boolean equals(Object obj) { + if (obj == this) return true; + if (!(obj instanceof Key)) return false; + Key that = (Key) obj; + + return returnType.equals(that.returnType) && useReturn == that.useReturn && Arrays.equals(locals, that.locals); + } + + @Override + public int hashCode() { + int out = 1; + + out = 31 * out + returnType.hashCode(); + out = 31 * out + Boolean.hashCode(useReturn); + out = 31 * out + Arrays.hashCode(locals); + + return out; + } + } + private static class CallbackClassInfo extends SyntheticClassInfo { + final Type returnType; + final boolean useReturn; + final Type[] locals; + int loaded = 0; + + CallbackClassInfo(IMixinInfo mixin, String name, Key key) { + super(mixin, name); + + returnType = key.returnType; + useReturn = key.useReturn; + locals = key.locals; + } + + String getSuperType() { + return CallbackInfo.getCallInfoClassName(returnType); + } + + String getSuperConstructor() { + return useReturn ? CallbackInfo.getConstructorDescriptor(returnType) : CallbackInfo.getConstructorDescriptor(); + } + + @Override + public boolean isLoaded() { + return this.loaded > 0; + } + } + private static final String CLASS_NAME_BASE = Constants.SYNTHETIC_PACKAGE + ".callback.CallbackInfoWithLocals$"; + private static final String LOCAL_FIELD = "local$"; + static final String SET_LOCALS = "setLocals"; + static final String GET_LOCAL = "getLocal$"; + private static final ILogger logger = MixinService.getService().getLogger("mixin"); + private final IConsumer registry; + private final Map pool = new HashMap(); + private final Map nameToClass = new HashMap(); + private int nextIndex = 1; + + public CallbackClassGenerator(IConsumer registry) { + this.registry = registry; + } + + @Override + public String getName() { + return "local-callback"; + } + + public ISyntheticClassInfo getArgsClass(IMixinInfo mixin, Type returnType, boolean useReturn, Type... locals) { + Key key = new Key(returnType, useReturn, locals); + + CallbackClassInfo info = pool.get(key); + if (info == null) { + String name = CLASS_NAME_BASE + nextIndex++; + logger.debug("CallbackClassGenerator generating {} for {} CallbackInfo{} with locals {}", name, useReturn ? "" : "mid-method", + Type.VOID_TYPE == returnType ? "" : "Returnable (returning " + returnType.getClassName() + ')', Arrays.toString(locals)); + info = new CallbackClassInfo(mixin, name, key); + + pool.put(key, info); + nameToClass.put(name, info); + registry.accept(info); + } + + return info; + } + + @Override + public boolean generate(String name, ClassNode classNode) { + CallbackClassInfo info = this.nameToClass.get(name); + if (info == null) { + return false; + } + + if (info.loaded > 0) { + logger.debug("CallbackClassGenerator is re-generating {}, already did this {} times!", name, info.loaded); + } + + ClassVisitor visitor = classNode; + if (MixinEnvironment.getCurrentEnvironment().getOption(Option.DEBUG_VERIFY)) { + visitor = new CheckClassAdapter(classNode); + } + + visitor.visit(Opcodes.V1_6, Opcodes.ACC_PUBLIC | Opcodes.ACC_SUPER | Opcodes.ACC_SYNTHETIC, info.getName(), null, info.getSuperType(), null); + visitor.visitSource(name.substring(name.lastIndexOf('.') + 1) + ".java", null); + + generateFields(info, visitor); + generateConstructor(info, visitor); + generateSetter(info, visitor); + generateGetters(info, visitor); + + visitor.visitEnd(); + info.loaded++; + + return true; + } + + private void generateFields(CallbackClassInfo info, ClassVisitor writer) { + for (int i = 0; i < info.locals.length; i++) { + writer.visitField(Opcodes.ACC_PRIVATE, LOCAL_FIELD + i, info.locals[i].getDescriptor(), null, null); + } + } + + private void generateConstructor(CallbackClassInfo info, ClassVisitor writer) { + int maxStack = info.useReturn ? 3 + info.returnType.getSize() : 3; + + MethodVisitor ctor = writer.visitMethod(Opcodes.ACC_PUBLIC, Constants.CTOR, info.getSuperConstructor(), null, null); + ctor.visitCode(); + ctor.visitVarInsn(Opcodes.ALOAD, 0); + ctor.visitVarInsn(Opcodes.ALOAD, 1); + ctor.visitVarInsn(Opcodes.ILOAD, 2); + if (info.useReturn) ctor.visitVarInsn(info.returnType.getOpcode(Opcodes.ILOAD), 3); + ctor.visitMethodInsn(Opcodes.INVOKESPECIAL, info.getSuperType(), Constants.CTOR, info.getSuperConstructor(), false); + ctor.visitInsn(Opcodes.RETURN); + ctor.visitMaxs(maxStack, maxStack); + ctor.visitEnd(); + } + + private void generateSetter(CallbackClassInfo info, ClassVisitor writer) { + MethodVisitor method = writer.visitMethod(Opcodes.ACC_PUBLIC, SET_LOCALS, Type.getMethodDescriptor(Type.VOID_TYPE, info.locals), null, null); + method.visitCode(); + int index = 1; + for (int i = 0; i < info.locals.length; i++) { + Type local = info.locals[i]; + method.visitVarInsn(Opcodes.ALOAD, 0); + method.visitVarInsn(local.getOpcode(Opcodes.ILOAD), index); + method.visitFieldInsn(Opcodes.PUTFIELD, info.getName(), LOCAL_FIELD + i, local.getDescriptor()); + index += local.getSize(); + } + method.visitInsn(Opcodes.RETURN); + method.visitMaxs(info.locals.length + 1 < index ? 3 : 2, index); + method.visitEnd(); + } + + private void generateGetters(CallbackClassInfo info, ClassVisitor writer) { + for (int i = 0; i < info.locals.length; i++) { + Type local = info.locals[i]; + + MethodVisitor method = writer.visitMethod(Opcodes.ACC_PUBLIC, GET_LOCAL + i, Type.getMethodDescriptor(local), null, null); + method.visitCode(); + method.visitVarInsn(Opcodes.ALOAD, 0); + method.visitFieldInsn(Opcodes.GETFIELD, info.getName(), LOCAL_FIELD + i, local.getDescriptor()); + method.visitInsn(local.getOpcode(Opcodes.IRETURN)); + method.visitMaxs(local.getSize(), 1); + method.visitEnd(); + } + } +} \ No newline at end of file diff --git a/src/main/java/org/spongepowered/asm/mixin/injection/callback/CallbackInjector.java b/src/main/java/org/spongepowered/asm/mixin/injection/callback/CallbackInjector.java index 933348fd8..0fad7b9fb 100644 --- a/src/main/java/org/spongepowered/asm/mixin/injection/callback/CallbackInjector.java +++ b/src/main/java/org/spongepowered/asm/mixin/injection/callback/CallbackInjector.java @@ -64,7 +64,7 @@ public class CallbackInjector extends Injector { /** * Struct to replace all the horrible state variables from before */ - private class Callback extends InsnList { + protected class Callback extends InsnList { /** * Handler method @@ -219,6 +219,10 @@ private class Callback extends InsnList { //If the handler doesn't captureArgs, the CallbackInfo(Returnable) will be the first LVT slot, otherwise it will be at the target's frameSize int callbackInfoSlot = handlerArgs.length == 1 ? Bytecode.isStatic(handler) ? 0 : 1 : frameSize; + usesCallbackInfo = usesCallbackInfo(handler, callbackInfoSlot); + } + + protected boolean usesCallbackInfo(MethodNode handler, int callbackInfoSlot) { boolean seenCallbackInfoUse = false; for (AbstractInsnNode insn : handler.instructions) { //Look for anywhere the CallbackInfo(Returnable) is loaded in the handler, it's unused if it is never loaded in @@ -245,7 +249,7 @@ private class Callback extends InsnList { Injector.logger.debug("{} w{} be passed a CallbackInfo{} as a result", info, seenCallbackInfoUse ? "ill" : "on't", Type.VOID_TYPE == target.returnType ? "" : "Returnable"); } - usesCallbackInfo = seenCallbackInfoUse; + return seenCallbackInfoUse; } /** @@ -267,6 +271,10 @@ String getDescriptorWithAllLocals() { return this.target.getCallbackDescriptor(true, this.localTypes, this.target.arguments, this.frameSize, Short.MAX_VALUE); } + protected String getCallbackInfoClass() { + return target.getCallbackInfoClass(); + } + String getCallbackInfoConstructorDescriptor() { return this.isAtReturn ? CallbackInfo.getConstructorDescriptor(this.target.returnType) : CallbackInfo.getConstructorDescriptor(); } @@ -367,7 +375,7 @@ int marshalVar() { /** * Decorator key for local variables decoration */ - private static final String LOCALS_KEY = "locals"; + protected static final String LOCALS_KEY = "locals"; /** * True if cancellable @@ -377,7 +385,7 @@ int marshalVar() { /** * Local variable capture behaviour */ - private final LocalCapture localCapture; + protected final LocalCapture localCapture; /** * ID to return from callbackinfo @@ -408,7 +416,11 @@ int marshalVar() { * @param localCapture Local variable capture behaviour */ public CallbackInjector(InjectionInfo info, boolean cancellable, LocalCapture localCapture, String identifier) { - super(info, "@Inject"); + this(info, "@Inject", cancellable, localCapture, identifier); + } + + protected CallbackInjector(InjectionInfo info, String annotation, boolean cancellable, LocalCapture localCapture, String identifier) { + super(info, annotation); this.cancellable = cancellable; this.localCapture = localCapture; this.identifier = identifier; @@ -610,7 +622,7 @@ private String generateBadLVTMessage(final Callback callback) { * @param message message for the error * @return generated method */ - private MethodNode generateErrorMethod(Callback callback, String errorClass, String message) { + protected MethodNode generateErrorMethod(Callback callback, String errorClass, String message) { MethodNode method = this.info.addMethod(this.methodNode.access, this.methodNode.name + "$missing", callback.getDescriptor()); method.maxLocals = Bytecode.getFirstNonArgLocalIndex(Type.getArgumentTypes(callback.getDescriptor()), !this.isStatic); method.maxStack = 3; @@ -662,6 +674,20 @@ private void printLocals(final Callback callback) { printer.add(" // Method body").add("}").add().print(System.err); } + /** + * Adds the necessary instructions to use the given callback, if it is needed + * + * @param callback callback handle + */ + protected void prepareCallbackIfNeeded(Callback callback, boolean forceStore) { + if (callback.usesCallbackInfo) { + dupReturnValue(callback); + if (forceStore || cancellable || totalInjections > 1) { + createCallbackInfo(callback, true); + } + } + } + /** * @param callback callback handle * @param store store the callback info in a local variable @@ -688,10 +714,10 @@ private void createCallbackInfo(final Callback callback, boolean store) { /** * @param callback callback handle */ - private void loadOrCreateCallbackInfo(final Callback callback) { + protected void loadOrCreateCallbackInfo(final Callback callback, boolean forceStore) { if (!callback.usesCallbackInfo) { callback.add(new InsnNode(Opcodes.ACONST_NULL)); - } else if (this.cancellable || this.totalInjections > 1) { + } else if (forceStore || this.cancellable || this.totalInjections > 1) { callback.add(new VarInsnNode(Opcodes.ALOAD, this.callbackInfoVar), false, true); } else { this.createCallbackInfo(callback, false); @@ -727,7 +753,7 @@ protected void instanceCallbackInfo(final Callback callback, String id, String d this.lastId = id; this.lastDesc = desc; this.callbackInfoVar = callback.marshalVar(); - this.callbackInfoClass = callback.target.getCallbackInfoClass(); + this.callbackInfoClass = callback.getCallbackInfoClass(); // If we were going to store the CI anyway, and if we need it again, and if the current injection isn't at // return or cancellable, inject the CI creation at the method head so that it's available everywhere @@ -768,7 +794,7 @@ private void invokeCallback(final Callback callback, final MethodNode callbackMe } // Push the callback info onto the stack - this.loadOrCreateCallbackInfo(callback); + this.loadOrCreateCallbackInfo(callback, false); // (Maybe) push the locals onto the stack if (callback.canCaptureLocals) { diff --git a/src/main/java/org/spongepowered/asm/mixin/injection/callback/CallbackLocalInjector.java b/src/main/java/org/spongepowered/asm/mixin/injection/callback/CallbackLocalInjector.java new file mode 100644 index 000000000..db6d77643 --- /dev/null +++ b/src/main/java/org/spongepowered/asm/mixin/injection/callback/CallbackLocalInjector.java @@ -0,0 +1,436 @@ +package org.spongepowered.asm.mixin.injection.callback; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import com.google.common.base.Joiner; + +import org.objectweb.asm.Opcodes; +import org.objectweb.asm.Type; +import org.objectweb.asm.tree.AbstractInsnNode; +import org.objectweb.asm.tree.InsnList; +import org.objectweb.asm.tree.LocalVariableNode; +import org.objectweb.asm.tree.MethodInsnNode; +import org.objectweb.asm.tree.MethodNode; +import org.objectweb.asm.tree.TypeInsnNode; +import org.objectweb.asm.tree.VarInsnNode; + +import org.spongepowered.asm.mixin.FabricUtil; +import org.spongepowered.asm.mixin.injection.InjectionPoint; +import org.spongepowered.asm.mixin.injection.InjectWithLocals.Modify; +import org.spongepowered.asm.mixin.injection.callback.CallbackLocalInjector.CallbackWithLocals.CapturedLocal; +import org.spongepowered.asm.mixin.injection.struct.InjectionInfo; +import org.spongepowered.asm.mixin.injection.struct.Target; +import org.spongepowered.asm.mixin.injection.struct.InjectionNodes.InjectionNode; +import org.spongepowered.asm.mixin.injection.throwables.InjectionError; +import org.spongepowered.asm.mixin.injection.throwables.InvalidInjectionException; +import org.spongepowered.asm.util.Annotations; +import org.spongepowered.asm.util.Bytecode; +import org.spongepowered.asm.util.PrettyPrinter; +import org.spongepowered.asm.util.SignaturePrinter; +import org.spongepowered.asm.util.PrettyPrinter.IPrettyPrintable; + +public class CallbackLocalInjector extends CallbackInjector { + public static class Local { + public final int ordinal, index; + public final Set names; + + public Local(int ordinal, int index, Set names) { + this.ordinal = ordinal; + this.index = index; + this.names = names; + } + + public boolean isImplicit() { + return index < 0 && ordinal < 0 && names.isEmpty(); + } + + @Override + public String toString() { + return String.format("Local[ordinal=%d, index=%d, names=%s]", ordinal, index, names); + } + } + protected class CallbackWithLocals extends Callback implements IPrettyPrintable { + public class CapturedLocal { + public final int index; + public final Type type; + public final boolean wasImplicit, matchedType, modifying; + + public CapturedLocal(int argOffset, int index, Type type) { + this(argOffset, index, type, false); + } + + public CapturedLocal(int argOffset, int index, Type type, boolean wasImplicit) { + this.index = index; + this.type = type; + this.wasImplicit = wasImplicit; + matchedType = index < -1 || index >= 0 && type.equals(localTypes[index]); + modifying = willModifyLocals() && Annotations.getVisibleParameter(methodNode, Modify.class, argOffset) != null; + } + + public boolean isSuccessful() { + return index >= 0 && matchedType; + } + + @Override + public String toString() { + return String.format("Local[index=%d, type=%s, matching=%b]", index, type, matchedType); + } + } + final int[] ordinals; + final CapturedLocal[] capturedLocals; + private boolean modifiesLocals; + + CallbackWithLocals(MethodNode handler, Target target, InjectionNode node, LocalVariableNode[] locals, Local[] localRequests) { + super(handler, target, node, locals, true); + + this.ordinals = new int[locals.length]; + Arrays.fill(ordinals, -1); + Map ordinalMap = new HashMap(); + Map typeMap = new HashMap(); + Map nameMap = new HashMap(); + for (int i = frameSize; i < locals.length; i++) { + if (locals[i] != null) { + Type type = localTypes[i]; + int[] ordinals = ordinalMap.get(type); + int ordinal; //It's like an int list, but more manual + if (ordinals == null) { + ordinal = 0; + ordinals = new int[1]; + } else { + ordinals = Arrays.copyOf(ordinals, (ordinal = ordinals.length) + 1); + } + ordinals[this.ordinals[i] = ordinal] = i; + ordinalMap.put(type, ordinals); + Integer typeIndex = typeMap.get(type); + typeMap.put(type, typeIndex != null ? (typeIndex < 0 ? typeIndex - 1 : -2) : i); + nameMap.put(locals[i].name, i); + } + } + + if (extraArgs < localRequests.length) {//Avoid any accidents immediately below + if (localCapture.isPrintLocals()) { + capturedLocals = new CapturedLocal[0]; + return; //Allow the invalid state if we're only printing anyway + } + throw new InvalidInjectionException(info, String.format("Too many locals specified in %s! Expected (up to) %d but had %d", info, extraArgs, localRequests.length)); + } + capturedLocals = new CapturedLocal[extraArgs]; + Type[] handlerArgs = Type.getArgumentTypes(handler.desc); + /** The point at which local arguments start in the handler */ + int argOffset = target.arguments.length + 1; + on: for (int i = 0; i < localRequests.length; i++, argOffset++) { + Local request = localRequests[i]; + Type type = handlerArgs[argOffset]; + + if (request.ordinal >= 0) { + int[] ordinals = ordinalMap.get(type); + if (ordinals != null && ordinals.length > request.ordinal) { + capturedLocals[i] = new CapturedLocal(argOffset, ordinals[request.ordinal], type); + continue; + } + } + if (request.index >= 0 && request.index < locals.length - frameSize) { + capturedLocals[i] = new CapturedLocal(argOffset, frameSize + request.index, type); + continue; + } + if (!request.names.isEmpty()) { + for (String name : request.names) { + Integer index = nameMap.get(name); + if (index != null) { + capturedLocals[i] = new CapturedLocal(argOffset, index, type); + continue on; + } + } + } + capturedLocals[i] = request.isImplicit() ? forType(typeMap, type, argOffset) : new CapturedLocal(argOffset, -1, type); + } + for (int i = localRequests.length; i < extraArgs; i++, argOffset++) { + capturedLocals[i] = forType(typeMap, handlerArgs[argOffset], argOffset); + } + logger.info(Arrays.toString(capturedLocals)); + } + + private CapturedLocal forType(Map typeMap, Type type, int argOffset) { + Integer typeIndex = typeMap.get(type); + if (typeIndex == null) {//Type not present at all + return new CapturedLocal(argOffset, -1, type, true); + } else { + return new CapturedLocal(argOffset, typeIndex, type, true); + } + } + + @Override + protected boolean usesCallbackInfo(MethodNode handler, int callbackInfoSlot) { + for (int i = target.arguments.length + 1, count = 0; count < extraArgs; i++, count++) { + if (Annotations.getVisibleParameter(handler, Modify.class, i) != null) { + logger.debug("{} does use it's CallbackInfo{} (for local {})", info, Type.VOID_TYPE == target.returnType ? "" : "Returnable", i); + return modifiesLocals = true; + } + } + + return super.usesCallbackInfo(handler, callbackInfoSlot); + } + + @Override + protected String getCallbackInfoClass() { + if (!modifiesLocals) return super.getCallbackInfoClass(); + List modifyingLocals = new ArrayList(capturedLocals.length); + + for (CapturedLocal local : capturedLocals) { + if (local.modifying) { + modifyingLocals.add(local.type); + } + } + + return classGenerator.getArgsClass(info.getMixin().getMixin(), target.returnType, isAtReturn, modifyingLocals.toArray(new Type[0])).getName(); + } + + boolean willModifyLocals() { + return modifiesLocals; + } + + @Override + boolean checkDescriptor(String desc) { + Type[] args = Type.getArgumentTypes(desc); + int i = 0; + + for (Type param : target.arguments) { + if (i >= args.length || !param.equals(args[i++])) {//Missing the target method's parameters + throw new InvalidInjectionException(info, String.format("Invalid descriptor on %s! Target parameter%s missing/incorrect, expected %s", info, + target.arguments.length > 1 ? "s are" : " is", target.method.desc.substring(1, target.method.desc.indexOf(')')))); + } + } + + Type callback = Type.getObjectType(target.getCallbackInfoClass()); + if (i >= args.length || !callback.equals(args[i++])) {//Callback is wrong, perhaps a returnable one has/n't been used when it should/n't + callback = Type.getObjectType(CallbackInfo.getCallInfoClassName(Type.VOID_TYPE.equals(target.returnType) ? Type.INT_TYPE : Type.VOID_TYPE)); + + if (--i < args.length && callback.equals(args[i])) {//Wrong callback it is + String correct = callback.getInternalName(); + throw new InvalidInjectionException(info, String.format("Invalid descriptor on %s! %s is required!", info, correct.substring(correct.lastIndexOf('/') + 1))); + } else {//Still not right... + String correct = target.getCallbackInfoClass(); + throw new InvalidInjectionException(info, String.format("Invalid descriptor on %s! Expected %s after target parameter%s but found %s", info, + target.arguments.length > 1 ? "s" : "", correct.substring(correct.lastIndexOf('/') + 1), args.length > i ? args[i] : "")); + } + } + + for (CapturedLocal local : capturedLocals) { + if (!local.isSuccessful() || i >= args.length || !local.type.equals(args[i++])) { + return false; + } + } + + return true; + } + + @Override + public void print(PrettyPrinter printer) { + printer.add(" %5s %7s %30s %s", "INDEX", "ORDINAL", "TYPE", "NAME"); + for (int i = isStatic() ? 0 : 1; i < frameSize; i++) { + if (locals[i] != null) { + printer.add(" PARAM [ x ] %30s %-50s", SignaturePrinter.getTypeName(localTypes[i], false), CallbackInjector.meltSnowman(i, locals[i].name)); + } else { + boolean isTop = i > 0 && localTypes[i - 1] != null && localTypes[i - 1].getSize() > 1; + printer.add(" PARAM %30s", isTop ? "" : "-"); + } + } + boolean[] captures = new boolean[locals.length]; + for (CapturedLocal local : capturedLocals) { + if (local.index >= 0) captures[local.index] = true; + } + for (int i = frameSize, index = 0; i < locals.length; i++, index++) { + char marker = i == frameSize ? '>' : ' '; + if (locals[i] != null) { + printer.add("%c [%3d] [%3d] %30s %-50s <%s>", marker, index, ordinals[i], SignaturePrinter.getTypeName(localTypes[i], false), + CallbackInjector.meltSnowman(i, locals[i].name), captures[i] ? "captured" : "skipped"); + } else { + boolean isTop = i > 0 && localTypes[i - 1] != null && localTypes[i - 1].getSize() > 1; + printer.add("%c [%3d] %30s", marker, index, isTop ? "" : "-"); + } + } + } + } + private final Local[] localRequests; + final CallbackClassGenerator classGenerator; + + public CallbackLocalInjector(InjectionInfo info, boolean cancellable, LocalCapture behaviour, Local[] locals, String identifier) { + super(info, "@InjectWithLocals", cancellable, behaviour, identifier); + + localRequests = locals; + classGenerator = info.getMixin().getExtensions().getGenerator(CallbackClassGenerator.class); + } + + @Override + protected void sanityCheck(Target target, List injectionPoints) { + super.sanityCheck(target, injectionPoints); + + if (localCapture == null || localCapture == LocalCapture.NO_CAPTURE) { + throw new InvalidInjectionException(info, String.format("Invalid value of local capture behaviour (%s) in %s", localCapture, this)); + } + } + + @Override + protected void inject(Target target, InjectionNode node) { + LocalVariableNode[] locals = node.getDecoration(CallbackInjector.LOCALS_KEY + ':' + FabricUtil.getCompatibility(info)); + CallbackWithLocals callback = new CallbackWithLocals(methodNode, target, node, locals, localRequests); + + if (localCapture.isPrintLocals()) { + printLocals(callback); + info.addCallbackInvocation(methodNode); + return; + } + + MethodNode callbackMethod; + if (!callback.checkDescriptor(methodNode.desc)) { + if (info.getTargetCount() > 1) { + return; // Look for a match in other targets before failing + } + + String message = generateBadLocalsMessage(callback); + switch (localCapture) { + case CAPTURE_FAILEXCEPTION: + logger.error("Injection error: {}", message); + callbackMethod = generateErrorMethod(callback, "org/spongepowered/asm/mixin/injection/throwables/InjectionError", message); + break; + case CAPTURE_FAILSOFT: + logger.warn("Injection warning: {}", message); + return; + default: + logger.error("Critical injection failure: {}", message); + throw new InjectionError(message); + } + } else { + callbackMethod = methodNode; + } + + prepareCallbackIfNeeded(callback, callback.willModifyLocals()); + invokeCallback(callback, callbackMethod); + if (callback.usesCallbackInfo) injectCancellationCode(callback); + + callback.inject(); + info.notifyInjected(callback.target); + } + + private void printLocals(CallbackWithLocals callback) { + PrettyPrinter printer = new PrettyPrinter(); + + printer.kv("Target Class", classNode.name.replace('/', '.')); + printer.kv("Target Method", new SignaturePrinter(callback.target.method, callback.argNames)); + printer.kv("Target Max LOCALS", callback.target.getMaxLocals()); + printer.kv("Initial Frame Size", callback.frameSize); + printer.kv("Callback Name", info.getMethodName()); + printer.kv("Instruction", "%s %s", callback.node.getClass().getSimpleName(), Bytecode.getOpcodeName(callback.node.getCurrentTarget().getOpcode())); + printer.hr(); + printer.add(callback).print(System.err); + } + + private String generateBadLocalsMessage(CallbackWithLocals callback) { + Type[] args = Type.getArgumentTypes(methodNode.desc); + List errors = new ArrayList(); + errors.add("Failed to capture all locals:"); + + for (int i = 0, index = callback.target.arguments.length + 1; i < callback.capturedLocals.length; i++, index++) { + CapturedLocal local = callback.capturedLocals[i]; + + String error; + if (!local.isSuccessful()) { + if (local.index == -1) { + if (local.wasImplicit) { + error = "No local with type not found"; + } else { + error = "No local found matching criteria"; + } + } else if (!local.matchedType) { + error = "Wrong type for slot, expected " + callback.localTypes[local.index].getClassName(); + } else { + error = "Expected one local with type but found " + -local.index; + } + } else if (index >= args.length) {//Shouldn't be allowed to happen but we'll cover the case anyway + error = "Missing parameter for local"; + } else if (!local.type.equals(args[index])) { + error = "Wrong parameter type for local, expected " + local.type.getClassName(); + } else { + continue; + } + + errors.add(String.format("[%2d] %s - %s", i, SignaturePrinter.getTypeName(local.type, false), error)); + } + + return Joiner.on("\n\t").join(errors); + } + + private void invokeCallback(CallbackWithLocals callback, MethodNode callbackMethod) { + // Push "this" onto the stack if the callback is not static + if (!isStatic) { + callback.add(new VarInsnNode(Opcodes.ALOAD, 0), false, true); + } + + // Push the target method's parameters onto the stack + if (callback.captureArgs()) { + Bytecode.loadArgs(callback.target.arguments, callback, isStatic ? 0 : 1, -1); + } + + // Push the callback info onto the stack + loadOrCreateCallbackInfo(callback, callback.willModifyLocals()); + + // Push the locals onto the stack + for (CapturedLocal local : callback.capturedLocals) { + callback.add(new VarInsnNode(local.type.getOpcode(Opcodes.ILOAD), local.index)); + } + + // Call the callback! + invokeHandler(callback, callbackMethod); + + // Capture changes to locals in the handler + if (callback.willModifyLocals() && Annotations.getInvisible(callbackMethod, ModificationsCaught.class) == null) { + String callbackType = callback.getCallbackInfoClass(); + + for (AbstractInsnNode insn : callbackMethod.instructions) { + if (insn.getType() == AbstractInsnNode.INSN && insn.getOpcode() >= Opcodes.IRETURN && insn.getOpcode() <= Opcodes.RETURN) { + InsnList list = new InsnList(); + list.add(new VarInsnNode(Opcodes.ALOAD, callback.frameSize)); + list.add(new TypeInsnNode(Opcodes.CHECKCAST, callbackType)); + StringBuilder desc = new StringBuilder("("); + int slot = callback.frameSize + 1; + for (CapturedLocal local : callback.capturedLocals) { + if (local.modifying) { + desc.append(local.type.getDescriptor()); + list.add(new VarInsnNode(local.type.getOpcode(Opcodes.ILOAD), slot)); + } + + slot += local.type.getSize(); + } + list.add(new MethodInsnNode(Opcodes.INVOKEVIRTUAL, callbackType, CallbackClassGenerator.SET_LOCALS, desc.append(")V").toString())); + callbackMethod.instructions.insertBefore(insn, list); + } + } + + Annotations.setInvisible(callbackMethod, ModificationsCaught.class); + } + } + + @Override + protected void injectCancellationCode(Callback callback) { + super.injectCancellationCode(callback); + + CallbackWithLocals callbackLocal; + if (callback instanceof CallbackWithLocals && (callbackLocal = (CallbackWithLocals) callback).willModifyLocals()) { + String callbackType = callback.getCallbackInfoClass(); + + for (int i = 0, used = 0; i < callbackLocal.capturedLocals.length; i++) { + CapturedLocal local = callbackLocal.capturedLocals[i]; + if (!local.modifying) continue; + + callback.add(new VarInsnNode(Opcodes.ALOAD, callback.marshalVar())); + callback.add(new MethodInsnNode(Opcodes.INVOKEVIRTUAL, callbackType, CallbackClassGenerator.GET_LOCAL + used++, Type.getMethodDescriptor(local.type))); + callback.add(new VarInsnNode(local.type.getOpcode(Opcodes.ISTORE), local.index)); + } + } + } +} \ No newline at end of file diff --git a/src/main/java/org/spongepowered/asm/mixin/injection/callback/ModificationsCaught.java b/src/main/java/org/spongepowered/asm/mixin/injection/callback/ModificationsCaught.java new file mode 100644 index 000000000..14b98ce7d --- /dev/null +++ b/src/main/java/org/spongepowered/asm/mixin/injection/callback/ModificationsCaught.java @@ -0,0 +1,11 @@ +package org.spongepowered.asm.mixin.injection.callback; + +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.CLASS) +@interface ModificationsCaught { +} \ No newline at end of file diff --git a/src/main/java/org/spongepowered/asm/mixin/injection/struct/CallbackLocalInjectionInfo.java b/src/main/java/org/spongepowered/asm/mixin/injection/struct/CallbackLocalInjectionInfo.java new file mode 100644 index 000000000..5e3c6a7f9 --- /dev/null +++ b/src/main/java/org/spongepowered/asm/mixin/injection/struct/CallbackLocalInjectionInfo.java @@ -0,0 +1,50 @@ +package org.spongepowered.asm.mixin.injection.struct; + +import java.util.Collections; +import java.util.HashSet; +import java.util.List; + +import org.objectweb.asm.tree.AnnotationNode; +import org.objectweb.asm.tree.MethodNode; + +import org.spongepowered.asm.mixin.injection.InjectWithLocals; +import org.spongepowered.asm.mixin.injection.callback.CallbackLocalInjector; +import org.spongepowered.asm.mixin.injection.callback.CallbackLocalInjector.Local; +import org.spongepowered.asm.mixin.injection.callback.LocalCapture; +import org.spongepowered.asm.mixin.injection.code.Injector; +import org.spongepowered.asm.mixin.injection.struct.InjectionInfo.AnnotationType; +import org.spongepowered.asm.mixin.transformer.MixinTargetContext; +import org.spongepowered.asm.util.Annotations; + +/** + * Information about a callback to inject, usually specified by {@link InjectWithLocals} + */ +@AnnotationType(InjectWithLocals.class) +public class CallbackLocalInjectionInfo extends CallbackInjectionInfo { + protected CallbackLocalInjectionInfo(MixinTargetContext mixin, MethodNode method, AnnotationNode annotation) { + super(mixin, method, annotation); + } + + @Override + protected Injector parseInjector(AnnotationNode injectAnnotation) { + boolean cancellable = Annotations.getValue(injectAnnotation, "cancellable", Boolean.FALSE); + LocalCapture behaviour = Annotations.getValue(injectAnnotation, "behaviour", LocalCapture.class, null); + List localNodes = Annotations.getValue(injectAnnotation, "locals", true); + Local[] locals = new Local[localNodes.size()]; + int i = 0; + for (AnnotationNode local : localNodes) { + int ordinal = Annotations.getValue(local, "ordinal", -1); + int index = Annotations.getValue(local, "index", -1); + List names = Annotations.>getValue(local, "name", Collections.emptyList()); + locals[i++] = new Local(ordinal, index, !names.isEmpty() ? new HashSet(names) : Collections.emptySet()); + } + String identifier = Annotations.getValue(injectAnnotation, "id", ""); + + return new CallbackLocalInjector(this, cancellable, behaviour, locals, identifier); + } + + @Override + protected String getDescription() { + return "Callback method with locals"; + } +} \ No newline at end of file diff --git a/src/main/java/org/spongepowered/asm/mixin/injection/struct/InjectionInfo.java b/src/main/java/org/spongepowered/asm/mixin/injection/struct/InjectionInfo.java index bb686fb55..782c25108 100644 --- a/src/main/java/org/spongepowered/asm/mixin/injection/struct/InjectionInfo.java +++ b/src/main/java/org/spongepowered/asm/mixin/injection/struct/InjectionInfo.java @@ -212,6 +212,8 @@ ITargetSelector getRoot() { InjectionInfo.register(RedirectInjectionInfo.class); // @Redirect InjectionInfo.register(ModifyVariableInjectionInfo.class); // @ModifyVariable InjectionInfo.register(ModifyConstantInjectionInfo.class); // @ModifyConstant + // Fabric added injectors + InjectionInfo.register(CallbackLocalInjectionInfo.class); // @InjectWithLocals } /** diff --git a/src/main/java/org/spongepowered/asm/mixin/transformer/DefaultExtensions.java b/src/main/java/org/spongepowered/asm/mixin/transformer/DefaultExtensions.java index cc4031d11..2245858c0 100644 --- a/src/main/java/org/spongepowered/asm/mixin/transformer/DefaultExtensions.java +++ b/src/main/java/org/spongepowered/asm/mixin/transformer/DefaultExtensions.java @@ -25,6 +25,7 @@ package org.spongepowered.asm.mixin.transformer; import org.spongepowered.asm.mixin.MixinEnvironment; +import org.spongepowered.asm.mixin.injection.callback.CallbackClassGenerator; import org.spongepowered.asm.mixin.injection.invoke.arg.ArgsClassGenerator; import org.spongepowered.asm.mixin.transformer.ext.Extensions; import org.spongepowered.asm.mixin.transformer.ext.extensions.ExtensionCheckClass; @@ -51,6 +52,7 @@ public void accept(ISyntheticClassInfo item) { }; extensions.add(new ArgsClassGenerator(registryDelegate)); + extensions.add(new CallbackClassGenerator(registryDelegate)); extensions.add(new InnerClassGenerator(registryDelegate, nestHostCoprocessor)); extensions.add(new ExtensionClassExporter(environment)); From 9006b6f147133ac3165414a07ea5079b9f87c552 Mon Sep 17 00:00:00 2001 From: Chocohead Date: Tue, 30 Aug 2022 19:01:14 +0100 Subject: [PATCH 2/8] Fix build? Guess this must be a JDT exclusive inference --- .../asm/mixin/injection/callback/CallbackLocalInjector.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/org/spongepowered/asm/mixin/injection/callback/CallbackLocalInjector.java b/src/main/java/org/spongepowered/asm/mixin/injection/callback/CallbackLocalInjector.java index db6d77643..f1ba23d89 100644 --- a/src/main/java/org/spongepowered/asm/mixin/injection/callback/CallbackLocalInjector.java +++ b/src/main/java/org/spongepowered/asm/mixin/injection/callback/CallbackLocalInjector.java @@ -54,7 +54,7 @@ public String toString() { return String.format("Local[ordinal=%d, index=%d, names=%s]", ordinal, index, names); } } - protected class CallbackWithLocals extends Callback implements IPrettyPrintable { + protected class CallbackWithLocals extends CallbackLocalInjector.Callback implements IPrettyPrintable { public class CapturedLocal { public final int index; public final Type type; From cc19412555adc91cf98cfb3b406a99799a07b366 Mon Sep 17 00:00:00 2001 From: Chocohead Date: Tue, 30 Aug 2022 19:04:06 +0100 Subject: [PATCH 3/8] Fix some mixed indentation New things with tabs, old things with spaces --- .../injection/callback/CallbackClassGenerator.java | 6 +++--- .../mixin/injection/callback/CallbackInjector.java | 4 ++-- .../injection/callback/CallbackLocalInjector.java | 4 ++-- .../injection/struct/CallbackLocalInjectionInfo.java | 12 ++++++------ 4 files changed, 13 insertions(+), 13 deletions(-) diff --git a/src/main/java/org/spongepowered/asm/mixin/injection/callback/CallbackClassGenerator.java b/src/main/java/org/spongepowered/asm/mixin/injection/callback/CallbackClassGenerator.java index d004552f7..5e9fb4595 100644 --- a/src/main/java/org/spongepowered/asm/mixin/injection/callback/CallbackClassGenerator.java +++ b/src/main/java/org/spongepowered/asm/mixin/injection/callback/CallbackClassGenerator.java @@ -87,9 +87,9 @@ public boolean isLoaded() { static final String GET_LOCAL = "getLocal$"; private static final ILogger logger = MixinService.getService().getLogger("mixin"); private final IConsumer registry; - private final Map pool = new HashMap(); - private final Map nameToClass = new HashMap(); - private int nextIndex = 1; + private final Map pool = new HashMap(); + private final Map nameToClass = new HashMap(); + private int nextIndex = 1; public CallbackClassGenerator(IConsumer registry) { this.registry = registry; diff --git a/src/main/java/org/spongepowered/asm/mixin/injection/callback/CallbackInjector.java b/src/main/java/org/spongepowered/asm/mixin/injection/callback/CallbackInjector.java index 0fad7b9fb..cf10e15c5 100644 --- a/src/main/java/org/spongepowered/asm/mixin/injection/callback/CallbackInjector.java +++ b/src/main/java/org/spongepowered/asm/mixin/injection/callback/CallbackInjector.java @@ -416,7 +416,7 @@ int marshalVar() { * @param localCapture Local variable capture behaviour */ public CallbackInjector(InjectionInfo info, boolean cancellable, LocalCapture localCapture, String identifier) { - this(info, "@Inject", cancellable, localCapture, identifier); + this(info, "@Inject", cancellable, localCapture, identifier); } protected CallbackInjector(InjectionInfo info, String annotation, boolean cancellable, LocalCapture localCapture, String identifier) { @@ -680,7 +680,7 @@ private void printLocals(final Callback callback) { * @param callback callback handle */ protected void prepareCallbackIfNeeded(Callback callback, boolean forceStore) { - if (callback.usesCallbackInfo) { + if (callback.usesCallbackInfo) { dupReturnValue(callback); if (forceStore || cancellable || totalInjections > 1) { createCallbackInfo(callback, true); diff --git a/src/main/java/org/spongepowered/asm/mixin/injection/callback/CallbackLocalInjector.java b/src/main/java/org/spongepowered/asm/mixin/injection/callback/CallbackLocalInjector.java index f1ba23d89..9f5c3d860 100644 --- a/src/main/java/org/spongepowered/asm/mixin/injection/callback/CallbackLocalInjector.java +++ b/src/main/java/org/spongepowered/asm/mixin/injection/callback/CallbackLocalInjector.java @@ -188,7 +188,7 @@ protected String getCallbackInfoClass() { } return classGenerator.getArgsClass(info.getMixin().getMixin(), target.returnType, isAtReturn, modifyingLocals.toArray(new Type[0])).getName(); - } + } boolean willModifyLocals() { return modifiesLocals; @@ -363,7 +363,7 @@ private String generateBadLocalsMessage(CallbackWithLocals callback) { } return Joiner.on("\n\t").join(errors); - } + } private void invokeCallback(CallbackWithLocals callback, MethodNode callbackMethod) { // Push "this" onto the stack if the callback is not static diff --git a/src/main/java/org/spongepowered/asm/mixin/injection/struct/CallbackLocalInjectionInfo.java b/src/main/java/org/spongepowered/asm/mixin/injection/struct/CallbackLocalInjectionInfo.java index 5e3c6a7f9..6f420b596 100644 --- a/src/main/java/org/spongepowered/asm/mixin/injection/struct/CallbackLocalInjectionInfo.java +++ b/src/main/java/org/spongepowered/asm/mixin/injection/struct/CallbackLocalInjectionInfo.java @@ -34,9 +34,9 @@ protected Injector parseInjector(AnnotationNode injectAnnotation) { int i = 0; for (AnnotationNode local : localNodes) { int ordinal = Annotations.getValue(local, "ordinal", -1); - int index = Annotations.getValue(local, "index", -1); - List names = Annotations.>getValue(local, "name", Collections.emptyList()); - locals[i++] = new Local(ordinal, index, !names.isEmpty() ? new HashSet(names) : Collections.emptySet()); + int index = Annotations.getValue(local, "index", -1); + List names = Annotations.>getValue(local, "name", Collections.emptyList()); + locals[i++] = new Local(ordinal, index, !names.isEmpty() ? new HashSet(names) : Collections.emptySet()); } String identifier = Annotations.getValue(injectAnnotation, "id", ""); @@ -44,7 +44,7 @@ protected Injector parseInjector(AnnotationNode injectAnnotation) { } @Override - protected String getDescription() { - return "Callback method with locals"; - } + protected String getDescription() { + return "Callback method with locals"; + } } \ No newline at end of file From 08287d96d713fb4af22cf8b67d636470be32a89b Mon Sep 17 00:00:00 2001 From: Chocohead Date: Tue, 30 Aug 2022 20:29:18 +0100 Subject: [PATCH 4/8] Fix missing the CallbackInfo giving a misleading error Got to make sure the index is only bumped when not going back --- .../mixin/injection/callback/CallbackLocalInjector.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/main/java/org/spongepowered/asm/mixin/injection/callback/CallbackLocalInjector.java b/src/main/java/org/spongepowered/asm/mixin/injection/callback/CallbackLocalInjector.java index 9f5c3d860..fcde73d89 100644 --- a/src/main/java/org/spongepowered/asm/mixin/injection/callback/CallbackLocalInjector.java +++ b/src/main/java/org/spongepowered/asm/mixin/injection/callback/CallbackLocalInjector.java @@ -54,7 +54,7 @@ public String toString() { return String.format("Local[ordinal=%d, index=%d, names=%s]", ordinal, index, names); } } - protected class CallbackWithLocals extends CallbackLocalInjector.Callback implements IPrettyPrintable { + protected class CallbackWithLocals extends CallbackInjector.Callback implements IPrettyPrintable { public class CapturedLocal { public final int index; public final Type type; @@ -207,10 +207,10 @@ boolean checkDescriptor(String desc) { } Type callback = Type.getObjectType(target.getCallbackInfoClass()); - if (i >= args.length || !callback.equals(args[i++])) {//Callback is wrong, perhaps a returnable one has/n't been used when it should/n't + if (i >= args.length || !callback.equals(args[i])) {//Callback is wrong, perhaps a returnable one has/n't been used when it should/n't callback = Type.getObjectType(CallbackInfo.getCallInfoClassName(Type.VOID_TYPE.equals(target.returnType) ? Type.INT_TYPE : Type.VOID_TYPE)); - if (--i < args.length && callback.equals(args[i])) {//Wrong callback it is + if (i < args.length && callback.equals(args[i])) {//Wrong callback it is String correct = callback.getInternalName(); throw new InvalidInjectionException(info, String.format("Invalid descriptor on %s! %s is required!", info, correct.substring(correct.lastIndexOf('/') + 1))); } else {//Still not right... @@ -218,7 +218,7 @@ boolean checkDescriptor(String desc) { throw new InvalidInjectionException(info, String.format("Invalid descriptor on %s! Expected %s after target parameter%s but found %s", info, target.arguments.length > 1 ? "s" : "", correct.substring(correct.lastIndexOf('/') + 1), args.length > i ? args[i] : "")); } - } + } else i++; for (CapturedLocal local : capturedLocals) { if (!local.isSuccessful() || i >= args.length || !local.type.equals(args[i++])) { From c31c57922c0177e3575274d23864eb7efe7584f3 Mon Sep 17 00:00:00 2001 From: Chocohead Date: Tue, 30 Aug 2022 20:36:02 +0100 Subject: [PATCH 5/8] Fix build some more Quite particular about its generics J6 --- .../asm/mixin/injection/callback/CallbackLocalInjector.java | 3 +-- .../mixin/injection/struct/CallbackLocalInjectionInfo.java | 4 ++-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/main/java/org/spongepowered/asm/mixin/injection/callback/CallbackLocalInjector.java b/src/main/java/org/spongepowered/asm/mixin/injection/callback/CallbackLocalInjector.java index fcde73d89..44b71a0b4 100644 --- a/src/main/java/org/spongepowered/asm/mixin/injection/callback/CallbackLocalInjector.java +++ b/src/main/java/org/spongepowered/asm/mixin/injection/callback/CallbackLocalInjector.java @@ -32,7 +32,6 @@ import org.spongepowered.asm.util.Bytecode; import org.spongepowered.asm.util.PrettyPrinter; import org.spongepowered.asm.util.SignaturePrinter; -import org.spongepowered.asm.util.PrettyPrinter.IPrettyPrintable; public class CallbackLocalInjector extends CallbackInjector { public static class Local { @@ -54,7 +53,7 @@ public String toString() { return String.format("Local[ordinal=%d, index=%d, names=%s]", ordinal, index, names); } } - protected class CallbackWithLocals extends CallbackInjector.Callback implements IPrettyPrintable { + protected class CallbackWithLocals extends CallbackInjector.Callback implements PrettyPrinter.IPrettyPrintable { public class CapturedLocal { public final int index; public final Type type; diff --git a/src/main/java/org/spongepowered/asm/mixin/injection/struct/CallbackLocalInjectionInfo.java b/src/main/java/org/spongepowered/asm/mixin/injection/struct/CallbackLocalInjectionInfo.java index 6f420b596..ec614961a 100644 --- a/src/main/java/org/spongepowered/asm/mixin/injection/struct/CallbackLocalInjectionInfo.java +++ b/src/main/java/org/spongepowered/asm/mixin/injection/struct/CallbackLocalInjectionInfo.java @@ -35,8 +35,8 @@ protected Injector parseInjector(AnnotationNode injectAnnotation) { for (AnnotationNode local : localNodes) { int ordinal = Annotations.getValue(local, "ordinal", -1); int index = Annotations.getValue(local, "index", -1); - List names = Annotations.>getValue(local, "name", Collections.emptyList()); - locals[i++] = new Local(ordinal, index, !names.isEmpty() ? new HashSet(names) : Collections.emptySet()); + List names = Annotations.>getValue(local, "name", Collections.emptyList()); + locals[i++] = new Local(ordinal, index, !names.isEmpty() ? new HashSet(names) : Collections.emptySet()); } String identifier = Annotations.getValue(injectAnnotation, "id", ""); From baac651028e07e10ad315feb173a3f4a7601e5a8 Mon Sep 17 00:00:00 2001 From: Chocohead Date: Tue, 30 Aug 2022 21:28:25 +0100 Subject: [PATCH 6/8] Finish fixing build This seems very unnecessary yet 5ada553 says otherwise --- .../asm/mixin/injection/callback/CallbackLocalInjector.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/org/spongepowered/asm/mixin/injection/callback/CallbackLocalInjector.java b/src/main/java/org/spongepowered/asm/mixin/injection/callback/CallbackLocalInjector.java index 44b71a0b4..085f07bbb 100644 --- a/src/main/java/org/spongepowered/asm/mixin/injection/callback/CallbackLocalInjector.java +++ b/src/main/java/org/spongepowered/asm/mixin/injection/callback/CallbackLocalInjector.java @@ -53,7 +53,7 @@ public String toString() { return String.format("Local[ordinal=%d, index=%d, names=%s]", ordinal, index, names); } } - protected class CallbackWithLocals extends CallbackInjector.Callback implements PrettyPrinter.IPrettyPrintable { + protected class CallbackWithLocals extends CallbackInjector.Callback implements org.spongepowered.asm.util.PrettyPrinter.IPrettyPrintable { public class CapturedLocal { public final int index; public final Type type; From ce9b4aff74567f464bfe5c94d81752600c0c4b42 Mon Sep 17 00:00:00 2001 From: Chocohead Date: Tue, 30 Aug 2022 21:36:31 +0100 Subject: [PATCH 7/8] Fix `CAPTURE_FAILEXCEPTION` Don't want to try pass in invalid locals into the error throwing method --- .../callback/CallbackLocalInjector.java | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/src/main/java/org/spongepowered/asm/mixin/injection/callback/CallbackLocalInjector.java b/src/main/java/org/spongepowered/asm/mixin/injection/callback/CallbackLocalInjector.java index 085f07bbb..bc01609e9 100644 --- a/src/main/java/org/spongepowered/asm/mixin/injection/callback/CallbackLocalInjector.java +++ b/src/main/java/org/spongepowered/asm/mixin/injection/callback/CallbackLocalInjector.java @@ -228,6 +228,11 @@ boolean checkDescriptor(String desc) { return true; } + @Override + String getDescriptor() { + return desc; //Only used for the error throwing handler + } + @Override public void print(PrettyPrinter printer) { printer.add(" %5s %7s %30s %s", "INDEX", "ORDINAL", "TYPE", "NAME"); @@ -309,7 +314,7 @@ protected void inject(Target target, InjectionNode node) { } prepareCallbackIfNeeded(callback, callback.willModifyLocals()); - invokeCallback(callback, callbackMethod); + invokeCallback(callback, callbackMethod, callbackMethod != methodNode); if (callback.usesCallbackInfo) injectCancellationCode(callback); callback.inject(); @@ -364,7 +369,7 @@ private String generateBadLocalsMessage(CallbackWithLocals callback) { return Joiner.on("\n\t").join(errors); } - private void invokeCallback(CallbackWithLocals callback, MethodNode callbackMethod) { + private void invokeCallback(CallbackWithLocals callback, MethodNode callbackMethod, boolean isError) { // Push "this" onto the stack if the callback is not static if (!isStatic) { callback.add(new VarInsnNode(Opcodes.ALOAD, 0), false, true); @@ -376,18 +381,20 @@ private void invokeCallback(CallbackWithLocals callback, MethodNode callbackMeth } // Push the callback info onto the stack - loadOrCreateCallbackInfo(callback, callback.willModifyLocals()); + loadOrCreateCallbackInfo(callback, !isError && callback.willModifyLocals()); // Push the locals onto the stack - for (CapturedLocal local : callback.capturedLocals) { - callback.add(new VarInsnNode(local.type.getOpcode(Opcodes.ILOAD), local.index)); + if (!isError) { + for (CapturedLocal local : callback.capturedLocals) { + callback.add(new VarInsnNode(local.type.getOpcode(Opcodes.ILOAD), local.index)); + } } // Call the callback! invokeHandler(callback, callbackMethod); // Capture changes to locals in the handler - if (callback.willModifyLocals() && Annotations.getInvisible(callbackMethod, ModificationsCaught.class) == null) { + if (!isError && callback.willModifyLocals() && Annotations.getInvisible(callbackMethod, ModificationsCaught.class) == null) { String callbackType = callback.getCallbackInfoClass(); for (AbstractInsnNode insn : callbackMethod.instructions) { From 701497d3dcbaec78add2652feb649595dbdcd91c Mon Sep 17 00:00:00 2001 From: Chocohead Date: Tue, 30 Aug 2022 23:39:32 +0100 Subject: [PATCH 8/8] License headers We'll get there eventually --- .../asm/mixin/injection/InjectWithLocals.java | 24 +++++++++++++++++++ .../callback/CallbackClassGenerator.java | 24 +++++++++++++++++++ .../callback/CallbackLocalInjector.java | 24 +++++++++++++++++++ .../callback/ModificationsCaught.java | 24 +++++++++++++++++++ .../struct/CallbackLocalInjectionInfo.java | 24 +++++++++++++++++++ 5 files changed, 120 insertions(+) diff --git a/src/main/java/org/spongepowered/asm/mixin/injection/InjectWithLocals.java b/src/main/java/org/spongepowered/asm/mixin/injection/InjectWithLocals.java index bd9cf19b9..a67275dc8 100644 --- a/src/main/java/org/spongepowered/asm/mixin/injection/InjectWithLocals.java +++ b/src/main/java/org/spongepowered/asm/mixin/injection/InjectWithLocals.java @@ -1,3 +1,27 @@ +/* + * This file is part of Mixin, licensed under the MIT License (MIT). + * + * Copyright (c) SpongePowered + * Copyright (c) contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ package org.spongepowered.asm.mixin.injection; import java.lang.annotation.ElementType; diff --git a/src/main/java/org/spongepowered/asm/mixin/injection/callback/CallbackClassGenerator.java b/src/main/java/org/spongepowered/asm/mixin/injection/callback/CallbackClassGenerator.java index 5e9fb4595..c938f0005 100644 --- a/src/main/java/org/spongepowered/asm/mixin/injection/callback/CallbackClassGenerator.java +++ b/src/main/java/org/spongepowered/asm/mixin/injection/callback/CallbackClassGenerator.java @@ -1,3 +1,27 @@ +/* + * This file is part of Mixin, licensed under the MIT License (MIT). + * + * Copyright (c) SpongePowered + * Copyright (c) contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ package org.spongepowered.asm.mixin.injection.callback; import java.util.Arrays; diff --git a/src/main/java/org/spongepowered/asm/mixin/injection/callback/CallbackLocalInjector.java b/src/main/java/org/spongepowered/asm/mixin/injection/callback/CallbackLocalInjector.java index bc01609e9..d9f84c85c 100644 --- a/src/main/java/org/spongepowered/asm/mixin/injection/callback/CallbackLocalInjector.java +++ b/src/main/java/org/spongepowered/asm/mixin/injection/callback/CallbackLocalInjector.java @@ -1,3 +1,27 @@ +/* + * This file is part of Mixin, licensed under the MIT License (MIT). + * + * Copyright (c) SpongePowered + * Copyright (c) contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ package org.spongepowered.asm.mixin.injection.callback; import java.util.ArrayList; diff --git a/src/main/java/org/spongepowered/asm/mixin/injection/callback/ModificationsCaught.java b/src/main/java/org/spongepowered/asm/mixin/injection/callback/ModificationsCaught.java index 14b98ce7d..a529c87ef 100644 --- a/src/main/java/org/spongepowered/asm/mixin/injection/callback/ModificationsCaught.java +++ b/src/main/java/org/spongepowered/asm/mixin/injection/callback/ModificationsCaught.java @@ -1,3 +1,27 @@ +/* + * This file is part of Mixin, licensed under the MIT License (MIT). + * + * Copyright (c) SpongePowered + * Copyright (c) contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ package org.spongepowered.asm.mixin.injection.callback; import java.lang.annotation.ElementType; diff --git a/src/main/java/org/spongepowered/asm/mixin/injection/struct/CallbackLocalInjectionInfo.java b/src/main/java/org/spongepowered/asm/mixin/injection/struct/CallbackLocalInjectionInfo.java index ec614961a..e9d22bdfe 100644 --- a/src/main/java/org/spongepowered/asm/mixin/injection/struct/CallbackLocalInjectionInfo.java +++ b/src/main/java/org/spongepowered/asm/mixin/injection/struct/CallbackLocalInjectionInfo.java @@ -1,3 +1,27 @@ +/* + * This file is part of Mixin, licensed under the MIT License (MIT). + * + * Copyright (c) SpongePowered + * Copyright (c) contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ package org.spongepowered.asm.mixin.injection.struct; import java.util.Collections;