/*
* Janino - An embedded Java[TM] compiler
*
* Copyright (c) 2001-2010, Arno Unkrig
* 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. The name of the author may not be used to endorse or promote products derived from this software without
* specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``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 AUTHOR 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.codehaus.janino;
import java.io.DataOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import org.codehaus.janino.util.ClassFile;
/**
* The context of the compilation of a function (constructor or method). Manages generation of
* byte code, the exception table, generation of line number tables, allocation of local variables,
* determining of stack size and local variable table size and flow analysis.
*/
public class CodeContext {
private static final boolean DEBUG = false;
private static final int INITIAL_SIZE = 128;
private static final byte UNEXAMINED = -1;
private static final byte INVALID_OFFSET = -2;
private static final int MAX_STACK_SIZE = 254;
private /*final*/ ClassFile classFile;
private short maxStack;
private short maxLocals;
private byte[] code;
private /*final*/ Offset beginning;
private /*final*/ Inserter end;
private Inserter currentInserter;
private /*final*/ List exceptionTableEntries; // ExceptionTableEntry
/**
* List of Java.LocalVariableSlot objects that contain all the local variables that
* are allocated in any block in this CodeContext.
*/
private List allLocalVars = new ArrayList();
/**
* List of List of Java.LocalVariableSlot objects. Each List of Java.LocalVariableSlot is
* the local variables allocated for a block. They are pushed and poped onto the list together
* to make allocation of the next local variable slot easy.
*/
private List scopedVars = new ArrayList();
private short nextLocalVariableSlot = 0;
private final List relocatables = new ArrayList();
/**
* Create an empty "Code" attribute.
*/
public CodeContext(ClassFile classFile) {
this.classFile = classFile;
this.maxStack = 0;
this.maxLocals = 0;
this.code = new byte[CodeContext.INITIAL_SIZE];
this.beginning = new Offset();
this.end = new Inserter();
this.currentInserter = this.end;
this.exceptionTableEntries = new ArrayList();
this.beginning.offset = 0;
this.end.offset = 0;
this.beginning.next = this.end;
this.end.prev = this.beginning;
}
public ClassFile getClassFile() {
return this.classFile;
}
/**
* Allocate space for a local variable of the given size (1 or 2)
* on the local variable array.
*
* As a side effect, the "max_locals" field of the "Code" attribute
* is updated.
*
* The only way to deallocate local variables is to
* {@link #saveLocalVariables()} and later {@link
* #restoreLocalVariables()}.
*
* @param size The number of slots to allocate (1 or 2)
* @return The slot index of the allocated variable
*/
public short allocateLocalVariable(
short size
) {
return allocateLocalVariable(size, null, null).getSlotIndex();
}
/**
* Allocate space for a local variable of the given size (1 or 2)
* on the local variable array.
*
* As a side effect, the "max_locals" field of the "Code" attribute
* is updated.
*
* The only way to deallocate local variables is to
* {@link #saveLocalVariables()} and later {@link
* #restoreLocalVariables()}.
* @param size Number of slots to use (1 or 2)
* @param name The variable name, if it's null, the variable won't be written to the localvariabletable
* @param type The variable type. if the name isn't null, the type is needed to write to the localvariabletable
*/
public Java.LocalVariableSlot allocateLocalVariable(
short size,
String name,
IClass type
) {
List currentVars = null;
if (this.scopedVars.size() == 0) {
throw new Error("saveLocalVariables must be called first");
} else {
currentVars = (List) this.scopedVars.get(this.scopedVars.size() - 1);
}
Java.LocalVariableSlot slot = new Java.LocalVariableSlot(name, this.nextLocalVariableSlot, type);
if (slot.getName() != null) {
slot.setStart(newOffset());
}
this.nextLocalVariableSlot += size;
currentVars.add(slot);
this.allLocalVars.add(slot);
if (this.nextLocalVariableSlot > this.maxLocals) {
this.maxLocals = this.nextLocalVariableSlot;
}
return slot;
}
/**
* Remember the current size of the local variables array.
*/
public List saveLocalVariables() {
//
// push empty list on the stack to hold a new block's local vars
//
List l = new ArrayList();
this.scopedVars.add(l);
return l;
}
/**
* Restore the previous size of the local variables array. This MUST to be called for every call
* to saveLocalVariables as it closes the variable extent for all the active local variables in
* the current block.
*/
public void restoreLocalVariables() {
//
// pop the list containing the current block's local vars
//
Iterator iter = ((List) this.scopedVars.remove(this.scopedVars.size() - 1)).iterator();
while (iter.hasNext()) {
Java.LocalVariableSlot slot = (Java.LocalVariableSlot) iter.next();
if (slot.getName() != null) {
slot.setEnd(newOffset());
}
}
}
/**
*
* @param dos
* @param lineNumberTableAttributeNameIndex 0 == don't generate a "LineNumberTable" attribute
* @throws IOException
*/
protected void storeCodeAttributeBody(
DataOutputStream dos,
short lineNumberTableAttributeNameIndex,
short localVariableTableAttributeNameIndex
) throws IOException {
dos.writeShort(this.maxStack); // max_stack
dos.writeShort(this.maxLocals); // max_locals
dos.writeInt(this.end.offset); // code_length
dos.write(this.code, 0, this.end.offset); // code
dos.writeShort(this.exceptionTableEntries.size()); // exception_table_length
for (int i = 0; i < this.exceptionTableEntries.size(); ++i) { // exception_table
ExceptionTableEntry exceptionTableEntry = (ExceptionTableEntry) this.exceptionTableEntries.get(i);
dos.writeShort(exceptionTableEntry.startPC.offset);
dos.writeShort(exceptionTableEntry.endPC.offset);
dos.writeShort(exceptionTableEntry.handlerPC.offset);
dos.writeShort(exceptionTableEntry.catchType);
}
List attributes = new ArrayList(); // ClassFile.AttributeInfo
// Add "LineNumberTable" attribute.
if (lineNumberTableAttributeNameIndex != 0) {
List lnt = new ArrayList();
for (Offset o = this.beginning; o != null; o = o.next) {
if (o instanceof LineNumberOffset) {
lnt.add(new ClassFile.LineNumberTableAttribute.Entry(o.offset, ((LineNumberOffset) o).lineNumber));
}
}
ClassFile.LineNumberTableAttribute.Entry[] lnte = (ClassFile.LineNumberTableAttribute.Entry[]) lnt.toArray(
new ClassFile.LineNumberTableAttribute.Entry[lnt.size()]
);
attributes.add(new ClassFile.LineNumberTableAttribute(
lineNumberTableAttributeNameIndex, // attributeNameIndex
lnte // lineNumberTableEntries
));
}
if (localVariableTableAttributeNameIndex != 0) {
ClassFile.AttributeInfo ai = storeLocalVariableTable(dos, localVariableTableAttributeNameIndex);
if (ai != null) attributes.add(ai);
}
dos.writeShort(attributes.size()); // attributes_count
for (Iterator it = attributes.iterator(); it.hasNext();) { // attributes;
ClassFile.AttributeInfo attribute = (ClassFile.AttributeInfo) it.next();
attribute.store(dos);
}
}
protected ClassFile.AttributeInfo storeLocalVariableTable(
DataOutputStream dos,
short localVariableTableAttributeNameIndex
) {
ClassFile cf = getClassFile();
Iterator iter = getAllLocalVars().iterator();
final List entryList = new ArrayList();
while (iter.hasNext()) {
Java.LocalVariableSlot slot = (Java.LocalVariableSlot) iter.next();
if (slot.getName() != null) {
String typeName = slot.getType().getDescriptor();
short classSlot = cf.addConstantUtf8Info(typeName);
short varNameSlot = cf.addConstantUtf8Info(slot.getName());
// System.out.println("slot: " + slot + ", typeSlot: " + classSlot + ", varSlot: " + varNameSlot);
ClassFile.LocalVariableTableAttribute.Entry entry = new ClassFile.LocalVariableTableAttribute.Entry(
(short) slot.getStart().offset,
(short) (slot.getEnd().offset - slot.getStart().offset),
varNameSlot,
classSlot,
slot.getSlotIndex()
);
entryList.add(entry);
}
}
if (entryList.size() > 0) {
Object entries = entryList.toArray(new ClassFile.LocalVariableTableAttribute.Entry [entryList.size()]);
return new ClassFile.LocalVariableTableAttribute(
localVariableTableAttributeNameIndex,
(ClassFile.LocalVariableTableAttribute.Entry []) entries
);
} else {
return null;
}
}
/**
* Checks the code for consistency; updates the "maxStack" member.
*
* Notice: On inconsistencies, a "RuntimeException" is thrown (KLUDGE).
*/
public void flowAnalysis(String functionName) {
if (CodeContext.DEBUG) {
System.err.println("flowAnalysis(" + functionName + ")");
}
short[] stackSizes = new short[this.end.offset];
Arrays.fill(stackSizes, CodeContext.UNEXAMINED);
// Analyze flow from offset zero.
this.flowAnalysis(
functionName,
this.code, // code
this.end.offset, // codeSize
0, // offset
(short) 0, // stackSize
stackSizes // stackSizes
);
// Analyze flow from exception handler entry points.
int analyzedExceptionHandlers = 0;
while (analyzedExceptionHandlers != this.exceptionTableEntries.size()) {
for (int i = 0; i < this.exceptionTableEntries.size(); ++i) {
ExceptionTableEntry exceptionTableEntry = (ExceptionTableEntry) this.exceptionTableEntries.get(i);
if (stackSizes[exceptionTableEntry.startPC.offset] != CodeContext.UNEXAMINED) {
this.flowAnalysis(
functionName,
this.code, // code
this.end.offset, // codeSize
exceptionTableEntry.handlerPC.offset, // offset
(short) (stackSizes[exceptionTableEntry.startPC.offset] + 1), // stackSize
stackSizes // stackSizes
);
++analyzedExceptionHandlers;
}
}
}
// Check results and determine maximum stack size.
this.maxStack = 0;
for (int i = 0; i < stackSizes.length; ++i) {
short ss = stackSizes[i];
if (ss == CodeContext.UNEXAMINED) {
if (CodeContext.DEBUG) {
System.out.println(functionName + ": Unexamined code at offset " + i);
return;
} else {
throw new JaninoRuntimeException(functionName + ": Unexamined code at offset " + i);
}
}
if (ss > this.maxStack) this.maxStack = ss;
}
}
private void flowAnalysis(
String functionName,
byte[] code, // Bytecode
int codeSize, // Size
int offset, // Current PC
short stackSize, // Stack size on entry
short[] stackSizes // Stack sizes in code
) {
for (;;) {
if (CodeContext.DEBUG) System.out.println("Offset = " + offset + ", stack size = " + stackSize);
// Check current bytecode offset.
if (offset < 0 || offset >= codeSize) {
throw new JaninoRuntimeException(functionName + ": Offset out of range");
}
// Have we hit an area that has already been analyzed?
int css = stackSizes[offset];
if (css == stackSize) return; // OK.
if (css == CodeContext.INVALID_OFFSET) throw new JaninoRuntimeException(functionName + ": Invalid offset");
if (css != CodeContext.UNEXAMINED) {
if (CodeContext.DEBUG) {
System.err.println(
functionName
+ ": Operand stack inconsistent at offset "
+ offset
+ ": Previous size "
+ css
+ ", now "
+ stackSize
);
return;
} else {
throw new JaninoRuntimeException(
functionName
+ ": Operand stack inconsistent at offset "
+ offset
+ ": Previous size "
+ css
+ ", now "
+ stackSize
);
}
}
stackSizes[offset] = stackSize;
// Analyze current opcode.
byte opcode = code[offset];
int operandOffset = offset + 1;
short props;
if (opcode == Opcode.WIDE) {
opcode = code[operandOffset++];
props = Opcode.WIDE_OPCODE_PROPERTIES[0xff & opcode];
} else {
props = Opcode.OPCODE_PROPERTIES[0xff & opcode];
}
if (props == Opcode.INVALID_OPCODE) {
throw new JaninoRuntimeException(
functionName
+ ": Invalid opcode "
+ (0xff & opcode)
+ " at offset "
+ offset
);
}
switch (props & Opcode.SD_MASK) {
case Opcode.SD_M4:
case Opcode.SD_M3:
case Opcode.SD_M2:
case Opcode.SD_M1:
case Opcode.SD_P0:
case Opcode.SD_P1:
case Opcode.SD_P2:
stackSize += (props & Opcode.SD_MASK) - Opcode.SD_P0;
break;
case Opcode.SD_0:
stackSize = 0;
break;
case Opcode.SD_GETFIELD:
--stackSize;
/* FALL THROUGH */
case Opcode.SD_GETSTATIC:
stackSize += this.determineFieldSize((short) (
extract16BitValue(0, operandOffset, code)
));
break;
case Opcode.SD_PUTFIELD:
--stackSize;
/* FALL THROUGH */
case Opcode.SD_PUTSTATIC:
stackSize -= this.determineFieldSize((short) (
extract16BitValue(0, operandOffset, code)
));
break;
case Opcode.SD_INVOKEVIRTUAL:
case Opcode.SD_INVOKESPECIAL:
case Opcode.SD_INVOKEINTERFACE:
--stackSize;
/* FALL THROUGH */
case Opcode.SD_INVOKESTATIC:
stackSize -= this.determineArgumentsSize((short) (
extract16BitValue(0, operandOffset, code)
));
break;
case Opcode.SD_MULTIANEWARRAY:
stackSize -= code[operandOffset + 2] - 1;
break;
default:
throw new JaninoRuntimeException(functionName + ": Invalid stack delta");
}
if (stackSize < 0) {
String msg = (
this.classFile.getThisClassName()
+ '.'
+ functionName
+ ": Operand stack underrun at offset "
+ offset
);
if (CodeContext.DEBUG) {
System.err.println(msg);
return;
} else {
throw new JaninoRuntimeException(msg);
}
}
if (stackSize > MAX_STACK_SIZE) {
String msg = (
this.classFile.getThisClassName()
+ '.'
+ functionName
+ ": Operand stack overflow at offset "
+ offset
);
if (CodeContext.DEBUG) {
System.err.println(msg);
return;
} else {
throw new JaninoRuntimeException(msg);
}
}
switch (props & Opcode.OP1_MASK) {
case 0:
;
break;
case Opcode.OP1_SB:
case Opcode.OP1_UB:
case Opcode.OP1_CP1:
case Opcode.OP1_LV1:
++operandOffset;
break;
case Opcode.OP1_SS:
case Opcode.OP1_CP2:
case Opcode.OP1_LV2:
operandOffset += 2;
break;
case Opcode.OP1_BO2:
if (CodeContext.DEBUG) {
System.out.println("Offset = " + offset);
System.out.println("Operand offset = " + operandOffset);
System.out.println(code[operandOffset]);
System.out.println(code[operandOffset + 1]);
}
this.flowAnalysis(
functionName,
code, codeSize,
extract16BitValue(offset, operandOffset, code),
stackSize,
stackSizes
);
operandOffset += 2;
break;
case Opcode.OP1_JSR:
if (CodeContext.DEBUG) {
System.out.println("Offset = " + offset);
System.out.println("Operand offset = " + operandOffset);
System.out.println(code[operandOffset]);
System.out.println(code[operandOffset + 1]);
}
int targetOffset = extract16BitValue(offset, operandOffset, code);
operandOffset += 2;
if (stackSizes[targetOffset] == CodeContext.UNEXAMINED) {
this.flowAnalysis(
functionName,
code, codeSize,
targetOffset,
(short) (stackSize + 1),
stackSizes
);
}
break;
case Opcode.OP1_BO4:
this.flowAnalysis(
functionName,
code, codeSize,
extract32BitValue(offset, operandOffset, code),
stackSize, stackSizes
);
operandOffset += 4;
break;
case Opcode.OP1_LOOKUPSWITCH:
while ((operandOffset & 3) != 0) ++operandOffset;
this.flowAnalysis(
functionName,
code, codeSize,
extract32BitValue(offset, operandOffset, code),
stackSize, stackSizes
);
operandOffset += 4;
int npairs = extract32BitValue(0, operandOffset, code);
operandOffset += 4;
for (int i = 0; i < npairs; ++i) {
operandOffset += 4; //skip match value
this.flowAnalysis(
functionName,
code, codeSize,
extract32BitValue(offset, operandOffset, code),
stackSize, stackSizes
);
operandOffset += 4; //advance over offset
}
break;
case Opcode.OP1_TABLESWITCH:
while ((operandOffset & 3) != 0) ++operandOffset;
this.flowAnalysis(
functionName,
code, codeSize,
extract32BitValue(offset, operandOffset, code),
stackSize, stackSizes
);
operandOffset += 4;
int low = extract32BitValue(offset, operandOffset, code);
operandOffset += 4;
int hi = extract32BitValue(offset, operandOffset, code);
operandOffset += 4;
for (int i = low; i <= hi; ++i) {
this.flowAnalysis(
functionName,
code, codeSize,
extract32BitValue(offset, operandOffset, code),
stackSize, stackSizes
);
operandOffset += 4;
}
break;
default:
throw new JaninoRuntimeException(functionName + ": Invalid OP1");
}
switch (props & Opcode.OP2_MASK) {
case 0:
;
break;
case Opcode.OP2_SB:
++operandOffset;
break;
case Opcode.OP2_SS:
operandOffset += 2;
break;
default:
throw new JaninoRuntimeException(functionName + ": Invalid OP2");
}
switch (props & Opcode.OP3_MASK) {
case 0:
;
break;
case Opcode.OP3_SB:
++operandOffset;
break;
default:
throw new JaninoRuntimeException(functionName + ": Invalid OP3");
}
Arrays.fill(stackSizes, offset + 1, operandOffset, CodeContext.INVALID_OFFSET);
if ((props & Opcode.NO_FALLTHROUGH) != 0) return;
offset = operandOffset;
}
}
/**
* Extract a 16 bit value at offset in code and add bias to it
*
* @param bias An int to skew the final result by (useful for calculating relative offsets)
* @param offset The position in the code array to extract the bytes from
* @param code The array of bytes
* @return An integer that treats the two bytes at position offset as an UNSIGNED SHORT
*/
private int extract16BitValue(int bias, int offset, byte[] code) {
int res = bias + (
((code[offset]) << 8)
+ (code[offset + 1] & 0xff)
);
if (CodeContext.DEBUG) {
System.out.println("extract16BitValue(bias, offset) = (" + bias + ", " + offset + ")");
System.out.println("bytes = {" + code[offset] + ", " + code[offset + 1] + "}");
System.out.println("result = " + res);
}
return res;
}
/**
* Extract a 32 bit value at offset in code and add bias to it
*
* @param bias An int to skew the final result by (useful for calculating relative offsets)
* @param offset The position in the code array to extract the bytes from
* @param code The array of bytes
* @return The 4 bytes at position offset + bias
*/
private int extract32BitValue(int bias, int offset, byte[] code) {
int res = bias + (
(code[offset] << 24) +
((0xff & code[offset + 1]) << 16) +
((0xff & code[offset + 2]) << 8) +
(0xff & code[offset + 3])
);
if (CodeContext.DEBUG) {
System.out.println("extract32BitValue(bias, offset) = (" + bias + ", " + offset + ")");
System.out.println(
"bytes = {" +
code[offset] + ", " +
code[offset + 1] + ", " +
code[offset + 2] + ", " +
code[offset + 3] + "}"
);
System.out.println("result = " + res);
}
return res;
}
/**
* fixUp() all of the offsets and relocate() all relocatables
*/
public void fixUpAndRelocate() {
// We do this in a loop to allow relocatables to adjust the size
// of things in the byte stream. It is extremely unlikely, but possible
// that a late relocatable will grow the size of the bytecode, and require
// an earlier relocatable to switch from 32K mode to 64K mode branching
do {
fixUp();
} while (!relocate());
}
/**
* Fix up all offsets.
*/
private void fixUp() {
for (Offset o = this.beginning; o != this.end; o = o.next) {
if (o instanceof FixUp) ((FixUp) o).fixUp();
}
}
/**
* Relocate all relocatables and aggregate their response into a single one
* @return true if all of them relocated successfully
* false if any of them needed to change size
*/
private boolean relocate() {
boolean finished = true;
for (int i = 0; i < this.relocatables.size(); ++i) {
//do not terminate earlier so that everything gets a chance to grow in the first pass
//changes the common case for this to be O(n) instead of O(n**2)
boolean part = ((Relocatable) this.relocatables.get(i)).relocate();
finished = finished && part;
}
return finished;
}
/**
* Analyse the descriptor of the Fieldref and return its size.
*/
private int determineFieldSize(short idx) {
ClassFile.ConstantFieldrefInfo cfi = (
(ClassFile.ConstantFieldrefInfo) this.classFile.getConstantPoolInfo(idx)
);
ClassFile.ConstantNameAndTypeInfo cnati = (
(ClassFile.ConstantNameAndTypeInfo) this.classFile.getConstantPoolInfo(cfi.getNameAndTypeIndex())
);
ClassFile.ConstantUtf8Info cui = (
(ClassFile.ConstantUtf8Info) this.classFile.getConstantPoolInfo(cnati.getDescriptorIndex())
);
return Descriptor.size(cui.getString());
}
/**
* Analyse the descriptor of the Methodref and return the sum of the
* arguments' sizes minus the return value's size.
*/
private int determineArgumentsSize(short idx) {
ClassFile.ConstantPoolInfo cpi = this.classFile.getConstantPoolInfo(idx);
ClassFile.ConstantNameAndTypeInfo nat = (ClassFile.ConstantNameAndTypeInfo) this.classFile.getConstantPoolInfo(
cpi instanceof ClassFile.ConstantInterfaceMethodrefInfo ?
((ClassFile.ConstantInterfaceMethodrefInfo) cpi).getNameAndTypeIndex() :
((ClassFile.ConstantMethodrefInfo) cpi).getNameAndTypeIndex()
);
ClassFile.ConstantUtf8Info cui = (
(ClassFile.ConstantUtf8Info) this.classFile.getConstantPoolInfo(nat.getDescriptorIndex())
);
String desc = cui.getString();
if (desc.charAt(0) != '(') throw new JaninoRuntimeException("Method descriptor does not start with \"(\"");
int i = 1;
int res = 0;
for (;;) {
switch (desc.charAt(i++)) {
case ')':
return res - Descriptor.size(desc.substring(i));
case 'B': case 'C': case 'F': case 'I': case 'S': case 'Z':
res += 1;
break;
case 'D': case 'J':
res += 2;
break;
case '[':
res += 1;
while (desc.charAt(i) == '[') ++i;
if ("BCFISZDJ".indexOf(desc.charAt(i)) != -1) { ++i; break; }
if (desc.charAt(i) != 'L') throw new JaninoRuntimeException("Invalid char after \"[\"");
++i;
while (desc.charAt(i++) != ';');
break;
case 'L':
res += 1;
while (desc.charAt(i++) != ';');
break;
default:
throw new JaninoRuntimeException("Invalid method descriptor");
}
}
}
/**
* Inserts a sequence of bytes at the current insertion position. Creates
* {@link LineNumberOffset}s as necessary.
*
* @param lineNumber The line number that corresponds to the byte code, or -1
* @param b
*/
public void write(short lineNumber, byte[] b) {
if (b.length == 0) return;
int ico = this.currentInserter.offset;
this.makeSpace(lineNumber, b.length);
System.arraycopy(b, 0, this.code, ico, b.length);
}
/**
* Inserts a byte at the current insertion position. Creates
* {@link LineNumberOffset}s as necessary.
* <p>
* This method is an optimization to avoid allocating small byte[] and ease
* GC load.
*
* @param lineNumber The line number that corresponds to the byte code, or -1
* @param b1
*/
public void write(short lineNumber, byte b1) {
int ico = this.currentInserter.offset;
this.makeSpace(lineNumber, 1);
this.code[ico] = b1;
}
/**
* Inserts bytes at the current insertion position. Creates
* {@link LineNumberOffset}s as necessary.
* <p>
* This method is an optimization to avoid allocating small byte[] and ease
* GC load.
*
* @param lineNumber The line number that corresponds to the byte code, or -1
* @param b1
* @param b2
*/
public void write(short lineNumber, byte b1, byte b2) {
int ico = this.currentInserter.offset;
this.makeSpace(lineNumber, 2);
this.code[ico++] = b1;
this.code[ico ] = b2;
}
/**
* Inserts bytes at the current insertion position. Creates
* {@link LineNumberOffset}s as necessary.
* <p>
* This method is an optimization to avoid allocating small byte[] and ease
* GC load.
*
* @param lineNumber The line number that corresponds to the byte code, or -1
* @param b1
* @param b2
* @param b3
*/
public void write(short lineNumber, byte b1, byte b2, byte b3) {
int ico = this.currentInserter.offset;
this.makeSpace(lineNumber, 3);
this.code[ico++] = b1;
this.code[ico++] = b2;
this.code[ico ] = b3;
}
/**
* Inserts bytes at the current insertion position. Creates
* {@link LineNumberOffset}s as necessary.
* <p>
* This method is an optimization to avoid allocating small byte[] and ease
* GC load.
*
* @param lineNumber The line number that corresponds to the byte code, or -1
* @param b1
* @param b2
* @param b3
* @param b4
*/
public void write(short lineNumber, byte b1, byte b2, byte b3, byte b4) {
int ico = this.currentInserter.offset;
this.makeSpace(lineNumber, 4);
this.code[ico++] = b1;
this.code[ico++] = b2;
this.code[ico++] = b3;
this.code[ico ] = b4;
}
/**
* Add space for size bytes at current offset. Creates
* {@link LineNumberOffset}s as necessary.
*
* @param lineNumber The line number that corresponds to the byte code, or -1
* @param size The size in bytes to inject
*/
public void makeSpace(short lineNumber, int size) {
if (size == 0) return;
INSERT_LINE_NUMBER_OFFSET:
if (lineNumber != -1) {
Offset o;
for (o = this.currentInserter.prev; o != this.beginning; o = o.prev) {
if (o instanceof LineNumberOffset) {
if (((LineNumberOffset) o).lineNumber == lineNumber) break INSERT_LINE_NUMBER_OFFSET;
break;
}
}
LineNumberOffset lno = new LineNumberOffset(this.currentInserter.offset, lineNumber);
lno.prev = this.currentInserter.prev;
lno.next = this.currentInserter;
this.currentInserter.prev.next = lno;
this.currentInserter.prev = lno;
}
int ico = this.currentInserter.offset;
if (this.end.offset + size <= this.code.length) {
// Optimization to avoid a trivial method call in the common case
if (ico != this.end.offset) {
System.arraycopy(this.code, ico, this.code, ico + size, this.end.offset - ico);
}
} else {
byte[] oldCode = this.code;
//double size to avoid horrible performance, but don't grow over our limit
int newSize = Math.max(Math.min(oldCode.length * 2, 0xffff), oldCode.length + size);
if (newSize > 0xffff) {
throw new JaninoRuntimeException(
"Code attribute in class \""
+ this.classFile.getThisClassName()
+ "\" grows beyond 64 KB"
);
}
this.code = new byte[newSize];
System.arraycopy(oldCode, 0, this.code, 0, ico);
System.arraycopy(oldCode, ico, this.code, ico + size, this.end.offset - ico);
}
Arrays.fill(this.code, ico, ico + size, (byte) 0);
for (Offset o = this.currentInserter; o != null; o = o.next) o.offset += size;
}
/**
* @param lineNumber The line number that corresponds to the byte code, or -1
*/
public void writeShort(short lineNumber, int v) {
this.write(lineNumber, (byte) (v >> 8), (byte) v);
}
/**
* @param lineNumber The line number that corresponds to the byte code, or -1
*/
public void writeBranch(short lineNumber, int opcode, final Offset dst) {
this.relocatables.add(new Branch(opcode, dst));
this.write(lineNumber, (byte) opcode, (byte) -1, (byte) -1);
}
private class Branch extends Relocatable {
public Branch(int opcode, Offset destination) {
this.opcode = opcode;
this.source = CodeContext.this.newInserter();
this.destination = destination;
if (opcode == Opcode.JSR_W || opcode == Opcode.GOTO_W) {
//no need to expand wide opcodes
this.expanded = true;
} else {
this.expanded = false;
}
}
public boolean relocate() {
if (this.destination.offset == Offset.UNSET) {
throw new JaninoRuntimeException("Cannot relocate branch to unset destination offset");
}
int offset = this.destination.offset - this.source.offset;
if (!this.expanded && (offset > Short.MAX_VALUE || offset < Short.MIN_VALUE)) {
//we want to insert the data without skewing our source position,
//so we will cache it and then restore it later.
int pos = this.source.offset;
CodeContext.this.pushInserter(this.source); {
// promotion to a wide instruction only requires 2 extra bytes
// everything else requires a new GOTO_W instruction after a negated if
CodeContext.this.makeSpace(
(short) -1,
this.opcode == Opcode.GOTO ? 2 : this.opcode == Opcode.JSR ? 2 : 5
);
} CodeContext.this.popInserter();
this.source.offset = pos;
this.expanded = true;
return false;
}
final byte[] ba;
if (!this.expanded) {
//we fit in a 16-bit jump
ba = new byte[] { (byte) this.opcode, (byte) (offset >> 8), (byte) offset };
} else {
if (this.opcode == Opcode.GOTO || this.opcode == Opcode.JSR) {
ba = new byte[] {
(byte) (this.opcode + 33), // GOTO => GOTO_W; JSR => JSR_W
(byte) (offset >> 24),
(byte) (offset >> 16),
(byte) (offset >> 8),
(byte) offset
};
} else
{
//exclude the if-statement from jump target
//if jumping backwards this will increase the jump to go over it
//if jumping forwards this will decrease the jump by it
offset -= 3;
// [if cond offset]
//expands to
// [if !cond skip_goto]
// [GOTO_W offset]
ba = new byte[] {
CodeContext.invertBranchOpcode((byte) this.opcode),
(byte) 0,
(byte) 8, //jump from this instruction past the GOTO_W
(byte) Opcode.GOTO_W,
(byte) (offset >> 24),
(byte) (offset >> 16),
(byte) (offset >> 8),
(byte) offset
};
}
}
System.arraycopy(ba, 0, CodeContext.this.code, this.source.offset, ba.length);
return true;
}
private boolean expanded; //marks whether this has been expanded to account for a wide branch
private final int opcode;
private final Inserter source;
private final Offset destination;
}
/**
* E.g. {@link Opcode#IFLT} ("less than") inverts to {@link Opcode#IFGE} ("greater than or equal to").
*/
private static byte invertBranchOpcode(byte branchOpcode) {
return ((Byte) CodeContext.BRANCH_OPCODE_INVERSION.get(new Byte(branchOpcode))).byteValue();
}
/** Byte branch-opcode => Byte inverted-branch-opcode */
private static final Map BRANCH_OPCODE_INVERSION = CodeContext.createBranchOpcodeInversion();
private static Map createBranchOpcodeInversion() {
Map m = new HashMap();
m.put(new Byte(Opcode.IF_ACMPEQ), new Byte(Opcode.IF_ACMPNE));
m.put(new Byte(Opcode.IF_ACMPNE), new Byte(Opcode.IF_ACMPEQ));
m.put(new Byte(Opcode.IF_ICMPEQ), new Byte(Opcode.IF_ICMPNE));
m.put(new Byte(Opcode.IF_ICMPNE), new Byte(Opcode.IF_ICMPEQ));
m.put(new Byte(Opcode.IF_ICMPGE), new Byte(Opcode.IF_ICMPLT));
m.put(new Byte(Opcode.IF_ICMPLT), new Byte(Opcode.IF_ICMPGE));
m.put(new Byte(Opcode.IF_ICMPGT), new Byte(Opcode.IF_ICMPLE));
m.put(new Byte(Opcode.IF_ICMPLE), new Byte(Opcode.IF_ICMPGT));
m.put(new Byte(Opcode.IFEQ), new Byte(Opcode.IFNE));
m.put(new Byte(Opcode.IFNE), new Byte(Opcode.IFEQ));
m.put(new Byte(Opcode.IFGE), new Byte(Opcode.IFLT));
m.put(new Byte(Opcode.IFLT), new Byte(Opcode.IFGE));
m.put(new Byte(Opcode.IFGT), new Byte(Opcode.IFLE));
m.put(new Byte(Opcode.IFLE), new Byte(Opcode.IFGT));
m.put(new Byte(Opcode.IFNULL), new Byte(Opcode.IFNONNULL));
m.put(new Byte(Opcode.IFNONNULL), new Byte(Opcode.IFNULL));
return Collections.unmodifiableMap(m);
}
public void writeOffset(short lineNumber, Offset src, final Offset dst) {
this.relocatables.add(new OffsetBranch(this.newOffset(), src, dst));
this.write(lineNumber, (byte) -1, (byte) -1, (byte) -1, (byte) -1);
}
private class OffsetBranch extends Relocatable {
public OffsetBranch(Offset where, Offset source, Offset destination) {
this.where = where;
this.source = source;
this.destination = destination;
}
public boolean relocate() {
if (
this.source.offset == Offset.UNSET ||
this.destination.offset == Offset.UNSET
) throw new JaninoRuntimeException("Cannot relocate offset branch to unset destination offset");
int offset = this.destination.offset - this.source.offset;
byte[] ba = new byte[] {
(byte) (offset >> 24),
(byte) (offset >> 16),
(byte) (offset >> 8),
(byte) offset
};
System.arraycopy(ba, 0, CodeContext.this.code, this.where.offset, 4);
return true;
}
private final Offset where, source, destination;
}
public Offset newOffset() {
Offset o = new Offset();
o.set();
return o;
}
/**
* Allocate an {@link Inserter}, set it to the current offset, and
* insert it before the current offset.
*
* In clear text, this means that you can continue writing to the
* "Code" attribute, then {@link #pushInserter(CodeContext.Inserter)} the
* {@link Inserter}, then write again (which inserts bytes into the
* "Code" attribute at the previously remembered position), and then
* {@link #popInserter()}.
*/
public Inserter newInserter() {
Inserter i = new Inserter();
i.set();
return i;
}
public Inserter currentInserter() {
return this.currentInserter;
}
/**
* Remember the current {@link Inserter}, then replace it with the
* new one.
*/
public void pushInserter(Inserter ins) {
if (ins.nextInserter != null) throw new JaninoRuntimeException("An Inserter can only be pushed once at a time");
ins.nextInserter = this.currentInserter;
this.currentInserter = ins;
}
/**
* Replace the current {@link Inserter} with the remembered one (see
* {@link #pushInserter(CodeContext.Inserter)}).
*/
public void popInserter() {
Inserter ni = this.currentInserter.nextInserter;
if (ni == null) throw new JaninoRuntimeException("Code inserter stack underflow");
this.currentInserter.nextInserter = null; // Mark it as "unpushed".
this.currentInserter = ni;
}
/**
* A class that represents an offset within a "Code" attribute.
*
* The concept of an "offset" is that if one writes into the middle of
* a "Code" attribute, all offsets behind the insertion point are
* automatically shifted.
*/
public class Offset {
int offset = Offset.UNSET;
Offset prev = null, next = null;
static final int UNSET = -1;
/**
* Set this "Offset" to the offset of the current inserter; insert
* this "Offset" before the current inserter.
*/
public void set() {
if (this.offset != Offset.UNSET) throw new JaninoRuntimeException("Cannot \"set()\" Offset more than once");
this.offset = CodeContext.this.currentInserter.offset;
this.prev = CodeContext.this.currentInserter.prev;
this.next = CodeContext.this.currentInserter;
this.prev.next = this;
this.next.prev = this;
}
public final CodeContext getCodeContext() { return CodeContext.this; }
public String toString() {
return CodeContext.this.classFile.getThisClassName() + ": " + this.offset;
}
}
/**
* Add another entry to the "exception_table" of this code attribute (see JVMS 4.7.3).
* @param startPC
* @param endPC
* @param handlerPC
* @param catchTypeFD
*/
public void addExceptionTableEntry(
Offset startPC,
Offset endPC,
Offset handlerPC,
String catchTypeFD // null == "finally" clause
) {
this.exceptionTableEntries.add(new ExceptionTableEntry(
startPC,
endPC,
handlerPC,
catchTypeFD == null ? (short) 0 : this.classFile.addConstantClassInfo(catchTypeFD)
));
}
/**
* Representation of an entry in the "exception_table" of a "Code" attribute (see JVMS
* 4.7.3).
*/
private static class ExceptionTableEntry {
ExceptionTableEntry(
Offset startPC,
Offset endPC,
Offset handlerPC,
short catchType
) {
this.startPC = startPC;
this.endPC = endPC;
this.handlerPC = handlerPC;
this.catchType = catchType;
}
final Offset startPC, endPC, handlerPC;
final short catchType; // 0 == "finally" clause
}
/**
* A class that implements an insertion point into a "Code"
* attribute.
*/
public class Inserter extends Offset {
private Inserter nextInserter = null; // null == not in "currentInserter" stack
}
public class LineNumberOffset extends Offset {
private final int lineNumber;
public LineNumberOffset(int offset, int lineNumber) {
this.lineNumber = lineNumber;
this.offset = offset;
}
}
private abstract class Relocatable {
/**
* Relocate this object.
* @return true if the relocation succeeded in place
* false if the relocation grew the number of bytes required
*/
public abstract boolean relocate();
}
/**
* A throw-in interface that marks {@link CodeContext.Offset}s
* as "fix-ups": During the execution of
* {@link CodeContext#fixUp}, all "fix-ups" are invoked and
* can do last touches to the code attribute.
* <p>
* This is currently used for inserting the "padding bytes" into the
* TABLESWITCH and LOOKUPSWITCH instructions.
*/
public interface FixUp {
void fixUp();
}
public List getAllLocalVars() { return this.allLocalVars; }
}