/* * 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.HashSet; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; 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. */ @SuppressWarnings({ "rawtypes", "unchecked" }) 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 final String functionName; private short maxStack; private short maxLocals; private byte[] code; private final Offset beginning; private final Inserter end; private Inserter currentInserter; private final List exceptionTableEntries; /** All the local variables that are allocated in any block in this {@link CodeContext}. */ private final 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 final List> scopedVars = new ArrayList(); private short nextLocalVariableSlot; private final List relocatables = new ArrayList(); /** Creates an empty "Code" attribute. */ public CodeContext(ClassFile classFile, String functionName) { this.classFile = classFile; this.functionName = functionName; 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; } /** The {@link ClassFile} this context is related to. */ 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 this.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(this.newOffset()); } this.nextLocalVariableSlot += size; currentVars.add(slot); this.allLocalVars.add(slot); if (this.nextLocalVariableSlot > this.maxLocals) { this.maxLocals = this.nextLocalVariableSlot; } return slot; } /** Remembers 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. List slots = ( (List) this.scopedVars.remove(this.scopedVars.size() - 1) ); for (Java.LocalVariableSlot slot : slots) { if (slot.getName() != null) { slot.setEnd(this.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 (ExceptionTableEntry exceptionTableEntry : this.exceptionTableEntries) { // exception_table dos.writeShort(exceptionTableEntry.startPC.offset); dos.writeShort(exceptionTableEntry.endPC.offset); dos.writeShort(exceptionTableEntry.handlerPC.offset); dos.writeShort(exceptionTableEntry.catchType); } List attributes = new ArrayList(); // 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 )); } // Add "LocalVariableTable" attribute. if (localVariableTableAttributeNameIndex != 0) { ClassFile.AttributeInfo ai = this.storeLocalVariableTable(dos, localVariableTableAttributeNameIndex); if (ai != null) attributes.add(ai); } dos.writeShort(attributes.size()); // attributes_count for (ClassFile.AttributeInfo attribute : attributes) { // attributes; attribute.store(dos); } } /** * @return A {@link org.codehaus.janino.util.ClassFile.LocalVariableTableAttribute} for this {@link CodeContext} */ protected ClassFile.AttributeInfo storeLocalVariableTable(DataOutputStream dos, short localVariableTableAttributeNameIndex) { ClassFile cf = this.getClassFile(); final List entryList = new ArrayList(); for (Java.LocalVariableSlot slot : this.getAllLocalVars()) { 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 (ExceptionTableEntry exceptionTableEntry : this.exceptionTableEntries) { 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) ( CodeContext.extract16BitValue(0, operandOffset, code) )); break; case Opcode.SD_PUTFIELD: --stackSize; /* FALL THROUGH */ case Opcode.SD_PUTSTATIC: stackSize -= this.determineFieldSize((short) ( CodeContext.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) ( CodeContext.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 > CodeContext.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, CodeContext.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 = CodeContext.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, CodeContext.extract32BitValue(offset, operandOffset, code), stackSize, stackSizes ); operandOffset += 4; break; case Opcode.OP1_LOOKUPSWITCH: while ((operandOffset & 3) != 0) ++operandOffset; this.flowAnalysis( functionName, code, codeSize, CodeContext.extract32BitValue(offset, operandOffset, code), stackSize, stackSizes ); operandOffset += 4; int npairs = CodeContext.extract32BitValue(0, operandOffset, code); operandOffset += 4; for (int i = 0; i < npairs; ++i) { operandOffset += 4; //skip match value this.flowAnalysis( functionName, code, codeSize, CodeContext.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, CodeContext.extract32BitValue(offset, operandOffset, code), stackSize, stackSizes ); operandOffset += 4; int low = CodeContext.extract32BitValue(offset, operandOffset, code); operandOffset += 4; int hi = CodeContext.extract32BitValue(offset, operandOffset, code); operandOffset += 4; for (int i = low; i <= hi; ++i) { this.flowAnalysis( functionName, code, codeSize, CodeContext.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 static 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 static 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; } /** Fixes up 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 { this.fixUp(); } while (!this.relocate()); } /** Fixes 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 (Relocatable relocatable : this.relocatables) { // 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). finished &= relocatable.relocate(); } return finished; } /** Analyses the descriptor of the Fieldref and return its size. */ private int determineFieldSize(short idx) { ClassFile.ConstantFieldrefInfo cfi = ( (ClassFile.ConstantFieldrefInfo) this.classFile.getConstantPoolInfo(idx) ); return Descriptor.size(cfi.getNameAndType(this.classFile).getDescriptor(this.classFile)); } /** * 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 = ( cpi instanceof ClassFile.ConstantInterfaceMethodrefInfo ? ((ClassFile.ConstantInterfaceMethodrefInfo) cpi).getNameAndType(this.classFile) : ((ClassFile.ConstantMethodrefInfo) cpi).getNameAndType(this.classFile) ); String desc = nat.getDescriptor(this.classFile); 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. *

* 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. *

* 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. *

* 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. *

* 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 {@code 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 of method \"" + this.functionName + "\" of 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; } } @Override 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. final 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), 0, 8, // Jump from this instruction past the GOTO_W 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(); } 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); } /** Writes a four-byte offset (as it is used in TABLESWITCH and LOOKUPSWITCH) into this code context. */ 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; } @Override 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; } /** Creates and inserts an {@link CodeContext.Offset} at the current inserter's current position. */ 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; } /** @return The current inserter */ 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 { /** The offset in the code attribute that this object represents. */ int offset = Offset.UNSET; /** Links to preceding and succeding offsets. */ Offset prev, next; /** * Special value for {@link #offset} which indicates that this {@link Offset} has not yet been {@link #set()} */ static final int UNSET = -1; /** * Sets this "Offset" to the offset of the current inserter; inserts 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; } /** @return The {@link CodeContext} that this {@link Offset} belongs to */ public final CodeContext getCodeContext() { return CodeContext.this; } @Override 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 catchTypeFd null == "finally" clause */ public void addExceptionTableEntry(Offset startPc, Offset endPc, Offset handlerPc, String catchTypeFd) { 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 == not in "currentInserter" stack } /** An {@link Offset} who#s sole purpose is to later create a 'LneNumberTable' attribute. */ 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. *

* This is currently used for inserting the "padding bytes" into the * TABLESWITCH and LOOKUPSWITCH instructions. */ public interface FixUp { /** @see FixUp */ void fixUp(); } /** @return All the local variables that are allocated in any block in this {@link CodeContext} */ public List getAllLocalVars() { return this.allLocalVars; } /** * Removes all code between {@code from} and {@code to}. Also removes any {@link CodeContext.Relocatable}s existing * in that range. */ public void removeCode(Offset from, Offset to) { if (from == to) return; int size = to.offset - from.offset; assert size >= 0; if (size == 0) return; // Short circuit. // Shift down the bytecode past 'to'. System.arraycopy(this.code, to.offset, this.code, from.offset, this.end.offset - to.offset); // Invalidate all offsets between 'from' and 'to'. // Remove all relocatables that originate between 'from' and 'to'. Set invalidOffsets = new HashSet(); { Offset o = from.next; for (; o != to;) { if (o == null) { System.currentTimeMillis(); } invalidOffsets.add(o); // Invalidate the offset for fast failure. o.offset = -77; o.prev = null; o = o.next; o.prev.next = null; } for (;; o = o.next) { o.offset -= size; if (o == this.end) break; } } // Invalidate all relocatables which originate or target a removed offset. for (Iterator it = this.relocatables.iterator(); it.hasNext();) { Relocatable r = (Relocatable) it.next(); if (r instanceof Branch) { Branch b = (Branch) r; if (invalidOffsets.contains(b.source)) { it.remove(); } else { assert !invalidOffsets.contains(b.destination); } } if (r instanceof OffsetBranch) { OffsetBranch ob = (OffsetBranch) r; if (invalidOffsets.contains(ob.source)) { it.remove(); } else { assert !invalidOffsets.contains(ob.destination); } } } for (Iterator it = this.exceptionTableEntries.iterator(); it.hasNext();) { ExceptionTableEntry ete = (ExceptionTableEntry) it.next(); // Start, end and handler must either ALL lie IN the range to remove or ALL lie outside. if (invalidOffsets.contains(ete.startPC)) { assert invalidOffsets.contains(ete.endPC); assert invalidOffsets.contains(ete.handlerPC); it.remove(); } else { assert !invalidOffsets.contains(ete.endPC); assert !invalidOffsets.contains(ete.handlerPC); } } from.next = to; to.prev = from; } }