org.objectweb.asm.commons.JSRInlinerAdapter Maven / Gradle / Ivy
// ASM: a very small and fast Java bytecode manipulation framework
// Copyright (c) 2000-2011 INRIA, France Telecom
// All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions
// are met:
// 1. Redistributions of source code must retain the above copyright
// notice, this list of conditions and the following disclaimer.
// 2. Redistributions in binary form must reproduce the above copyright
// notice, this list of conditions and the following disclaimer in the
// documentation and/or other materials provided with the distribution.
// 3. Neither the name of the copyright holders nor the names of its
// contributors may be used to endorse or promote products derived from
// this software without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
// THE POSSIBILITY OF SUCH DAMAGE.
package org.objectweb.asm.commons;
import java.util.AbstractMap;
import java.util.ArrayList;
import java.util.BitSet;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.objectweb.asm.Label;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.tree.AbstractInsnNode;
import org.objectweb.asm.tree.InsnList;
import org.objectweb.asm.tree.InsnNode;
import org.objectweb.asm.tree.JumpInsnNode;
import org.objectweb.asm.tree.LabelNode;
import org.objectweb.asm.tree.LocalVariableNode;
import org.objectweb.asm.tree.LookupSwitchInsnNode;
import org.objectweb.asm.tree.MethodNode;
import org.objectweb.asm.tree.TableSwitchInsnNode;
import org.objectweb.asm.tree.TryCatchBlockNode;
/**
* A {@link org.objectweb.asm.MethodVisitor} that removes JSR instructions and inlines the
* referenced subroutines.
*
* @author Niko Matsakis
*/
// DontCheck(AbbreviationAsWordInName): can't be renamed (for backward binary compatibility).
public class JSRInlinerAdapter extends MethodNode implements Opcodes {
/**
* The instructions that belong to the main "subroutine". Bit i is set iff instruction at index i
* belongs to this main "subroutine".
*/
private final BitSet mainSubroutineInsns = new BitSet();
/**
* The instructions that belong to each subroutine. For each label which is the target of a JSR
* instruction, bit i of the corresponding BitSet in this map is set iff instruction at index i
* belongs to this subroutine.
*/
private final Map subroutinesInsns = new HashMap<>();
/**
* The instructions that belong to more that one subroutine. Bit i is set iff instruction at index
* i belongs to more than one subroutine.
*/
final BitSet sharedSubroutineInsns = new BitSet();
/**
* Constructs a new {@link JSRInlinerAdapter}. Subclasses must not use this constructor.
* Instead, they must use the {@link #JSRInlinerAdapter(int, MethodVisitor, int, String, String,
* String, String[])} version.
*
* @param methodVisitor the method visitor to send the resulting inlined method code to, or
* null
.
* @param access the method's access flags.
* @param name the method's name.
* @param descriptor the method's descriptor.
* @param signature the method's signature. May be {@literal null}.
* @param exceptions the internal names of the method's exception classes. May be {@literal null}.
* @throws IllegalStateException if a subclass calls this constructor.
*/
public JSRInlinerAdapter(
final MethodVisitor methodVisitor,
final int access,
final String name,
final String descriptor,
final String signature,
final String[] exceptions) {
this(
/* latest api = */ Opcodes.ASM8,
methodVisitor,
access,
name,
descriptor,
signature,
exceptions);
if (getClass() != JSRInlinerAdapter.class) {
throw new IllegalStateException();
}
}
/**
* Constructs a new {@link JSRInlinerAdapter}.
*
* @param api the ASM API version implemented by this visitor. Must be one of {@link
* Opcodes#ASM4}, {@link Opcodes#ASM5}, {@link Opcodes#ASM6}, {@link Opcodes#ASM7} or {@link
* Opcodes#ASM8}.
* @param methodVisitor the method visitor to send the resulting inlined method code to, or
* null
.
* @param access the method's access flags (see {@link Opcodes}). This parameter also indicates if
* the method is synthetic and/or deprecated.
* @param name the method's name.
* @param descriptor the method's descriptor.
* @param signature the method's signature. May be {@literal null}.
* @param exceptions the internal names of the method's exception classes. May be {@literal null}.
*/
protected JSRInlinerAdapter(
final int api,
final MethodVisitor methodVisitor,
final int access,
final String name,
final String descriptor,
final String signature,
final String[] exceptions) {
super(api, access, name, descriptor, signature, exceptions);
this.mv = methodVisitor;
}
@Override
public void visitJumpInsn(final int opcode, final Label label) {
super.visitJumpInsn(opcode, label);
LabelNode labelNode = ((JumpInsnNode) instructions.getLast()).label;
if (opcode == JSR && !subroutinesInsns.containsKey(labelNode)) {
subroutinesInsns.put(labelNode, new BitSet());
}
}
@Override
public void visitEnd() {
if (!subroutinesInsns.isEmpty()) {
// If the code contains at least one JSR instruction, inline the subroutines.
findSubroutinesInsns();
emitCode();
}
if (mv != null) {
accept(mv);
}
}
/** Determines, for each instruction, to which subroutine(s) it belongs. */
private void findSubroutinesInsns() {
// Find the instructions that belong to main subroutine.
BitSet visitedInsns = new BitSet();
findSubroutineInsns(0, mainSubroutineInsns, visitedInsns);
// For each subroutine, find the instructions that belong to this subroutine.
for (Map.Entry entry : subroutinesInsns.entrySet()) {
LabelNode jsrLabelNode = entry.getKey();
BitSet subroutineInsns = entry.getValue();
findSubroutineInsns(instructions.indexOf(jsrLabelNode), subroutineInsns, visitedInsns);
}
}
/**
* Finds the instructions that belong to the subroutine starting at the given instruction index.
* For this the control flow graph is visited with a depth first search (this includes the normal
* control flow and the exception handlers).
*
* @param startInsnIndex the index of the first instruction of the subroutine.
* @param subroutineInsns where the indices of the instructions of the subroutine must be stored.
* @param visitedInsns the indices of the instructions that have been visited so far (including in
* previous calls to this method). This bitset is updated by this method each time a new
* instruction is visited. It is used to make sure each instruction is visited at most once.
*/
private void findSubroutineInsns(
final int startInsnIndex, final BitSet subroutineInsns, final BitSet visitedInsns) {
// First find the instructions reachable via normal execution.
findReachableInsns(startInsnIndex, subroutineInsns, visitedInsns);
// Then find the instructions reachable via the applicable exception handlers.
while (true) {
boolean applicableHandlerFound = false;
for (TryCatchBlockNode tryCatchBlockNode : tryCatchBlocks) {
// If the handler has already been processed, skip it.
int handlerIndex = instructions.indexOf(tryCatchBlockNode.handler);
if (subroutineInsns.get(handlerIndex)) {
continue;
}
// If an instruction in the exception handler range belongs to the subroutine, the handler
// can be reached from the routine, and its instructions must be added to the subroutine.
int startIndex = instructions.indexOf(tryCatchBlockNode.start);
int endIndex = instructions.indexOf(tryCatchBlockNode.end);
int firstSubroutineInsnAfterTryCatchStart = subroutineInsns.nextSetBit(startIndex);
if (firstSubroutineInsnAfterTryCatchStart >= startIndex
&& firstSubroutineInsnAfterTryCatchStart < endIndex) {
findReachableInsns(handlerIndex, subroutineInsns, visitedInsns);
applicableHandlerFound = true;
}
}
// If an applicable exception handler has been found, other handlers may become applicable, so
// we must examine them again.
if (!applicableHandlerFound) {
return;
}
}
}
/**
* Finds the instructions that are reachable from the given instruction, without following any JSR
* instruction nor any exception handler. For this the control flow graph is visited with a depth
* first search.
*
* @param insnIndex the index of an instruction of the subroutine.
* @param subroutineInsns where the indices of the instructions of the subroutine must be stored.
* @param visitedInsns the indices of the instructions that have been visited so far (including in
* previous calls to this method). This bitset is updated by this method each time a new
* instruction is visited. It is used to make sure each instruction is visited at most once.
*/
private void findReachableInsns(
final int insnIndex, final BitSet subroutineInsns, final BitSet visitedInsns) {
int currentInsnIndex = insnIndex;
// We implicitly assume below that execution can always fall through to the next instruction
// after a JSR. But a subroutine may never return, in which case the code after the JSR is
// unreachable and can be anything. In particular, it can seem to fall off the end of the
// method, so we must handle this case here (we could instead detect whether execution can
// return or not from a JSR, but this is more complicated).
while (currentInsnIndex < instructions.size()) {
// Visit each instruction at most once.
if (subroutineInsns.get(currentInsnIndex)) {
return;
}
subroutineInsns.set(currentInsnIndex);
// Check if this instruction has already been visited by another subroutine.
if (visitedInsns.get(currentInsnIndex)) {
sharedSubroutineInsns.set(currentInsnIndex);
}
visitedInsns.set(currentInsnIndex);
AbstractInsnNode currentInsnNode = instructions.get(currentInsnIndex);
if (currentInsnNode.getType() == AbstractInsnNode.JUMP_INSN
&& currentInsnNode.getOpcode() != JSR) {
// Don't follow JSR instructions in the control flow graph.
JumpInsnNode jumpInsnNode = (JumpInsnNode) currentInsnNode;
findReachableInsns(instructions.indexOf(jumpInsnNode.label), subroutineInsns, visitedInsns);
} else if (currentInsnNode.getType() == AbstractInsnNode.TABLESWITCH_INSN) {
TableSwitchInsnNode tableSwitchInsnNode = (TableSwitchInsnNode) currentInsnNode;
findReachableInsns(
instructions.indexOf(tableSwitchInsnNode.dflt), subroutineInsns, visitedInsns);
for (LabelNode labelNode : tableSwitchInsnNode.labels) {
findReachableInsns(instructions.indexOf(labelNode), subroutineInsns, visitedInsns);
}
} else if (currentInsnNode.getType() == AbstractInsnNode.LOOKUPSWITCH_INSN) {
LookupSwitchInsnNode lookupSwitchInsnNode = (LookupSwitchInsnNode) currentInsnNode;
findReachableInsns(
instructions.indexOf(lookupSwitchInsnNode.dflt), subroutineInsns, visitedInsns);
for (LabelNode labelNode : lookupSwitchInsnNode.labels) {
findReachableInsns(instructions.indexOf(labelNode), subroutineInsns, visitedInsns);
}
}
// Check if this instruction falls through to the next instruction; if not, return.
switch (instructions.get(currentInsnIndex).getOpcode()) {
case GOTO:
case RET:
case TABLESWITCH:
case LOOKUPSWITCH:
case IRETURN:
case LRETURN:
case FRETURN:
case DRETURN:
case ARETURN:
case RETURN:
case ATHROW:
// Note: this either returns from this subroutine, or from a parent subroutine.
return;
default:
// Go to the next instruction.
currentInsnIndex++;
break;
}
}
}
/**
* Creates the new instructions, inlining each instantiation of each subroutine until the code is
* fully elaborated.
*/
private void emitCode() {
LinkedList worklist = new LinkedList<>();
// Create an instantiation of the main "subroutine", which is just the main routine.
worklist.add(new Instantiation(null, mainSubroutineInsns));
// Emit instantiations of each subroutine we encounter, including the main subroutine.
InsnList newInstructions = new InsnList();
List newTryCatchBlocks = new ArrayList<>();
List newLocalVariables = new ArrayList<>();
while (!worklist.isEmpty()) {
Instantiation instantiation = worklist.removeFirst();
emitInstantiation(
instantiation, worklist, newInstructions, newTryCatchBlocks, newLocalVariables);
}
instructions = newInstructions;
tryCatchBlocks = newTryCatchBlocks;
localVariables = newLocalVariables;
}
/**
* Emits an instantiation of a subroutine, specified by instantiation
. May add new
* instantiations that are invoked by this one to the worklist
, and new try/catch
* blocks to newTryCatchBlocks
.
*
* @param instantiation the instantiation that must be performed.
* @param worklist list of the instantiations that remain to be done.
* @param newInstructions the instruction list to which the instantiated code must be appended.
* @param newTryCatchBlocks the exception handler list to which the instantiated handlers must be
* appended.
* @param newLocalVariables the local variables list to which the instantiated local variables
* must be appended.
*/
private void emitInstantiation(
final Instantiation instantiation,
final List worklist,
final InsnList newInstructions,
final List newTryCatchBlocks,
final List newLocalVariables) {
LabelNode previousLabelNode = null;
for (int i = 0; i < instructions.size(); ++i) {
AbstractInsnNode insnNode = instructions.get(i);
if (insnNode.getType() == AbstractInsnNode.LABEL) {
// Always clone all labels, while avoiding to add the same label more than once.
LabelNode labelNode = (LabelNode) insnNode;
LabelNode clonedLabelNode = instantiation.getClonedLabel(labelNode);
if (clonedLabelNode != previousLabelNode) {
newInstructions.add(clonedLabelNode);
previousLabelNode = clonedLabelNode;
}
} else if (instantiation.findOwner(i) == instantiation) {
// Don't emit instructions that were already emitted by an ancestor subroutine. Note that it
// is still possible for a given instruction to be emitted twice because it may belong to
// two subroutines that do not invoke each other.
if (insnNode.getOpcode() == RET) {
// Translate RET instruction(s) to a jump to the return label for the appropriate
// instantiation. The problem is that the subroutine may "fall through" to the ret of a
// parent subroutine; therefore, to find the appropriate ret label we find the oldest
// instantiation that claims to own this instruction.
LabelNode retLabel = null;
for (Instantiation retLabelOwner = instantiation;
retLabelOwner != null;
retLabelOwner = retLabelOwner.parent) {
if (retLabelOwner.subroutineInsns.get(i)) {
retLabel = retLabelOwner.returnLabel;
}
}
if (retLabel == null) {
// This is only possible if the mainSubroutine owns a RET instruction, which should
// never happen for verifiable code.
throw new IllegalArgumentException(
"Instruction #" + i + " is a RET not owned by any subroutine");
}
newInstructions.add(new JumpInsnNode(GOTO, retLabel));
} else if (insnNode.getOpcode() == JSR) {
LabelNode jsrLabelNode = ((JumpInsnNode) insnNode).label;
BitSet subroutineInsns = subroutinesInsns.get(jsrLabelNode);
Instantiation newInstantiation = new Instantiation(instantiation, subroutineInsns);
LabelNode clonedJsrLabelNode = newInstantiation.getClonedLabelForJumpInsn(jsrLabelNode);
// Replace the JSR instruction with a GOTO to the instantiated subroutine, and push NULL
// for what was once the return address value. This hack allows us to avoid doing any sort
// of data flow analysis to figure out which instructions manipulate the old return
// address value pointer which is now known to be unneeded.
newInstructions.add(new InsnNode(ACONST_NULL));
newInstructions.add(new JumpInsnNode(GOTO, clonedJsrLabelNode));
newInstructions.add(newInstantiation.returnLabel);
// Insert this new instantiation into the queue to be emitted later.
worklist.add(newInstantiation);
} else {
newInstructions.add(insnNode.clone(instantiation));
}
}
}
// Emit the try/catch blocks that are relevant for this instantiation.
for (TryCatchBlockNode tryCatchBlockNode : tryCatchBlocks) {
final LabelNode start = instantiation.getClonedLabel(tryCatchBlockNode.start);
final LabelNode end = instantiation.getClonedLabel(tryCatchBlockNode.end);
if (start != end) {
final LabelNode handler =
instantiation.getClonedLabelForJumpInsn(tryCatchBlockNode.handler);
if (start == null || end == null || handler == null) {
throw new AssertionError("Internal error!");
}
newTryCatchBlocks.add(new TryCatchBlockNode(start, end, handler, tryCatchBlockNode.type));
}
}
// Emit the local variable nodes that are relevant for this instantiation.
for (LocalVariableNode localVariableNode : localVariables) {
final LabelNode start = instantiation.getClonedLabel(localVariableNode.start);
final LabelNode end = instantiation.getClonedLabel(localVariableNode.end);
if (start != end) {
newLocalVariables.add(
new LocalVariableNode(
localVariableNode.name,
localVariableNode.desc,
localVariableNode.signature,
start,
end,
localVariableNode.index));
}
}
}
/** An instantiation of a subroutine. */
private class Instantiation extends AbstractMap {
/**
* The instantiation from which this one was created (or {@literal null} for the instantiation
* of the main "subroutine").
*/
final Instantiation parent;
/**
* The original instructions that belong to the subroutine which is instantiated. Bit i is set
* iff instruction at index i belongs to this subroutine.
*/
final BitSet subroutineInsns;
/**
* A map from labels from the original code to labels pointing at code specific to this
* instantiation, for use in remapping try/catch blocks, as well as jumps.
*
* Note that in the presence of instructions belonging to several subroutines, we map the
* target label of a GOTO to the label used by the oldest instantiation (parent instantiations
* are older than their children). This avoids code duplication during inlining in most cases.
*/
final Map clonedLabels;
/** The return label for this instantiation, to which all original returns will be mapped. */
final LabelNode returnLabel;
Instantiation(final Instantiation parent, final BitSet subroutineInsns) {
for (Instantiation instantiation = parent;
instantiation != null;
instantiation = instantiation.parent) {
if (instantiation.subroutineInsns == subroutineInsns) {
throw new IllegalArgumentException("Recursive invocation of " + subroutineInsns);
}
}
this.parent = parent;
this.subroutineInsns = subroutineInsns;
this.returnLabel = parent == null ? null : new LabelNode();
this.clonedLabels = new HashMap<>();
// Create a clone of each label in the original code of the subroutine. Note that we collapse
// labels which point at the same instruction into one.
LabelNode clonedLabelNode = null;
for (int insnIndex = 0; insnIndex < instructions.size(); insnIndex++) {
AbstractInsnNode insnNode = instructions.get(insnIndex);
if (insnNode.getType() == AbstractInsnNode.LABEL) {
LabelNode labelNode = (LabelNode) insnNode;
// If we already have a label pointing at this spot, don't recreate it.
if (clonedLabelNode == null) {
clonedLabelNode = new LabelNode();
}
clonedLabels.put(labelNode, clonedLabelNode);
} else if (findOwner(insnIndex) == this) {
// We will emit this instruction, so clear the duplicateLabelNode flag since the next
// Label will refer to a distinct instruction.
clonedLabelNode = null;
}
}
}
/**
* Returns the "owner" of a particular instruction relative to this instantiation: the owner
* refers to the Instantiation which will emit the version of this instruction that we will
* execute.
*
* Typically, the return value is either this
or null
. this
*
indicates that this instantiation will generate the version of this instruction that
* we will execute, and null
indicates that this instantiation never executes the
* given instruction.
*
*
Sometimes, however, an instruction can belong to multiple subroutines; this is called a
* shared instruction, and occurs when multiple subroutines branch to common points of control.
* In this case, the owner is the oldest instantiation which owns the instruction in question
* (parent instantiations are older than their children).
*
* @param insnIndex the index of an instruction in the original code.
* @return the "owner" of a particular instruction relative to this instantiation.
*/
Instantiation findOwner(final int insnIndex) {
if (!subroutineInsns.get(insnIndex)) {
return null;
}
if (!sharedSubroutineInsns.get(insnIndex)) {
return this;
}
Instantiation owner = this;
for (Instantiation instantiation = parent;
instantiation != null;
instantiation = instantiation.parent) {
if (instantiation.subroutineInsns.get(insnIndex)) {
owner = instantiation;
}
}
return owner;
}
/**
* Returns the clone of the given original label that is appropriate for use in a jump
* instruction.
*
* @param labelNode a label of the original code.
* @return a clone of the given label for use in a jump instruction in the inlined code.
*/
LabelNode getClonedLabelForJumpInsn(final LabelNode labelNode) {
// findOwner should never return null, because owner is null only if an instruction cannot be
// reached from this subroutine.
return findOwner(instructions.indexOf(labelNode)).clonedLabels.get(labelNode);
}
/**
* Returns the clone of the given original label that is appropriate for use by a try/catch
* block or a variable annotation.
*
* @param labelNode a label of the original code.
* @return a clone of the given label for use by a try/catch block or a variable annotation in
* the inlined code.
*/
LabelNode getClonedLabel(final LabelNode labelNode) {
return clonedLabels.get(labelNode);
}
// AbstractMap implementation
@Override
public Set> entrySet() {
throw new UnsupportedOperationException();
}
@Override
public LabelNode get(final Object key) {
return getClonedLabelForJumpInsn((LabelNode) key);
}
@Override
public boolean equals(final Object other) {
throw new UnsupportedOperationException();
}
@Override
public int hashCode() {
throw new UnsupportedOperationException();
}
}
}