org.spongepowered.asm.mixin.injection.struct.Target Maven / Gradle / Ivy
Show all versions of sponge-mixin Show documentation
/*
* 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.ArrayList;
import java.util.Iterator;
import java.util.List;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.Type;
import org.objectweb.asm.tree.AbstractInsnNode;
import org.objectweb.asm.tree.ClassNode;
import org.objectweb.asm.tree.InsnList;
import org.objectweb.asm.tree.LabelNode;
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.spongepowered.asm.mixin.injection.callback.CallbackInfo;
import org.spongepowered.asm.mixin.injection.points.BeforeNew;
import org.spongepowered.asm.mixin.injection.struct.InjectionNodes.InjectionNode;
import org.spongepowered.asm.mixin.transformer.ClassInfo;
import org.spongepowered.asm.util.Bytecode;
import org.spongepowered.asm.util.Constants;
import org.spongepowered.asm.util.Counter;
import org.spongepowered.asm.util.Locals.SyntheticLocalVariableNode;
/**
* Information about the current injection target (method) which bundles common
* injection context with the target method in order to allow injectors to
* interoperate.
*/
public class Target implements Comparable, Iterable {
/**
* A stateful extension of stack or locals space for a target method which
* can be applied.
*
* Stack and locals extensions are generally localised to a particular
* injection. This means they don't necessarily need to accumulate, it's
* generally sufficient to expand the method's original stack or
* locals size by the extension amount, with the largest extension
* determining the final stack size of the method.
*
* Before this mechanism, injectors generally needed to keep track of
* their extension sizes via arbitrary integers, then apply them in one go.
* However some injector logic made the flow of these integers hard to track
* and lead to insidious bugs when values which should be combined were
* instead applied sequentially. The minor performance trade-off of using
* extensions to track these operations is covered by the increase in
* robustness.
*/
public class Extension {
/**
* True if this is a locals extension, false for stack.
*/
private final boolean locals;
/**
* The current 'depth' of this extension. Discrete operations maintain
* their own size value. When an extension is applied this value resets
* to zero so that the same extension can be used for multiple
* operations.
*/
private int size;
Extension(boolean locals) {
this.locals = locals;
}
/**
* Add 1 to this extension
*
* @return fluent interface
*/
public Extension add() {
this.size++;
return this;
}
/**
* Add the specified size to this extension
*
* @param size size to add
* @return fluent interface
*/
public Extension add(int size) {
this.size += size;
return this;
}
/**
* Add the size of the supplied type array to this extension. The size
* of the supplied args will first be computed, then the result will be
* added to the extension.
*
* @param types Types to add
* @return fluent interface
*/
public Extension add(Type[] types) {
return this.add(Bytecode.getArgsSize(types));
}
/**
* Set the size of this extension
*
* @param size Size to set
* @return fluent interface
*/
public Extension set(int size) {
this.size = size;
return this;
}
/**
* Get the current size of this extension
*
* @return fluent interface
*/
public int get() {
return this.size;
}
/**
* Apply this extension to the target. This reduces the current size to
* zero so that a new operation can begin if required.
*/
public void apply() {
if (this.locals) {
Target.this.extendLocalsBy(this.size);
} else {
Target.this.extendStackBy(this.size);
}
this.size = 0;
}
}
/**
* Target class info
*/
public final ClassInfo classInfo;
/**
* Target class node
*/
public final ClassNode classNode;
/**
* Target method
*/
public final MethodNode method;
/**
* Method instructions
*/
public final InsnList insns;
/**
* True if the method is static
*/
public final boolean isStatic;
/**
* Method arguments
*/
public final Type[] arguments;
/**
* Return type computed from the method descriptor
*/
public final Type returnType;
/**
* Method's (original) MAXS
*/
private final int maxStack;
/**
* Method's original max locals
*/
private final int maxLocals;
/**
* Nodes targetted by injectors
*/
private final InjectionNodes injectionNodes = new InjectionNodes();
/**
* Callback info class
*/
private String callbackInfoClass;
/**
* Callback method descriptor based on this target
*/
private String callbackDescriptor;
/**
* Method argument slots
*/
private int[] argIndices;
/**
* Local variables used for argmap slots
*/
private List argMapVars;
/**
* Labels for LVT ranges, generated as needed
*/
private LabelNode start, end;
/**
* Make a new Target for the supplied method
*
* @param method target method
*/
Target(ClassInfo classInfo, ClassNode classNode, MethodNode method) {
this.classInfo = classInfo;
this.classNode = classNode;
this.method = method;
this.insns = method.instructions;
this.isStatic = Bytecode.isStatic(method);
this.arguments = Type.getArgumentTypes(method.desc);
this.returnType = Type.getReturnType(method.desc);
this.maxStack = method.maxStack;
this.maxLocals = method.maxLocals;
}
/**
* Add an injection node to this target if it does not already exist,
* returns the existing node if it exists
*
* @param node Instruction node to add
* @return wrapper for the specified node
*/
public InjectionNode addInjectionNode(AbstractInsnNode node) {
return this.injectionNodes.add(node);
}
/**
* Get an injection node from this collection if it already exists, returns
* null if the node is not tracked
*
* @param node instruction node
* @return wrapper node or null if not tracked
*/
public InjectionNode getInjectionNode(AbstractInsnNode node) {
return this.injectionNodes.get(node);
}
/**
* Get the target method name
*/
public String getName() {
return this.method.name;
}
/**
* Get the target method descriptor
*/
public String getDesc() {
return this.method.desc;
}
/**
* Get the target method signature
*/
public String getSignature() {
return this.method.signature;
}
/**
* Get the original max locals of the method
*
* @return the original max locals value
*/
public int getMaxLocals() {
return this.maxLocals;
}
/**
* Get the original max stack of the method
*
* @return the original max stack value
*/
public int getMaxStack() {
return this.maxStack;
}
/**
* Get the current max locals of the method
*
* @return the current max local value
*/
public int getCurrentMaxLocals() {
return this.method.maxLocals;
}
/**
* Get the current max stack of the method
*
* @return the current max stack value
*/
public int getCurrentMaxStack() {
return this.method.maxStack;
}
/**
* Allocate a new local variable for the method
*
* Note that allocating a local differs from extending
* locals (see {@link #extendLocals()}) in that the allocation immediately
* increases the target max locals and returns the new index of the local
* just allocated. Extending locals is used for temporary localised
* extensions to the locals table for the purposes of injectors.
*
* @return the allocated local index
*/
public int allocateLocal() {
return this.allocateLocals(1);
}
/**
* Allocate a number of new local variables for this method, returns the
* first local variable index of the allocated range.
*
* Note that allocating locals differs from extending
* locals (see {@link #extendLocals()}) in that the allocation immediately
* increases the target max locals and returns the new index of the local
* just allocated. Extending locals is used for temporary localised
* extensions to the locals table for the purposes of injectors.
*
* @param locals number of locals to allocate
* @return the first local variable index of the allocated range
*/
public int allocateLocals(int locals) {
int nextLocal = this.method.maxLocals;
this.method.maxLocals += locals;
return nextLocal;
}
/**
* Creates a new locals extension. The returned object can be used to create
* extensions to the method locals for an injector to use. Extensions are
* not designed to overlap, and the largest extension for a method will
* determine the amount by which the locals table is extended.
*
* @return extension handle
*/
public Extension extendLocals() {
return new Extension(true);
}
/**
* Creates a new stack extension. The returned object can be used to create
* extensions to the method stack for an injector to use. Extensions are
* not designed to overlap, and the largest extension for a method will
* determine the amount by which the stack is extended.
*
* @return extension handle
*/
public Extension extendStack() {
return new Extension(false);
}
/**
* Allocate a number of new local variables for this method
*
* @param locals number of locals to allocate
*/
void extendLocalsBy(int locals) {
this.setMaxLocals(this.maxLocals + locals);
}
/**
* Set the maxlocals for this target to the specified value, the specfied
* value must be higher than the original max locals
*
* @param maxLocals max locals value to set
*/
private void setMaxLocals(int maxLocals) {
if (maxLocals > this.method.maxLocals) {
this.method.maxLocals = maxLocals;
}
}
/**
* Allocate a number of new stack variables for this method
*
* @param stack number of stack entries to allocate
*/
void extendStackBy(int stack) {
this.setMaxStack(this.maxStack + stack);
}
/**
* Set the max stack size for this target to the specified value, the
* specfied value must be higher than the original max stack
*
* @param maxStack max stack value to set
*/
private void setMaxStack(int maxStack) {
if (maxStack > this.method.maxStack) {
this.method.maxStack = maxStack;
}
}
/**
* Generate an array containing local indexes for the specified args,
* returns an array of identical size to the supplied array with an
* allocated local index in each corresponding position
*
* @param args Argument types
* @param start starting index
* @return array containing a corresponding local arg index for each member
* of the supplied args array
*/
public int[] generateArgMap(Type[] args, int start) {
return this.generateArgMap(args, start, false);
}
/**
* Generate an array containing local indexes for the specified args,
* returns an array of identical size to the supplied array with an
* allocated local index in each corresponding position
*
* @param args Argument types
* @param start starting index
* @param fresh allocate fresh locals only, do not reuse existing argmap
* slots
* @return array containing a corresponding local arg index for each member
* of the supplied args array
*/
public int[] generateArgMap(Type[] args, int start, boolean fresh) {
if (this.argMapVars == null) {
this.argMapVars = new ArrayList();
}
int[] argMap = new int[args.length];
Counter index = new Counter();
for (int arg = start; arg < args.length; arg++) {
int size = args[arg].getSize();
if (fresh) {
argMap[arg] = this.allocateLocals(size);
index.value += size;
} else {
argMap[arg] = this.allocateArgMapLocal(index, size);
}
}
return argMap;
}
/**
* Allocates a local variable to be used for argmap. Since argmaps do not
* overlap we can safely reuse local variables allocated for this purpose.
* We do however need to deal with expanding the argmap allocation when
* necessary, which is complicated slightly by the fact that some variables
* occupy more than one local slot.
*
* @param index Argmap index
* @param size Size of variable
* @return local offset to use
*/
private int allocateArgMapLocal(Counter index, int size) {
boolean isLastSlotFree = index.value < this.argMapVars.size();
while (index.value < this.argMapVars.size()) {
int local = this.argMapVars.get(index.value);
if (size == 1) {
index.value++;
return local;
}
int nextIndex = index.value + 1;
if (nextIndex < this.argMapVars.size() && local + 1 == this.argMapVars.get(nextIndex)) {
// We own the next slot so this is a safe place to store a double-width type.
// We've consumed the next slot so increment the next available index.
index.value += 2;
return local;
}
index.value++;
}
// We don't have anywhere to store the arg.
int newLocal = this.allocateLocal();
this.argMapVars.add(newLocal);
index.value++;
if (size == 1) {
return newLocal;
}
if (isLastSlotFree && newLocal == this.argMapVars.get(this.argMapVars.size() - 2) + 1) {
// We own the preceding local as well, and it is not yet used by this argMap, so we can store our
// double-width type there.
return newLocal - 1;
}
// Allocate 1 more local so we can fit the double-width type. We know it will follow directly from the
// previous one.
this.argMapVars.add(this.allocateLocal());
index.value++;
return newLocal;
}
/**
* Get the argument indices for this target, calculated on first use
*
* @return argument indices for this target
*/
public int[] getArgIndices() {
if (this.argIndices == null) {
this.argIndices = this.calcArgIndices(this.isStatic ? 0 : 1);
}
return this.argIndices;
}
private int[] calcArgIndices(int local) {
int[] argIndices = new int[this.arguments.length];
for (int arg = 0; arg < this.arguments.length; arg++) {
argIndices[arg] = local;
local += this.arguments[arg].getSize();
}
return argIndices;
}
/**
* Get the CallbackInfo class used for this target, based on the target
* return type
*
* @return CallbackInfo class name
*/
public String getCallbackInfoClass() {
if (this.callbackInfoClass == null) {
this.callbackInfoClass = CallbackInfo.getCallInfoClassName(this.returnType);
}
return this.callbackInfoClass;
}
/**
* Get "simple" callback descriptor (descriptor with only CallbackInfo)
*
* @return generated descriptor
*/
public String getSimpleCallbackDescriptor() {
return String.format("(L%s;)V", this.getCallbackInfoClass());
}
/**
* Get the callback descriptor
*
* @param locals Local variable types
* @param argumentTypes Argument types
* @return generated descriptor
*/
public String getCallbackDescriptor(final Type[] locals, Type[] argumentTypes) {
return this.getCallbackDescriptor(false, locals, argumentTypes, 0, Short.MAX_VALUE);
}
/**
* Get the callback descriptor
*
* @param captureLocals True if the callback is capturing locals
* @param locals Local variable types
* @param argumentTypes Argument types
* @param startIndex local index to start at
* @param extra extra locals to include
* @return generated descriptor
*/
public String getCallbackDescriptor(final boolean captureLocals, final Type[] locals, Type[] argumentTypes, int startIndex, int extra) {
if (this.callbackDescriptor == null) {
this.callbackDescriptor = String.format("(%sL%s;)V", this.getDesc().substring(1, this.getDesc().indexOf(')')),
this.getCallbackInfoClass());
}
if (!captureLocals || locals == null) {
return this.callbackDescriptor;
}
StringBuilder descriptor = new StringBuilder(this.callbackDescriptor.substring(0, this.callbackDescriptor.indexOf(')')));
for (int l = startIndex; l < locals.length && extra > 0; l++) {
if (locals[l] != null) {
descriptor.append(locals[l].getDescriptor());
extra--;
}
}
return descriptor.append(")V").toString();
}
@Override
public String toString() {
return String.format("%s::%s%s", this.classNode.name, this.getName(), this.getDesc());
}
@Override
public int compareTo(Target o) {
if (o == null) {
return Integer.MAX_VALUE;
}
return this.toString().compareTo(o.toString());
}
/**
* Return the index of the specified instruction in this instruction list
*
* @param node instruction to locate, must exist in the target
* @return opcode index
*/
public int indexOf(InjectionNode node) {
return this.insns.indexOf(node.getCurrentTarget());
}
/**
* Return the index of the specified instruction in this instruction list
*
* @param insn instruction to locate, must exist in the target
* @return opcode index
*/
public int indexOf(AbstractInsnNode insn) {
return this.insns.indexOf(insn);
}
/**
* Return the instruction at the specified index
*
* @param index opcode index
* @return requested instruction
*/
public AbstractInsnNode get(int index) {
return this.insns.get(index);
}
/* (non-Javadoc)
* @see java.lang.Iterable#iterator()
*/
@Override
public Iterator iterator() {
return this.insns.iterator();
}
/**
* Find the first <init> invocation after the specified
* NEW insn
*
* @param newNode NEW insn
* @return INVOKESPECIAL opcode of ctor, or null if not found
*/
public MethodInsnNode findInitNodeFor(TypeInsnNode newNode) {
return this.findInitNodeFor(newNode, null);
}
/**
* Find the matching <init> invocation after the specified
* NEW insn, ensuring that the supplied descriptor matches. If the
* supplied descriptor is null then any invocation matches. If
* additional NEW insns are encountered then corresponding
* <init> calls are skipped.
*
* @param newNode NEW insn
* @param desc Descriptor to match
* @return INVOKESPECIAL opcode of ctor, or null if not found
*/
public MethodInsnNode findInitNodeFor(TypeInsnNode newNode, String desc) {
return BeforeNew.findInitNodeFor(this.insns, newNode, desc);
}
/**
* Insert the supplied instructions after the specified instruction
*
* @param location Instruction to insert before
* @param insns Instructions to insert
*/
public void insert(InjectionNode location, final InsnList insns) {
this.insns.insert(location.getCurrentTarget(), insns);
}
/**
* Insert the supplied instruction after the specified instruction
*
* @param location Instruction to insert before
* @param insn Instruction to insert
*/
public void insert(InjectionNode location, final AbstractInsnNode insn) {
this.insns.insert(location.getCurrentTarget(), insn);
}
/**
* Insert the supplied instructions after the specified instruction
*
* @param location Instruction to insert before
* @param insns Instructions to insert
*/
public void insert(AbstractInsnNode location, final InsnList insns) {
this.insns.insert(location, insns);
}
/**
* Insert the supplied instruction after the specified instruction
*
* @param location Instruction to insert before
* @param insn Instruction to insert
*/
public void insert(AbstractInsnNode location, final AbstractInsnNode insn) {
this.insns.insert(location, insn);
}
/**
* Insert the supplied instructions before the specified instruction
*
* @param location Instruction to insert before
* @param insns Instructions to insert
*/
public void insertBefore(InjectionNode location, final InsnList insns) {
this.insns.insertBefore(location.getCurrentTarget(), insns);
}
/**
* Insert the supplied instruction before the specified instruction
*
* @param location Instruction to insert before
* @param insn Instruction to insert
*/
public void insertBefore(InjectionNode location, final AbstractInsnNode insn) {
this.insns.insertBefore(location.getCurrentTarget(), insn);
}
/**
* Insert the supplied instructions before the specified instruction
*
* @param location Instruction to insert before
* @param insns Instructions to insert
*/
public void insertBefore(AbstractInsnNode location, final InsnList insns) {
this.insns.insertBefore(location, insns);
}
/**
* Insert the supplied instruction before the specified instruction
*
* @param location Instruction to insert before
* @param insn Instruction to insert
*/
public void insertBefore(AbstractInsnNode location, final AbstractInsnNode insn) {
this.insns.insertBefore(location, insn);
}
/**
* Replace an instruction in this target with the specified instruction and
* mark the node as replaced for other injectors
*
* @param location Instruction to replace
* @param insn Instruction to replace with
*/
public void replaceNode(AbstractInsnNode location, AbstractInsnNode insn) {
this.insns.insertBefore(location, insn);
this.insns.remove(location);
this.injectionNodes.replace(location, insn);
}
/**
* Replace an instruction in this target with the specified instructions and
* mark the node as replaced with the specified champion node from the list.
*
* @param location Instruction to replace
* @param champion Instruction which notionally replaces the original insn
* @param insns Instructions to actually insert (must contain champion)
*/
public void replaceNode(AbstractInsnNode location, AbstractInsnNode champion, InsnList insns) {
this.insns.insertBefore(location, insns);
this.insns.remove(location);
this.injectionNodes.replace(location, champion);
}
/**
* Wrap instruction in this target with the specified instructions and mark
* the node as replaced with the specified champion node from the list.
*
* @param location Instruction to replace
* @param champion Instruction which notionally replaces the original insn
* @param before Instructions to actually insert (must contain champion)
* @param after Instructions to insert after the specified location
*/
public void wrapNode(AbstractInsnNode location, AbstractInsnNode champion, InsnList before, InsnList after) {
this.insns.insertBefore(location, before);
this.insns.insert(location, after);
this.injectionNodes.replace(location, champion);
}
/**
* Replace an instruction in this target with the specified instructions and
* mark the original node as removed
*
* @param location Instruction to replace
* @param insns Instructions to replace with
*/
public void replaceNode(AbstractInsnNode location, InsnList insns) {
this.insns.insertBefore(location, insns);
this.removeNode(location);
}
/**
* Remove the specified instruction from the target and mark it as removed
* for injections
*
* @param insn instruction to remove
*/
public void removeNode(AbstractInsnNode insn) {
this.insns.remove(insn);
this.injectionNodes.remove(insn);
}
/**
* Add an entry to the target LVT
*
* @param index local variable index
* @param name local variable name
* @param desc local variable type
*/
public void addLocalVariable(int index, String name, String desc) {
this.addLocalVariable(index, name, desc, null, null);
}
/**
* Add an entry to the target LVT between the specified start and end labels
*
* @param index local variable index
* @param name local variable name
* @param desc local variable type
* @param from start of range
* @param to end of range
*/
public void addLocalVariable(int index, String name, String desc, LabelNode from, LabelNode to) {
if (from == null) {
from = this.getStartLabel();
}
if (to == null) {
to = this.getEndLabel();
}
if (this.method.localVariables == null) {
this.method.localVariables = new ArrayList();
}
for (Iterator iter = this.method.localVariables.iterator(); iter.hasNext();) {
LocalVariableNode local = iter.next();
if (local != null && local.index == index && from == local.start && to == local.end) {
iter.remove();
}
}
this.method.localVariables.add(new SyntheticLocalVariableNode(name, desc, null, from, to, index));
}
/**
* Get a label which marks the very start of the method
*/
private LabelNode getStartLabel() {
if (this.start == null) {
this.insns.insert(this.start = new LabelNode());
}
return this.start;
}
/**
* Get a label which marks the very end of the method
*/
private LabelNode getEndLabel() {
if (this.end == null) {
this.insns.add(this.end = new LabelNode());
}
return this.end;
}
public static Target of(ClassInfo classInfo, ClassNode classNode, MethodNode method) {
if (method.name.equals(Constants.CTOR)) {
return new Constructor(classInfo, classNode, method);
}
return new Target(classInfo, classNode, method);
}
}