package com.rc.retroweaver; import com.rc.retroweaver.event.*; import jace.parser.*; import jace.parser.constant.*; import jace.parser.method.*; import jace.parser.field.*; import jace.parser.attribute.*; import org.apache.bcel.*; import org.apache.bcel.classfile.*; import org.apache.bcel.generic.*; import org.apache.bcel.util.*; import java.io.*; import java.util.*; /** * A bytecode enhancer that translates Java 1.5 class files into Java 1.4 class * files. The enhancer performs primarily two tasks: * * 1) Reverses changes made to the class file format in 1.5 to the * former 1.4 format. * * 2) Replaces compiler generated calls into the new 1.5 runtime with calls * into RetroWeaver's replacement runtime. * * @author Toby Reyelts * */ public class RetroWeaver { private ClassParser parser; private JavaClass javaClass; private ClassGen generator; private String className; private ConstantPoolGen constantPool; private String sourcePath; private String outputPath; private int newVersion; private boolean lazy; private WeaveListener listener = new WeaveListener() { public void weavingPath( String sourcePath ) { System.out.println( "[RetroWeaver] Weaving " + sourcePath ); } }; private static final String newLine = System.getProperty( "line.separator" ); private static Map<Type,String> boxTypes = new HashMap<Type,String>(); private static final String AUTOBOX_CLASS = "com.rc.retroweaver.runtime.Autobox"; private static final String VALUE_OF_METHOD = "valueOf"; private static final String CLASS_LITERAL_CLASS = "com.rc.retroweaver.runtime.ClassLiteral"; private static final String GET_CLASS_METHOD = "getClass"; private static final String GET_CLASS_SIGNATURE = "(Ljava/lang/String;)Ljava/lang/Class;"; static { boxTypes.put( Type.getType( Boolean.class ), "(Z)Ljava/lang/Boolean;" ); boxTypes.put( Type.getType( Byte.class ), "(B)Ljava/lang/Byte;" ); boxTypes.put( Type.getType( Character.class ), "(C)Ljava/lang/Character;" ); boxTypes.put( Type.getType( Short.class ), "(S)Ljava/lang/Short;" ); boxTypes.put( Type.getType( Integer.class ), "(I)Ljava/lang/Integer;" ); boxTypes.put( Type.getType( Long.class ), "(J)Ljava/lang/Long;" ); boxTypes.put( Type.getType( Float.class ), "(F)Ljava/lang/Float;" ); boxTypes.put( Type.getType( Double.class ), "(D)Ljava/lang/Double;" ); } public RetroWeaver( int version ) { newVersion = version; } public void weave( String sourcePath, String outputPath ) throws IOException { this.sourcePath = sourcePath; if ( outputPath == null ) { outputPath = sourcePath; } this.outputPath = outputPath; if ( ! fixupFormat() ) { // We're lazy and the class already has the target version. File sf = new File( sourcePath ); File of = new File( outputPath ); if ( ! of.isFile() || ! of.getCanonicalPath().equals( sf.getCanonicalPath() ) ) { // Target doesn't exist or is different from source so copy the file and transfer utime. FileInputStream fis = new FileInputStream( sf ); byte[] bytes = new byte[ ( int ) sf.length() ]; fis.read( bytes ); fis.close(); FileOutputStream fos = new FileOutputStream( of ); fos.write( bytes ); fos.close(); of.setLastModified( sf.lastModified() ); } return; } if ( listener != null ) { listener.weavingPath( sourcePath ); } // System.out.println( "BCEL Parsing file" ); // We read from the outputPath, because that is where the new fixed up class file lives parser = new ClassParser( this.outputPath ); javaClass = parser.parse(); generator = new ClassGen( javaClass ); className = generator.getClassName(); constantPool = generator.getConstantPool(); if ( alreadyAspected() ) { return; } // System.out.println( "Fixing up runtime" ); fixupRuntimeCalls(); // System.out.println( "Dumping class" ); generator.getJavaClass().dump( this.outputPath ); } /** * Replaces '+' characters with '$' characters in the identifier string. * (See http://forum.java.sun.com/thread.jsp?forum=316&thread=499645 for more info on "+"s * and identifiers). * * Replaces references to StringBuilder with StringBuffer - StringBuilder is 1.5 only. * I believe the compiler doesn't use StringBuilder in a way that is incompatible with * StringBuffer - this has yet to be confirmed by Neal, though. The ultra-safe way of * doing this is to create a compatible clone of StringBuilder for 1.4. * * @param index - a constant pool index to a UTF8Constant. */ private int updateId( jace.parser.ConstantPool pool, int index ) { UTF8Constant constant = ( UTF8Constant ) pool.getConstantAt( index ); String name = constant.toString(); String newName = name; // System.out.println( "considering: " + name ); if ( name.indexOf( '+' ) != -1 ) { // System.out.println( "Replacing '+'" ); newName = name.replace( '+', '$' ); } if ( newName.indexOf( "java/lang/StringBuilder" ) != -1 ) { // System.out.println( "Replacing StringBuilder." ); newName = newName.replace( "java/lang/StringBuilder", "java/lang/StringBuffer" ); } // Change all references from java.lang.Iterable to our own Iterable_ class // which is signature compatible with Iterable. // if ( newName.indexOf( "java/lang/Iterable" ) != -1 ) { // System.out.println( "Replacing Iterable." ); newName = newName.replace( "java/lang/Iterable", "com/rc/retroweaver/runtime/Iterable_" ); } // Change all references from java.lang.Enum to our own Enum_ class // which is signature compatible with Enum. // if ( newName.indexOf( "java/lang/Enum" ) != -1 ) { // System.out.println( "Replacing Enum." ); newName = newName.replace( "java/lang/Enum", "com/rc/retroweaver/runtime/Enum_" ); } if ( newName != name ) { int value = pool.addUTF8( newName ); // System.out.println( "Replacing " + name + " with " + newName ); return value; } return -1; } /** * Changes the format of the class file from 1.5 to 1.4. * * 1) Changes the version flag to 1.4 * 2) Rewrites synthetic access specifiers as synthetic attributes * * I have to use Jace here, because I don't see how I can make this * work with BCEL (easily anyway). * */ private boolean fixupFormat() throws IOException { ClassFile classFile = new ClassFile( sourcePath ); if ( lazy && classFile.getMajorVersion() == newVersion ) { return false; } classFile.setVersion( newVersion, 0 ); // Fix up identifiers in the constant pool. // Change the class name and class file if necessary // if ( classFile.getClassName().indexOf( '+' ) != -1 ) { String newClassName = classFile.getClassName().replace( '+', '$' ); classFile.setClassName( newClassName ); String simpleName = ""; // System.out.println( "Setting new class name: " + newClassName ); StringTokenizer st = new StringTokenizer( newClassName, "/" ); while ( st.hasMoreTokens() ) { simpleName = st.nextToken(); } File f = new File( outputPath ); // Delete the old class file, if we're weaving to the same place. if ( sourcePath.equals( outputPath ) ) { // System.out.println( "deleting " + sourcePath ); f.delete(); } File dir = f.getParentFile(); String name = f.getName(); outputPath = dir.getCanonicalPath() + File.separator + simpleName + ".class"; // System.out.println( "Updating output path: " + outputPath ); } // Change the super class name if necessary // String newClassName = classFile.getSuperClassName().replace( '+', '$' ); newClassName = newClassName.replace( "java/lang/Iterable", "com/rc/retroweaver/runtime/Iterable_" ); newClassName = newClassName.replace( "java/lang/Enum", "com/rc/retroweaver/runtime/Enum_" ); classFile.setSuperClassName( newClassName ); // Run through all of the CONSTANT_Class, CONSTANT_Field, and CONSTANT_Method // pool entries, and update their identifers. This catches the interfaces // implemented by the class too. // jace.parser.ConstantPool pool = classFile.getConstantPool(); for ( int i = 0; i < pool.getSize(); ++i ) { jace.parser.constant.Constant c = pool.getConstant( i ); if ( c instanceof ClassConstant ) { ClassConstant constant = ( ClassConstant ) c; update( pool, constant ); } else if ( c instanceof NameAndTypeConstant ) { // catches fieldref, methodref, and interfacemethodref constants NameAndTypeConstant constant = ( NameAndTypeConstant ) c; update( pool, constant ); } else { continue; } } // Now run through all of the methods, fields, and local variables // updating their names and signatures. // for ( ClassMethod m : classFile.getMethods() ) { update( pool, m ); CodeAttribute code = m.getCode(); if ( code != null ) { LocalVariableTableAttribute lvt = code.getLocalVariableTable(); if ( lvt != null ) { for ( LocalVariableTableAttribute.Variable v : lvt.getVariables() ) { update( pool, v ); } } } } for ( ClassField f : classFile.getFields() ) { update( pool, f ); } classFile.writeClass( outputPath ); return true; } private void update( jace.parser.ConstantPool pool, ClassConstant constant ) { int nameIndex = updateId( pool, constant.getNameIndex() ); if ( nameIndex != -1 ) { constant.setNameIndex( nameIndex ); } } private void update( jace.parser.ConstantPool pool, NameAndTypeConstant constant ) { int nameIndex = updateId( pool, constant.getNameIndex() ); if ( nameIndex != -1 ) { constant.setNameIndex( nameIndex ); } int descriptorIndex = updateId( pool, constant.getDescriptorIndex() ); if ( descriptorIndex != -1 ) { constant.setDescriptorIndex( descriptorIndex ); } } private void update( jace.parser.ConstantPool pool, LocalVariableTableAttribute.Variable var ) { int nameIndex = updateId( pool, var.nameIndex() ); if ( nameIndex != -1 ) { var.setNameIndex( nameIndex ); } int descriptorIndex = updateId( pool, var.descriptorIndex() ); if ( descriptorIndex != -1 ) { var.setDescriptorIndex( descriptorIndex ); } } private void update( jace.parser.ConstantPool pool, ClassField field ) { int nameIndex = updateId( pool, field.getNameIndex() ); if ( nameIndex != -1 ) { field.setNameIndex( nameIndex ); } int descriptorIndex = updateId( pool, field.getDescriptorIndex() ); if ( descriptorIndex != -1 ) { field.setDescriptorIndex( descriptorIndex ); } } private void update( jace.parser.ConstantPool pool, ClassMethod method ) { int nameIndex = updateId( pool, method.getNameIndex() ); if ( nameIndex != -1 ) { method.setNameIndex( nameIndex ); } int descriptorIndex = updateId( pool, method.getDescriptorIndex() ); if ( descriptorIndex != -1 ) { method.setDescriptorIndex( descriptorIndex ); } } /** * Replaces calls to the 1.5 runtime with calls to the Retroweaver runtime. * Fixes autoboxing, class literals, (_future_ enums too?) */ private void fixupRuntimeCalls() throws IOException { // Fix up code in all of the methods // Method[] methods = generator.getMethods(); for ( int i = 0; i < methods.length; ++i ) { Method m = methods[ i ]; MethodGen methodGen = new MethodGen( m, className, constantPool ); InstructionList list = methodGen.getInstructionList(); // It's possible that this is a native or abstract method // and has no instructions associated with it. // In that case, it's not going to be a match. if ( list == null ) { continue; } fixupAutoboxing( list ); fixupClassLiterals( list ); methodGen.setMaxLocals(); methodGen.setMaxStack(); generator.removeMethod( m ); generator.addMethod( methodGen.getMethod() ); list.dispose(); } } /** * Fix autoboxing. * * Search for calls to invokestatic on any of the <Primitive>.valueOf functions * and replace them with calls to our own runtime. */ private void fixupAutoboxing( InstructionList list ) { InstructionFinder finder = new InstructionFinder( list ); InstructionFinder.CodeConstraint constraint = new InstructionFinder.CodeConstraint() { public boolean checkCode( InstructionHandle[] match ) { INVOKESTATIC instruction = ( INVOKESTATIC ) match[ 0 ].getInstruction(); String methodName = instruction.getMethodName( constantPool ); Type t = instruction.getType( constantPool ); Type[] argTypes = instruction.getArgumentTypes( constantPool ); // System.out.println( "type = " + t ); boolean matches = methodName.equals( VALUE_OF_METHOD ) && boxTypes.containsKey( t ) && argTypes.length == 1 && argTypes[ 0 ] instanceof BasicType; // System.out.println( "match = " + matches ); return matches; } }; Iterator it = finder.search( "INVOKESTATIC", constraint ); if ( ! it.hasNext() ) { return; } while ( it.hasNext() ) { InstructionHandle[] instructions = ( InstructionHandle[] ) it.next(); InstructionHandle match = instructions[ 0 ]; INVOKESTATIC instruction = ( INVOKESTATIC ) match.getInstruction(); Type t = instruction.getType( constantPool ); constantPool.addClass( AUTOBOX_CLASS ); int methodIndex = constantPool.addMethodref( AUTOBOX_CLASS, VALUE_OF_METHOD, ( String ) boxTypes.get( t ) ); INVOKESTATIC replacementInstruction = new INVOKESTATIC( methodIndex ); InstructionHandle handle = list.insert( match, replacementInstruction ); try { list.delete( match ); } catch( TargetLostException e ) { InstructionHandle[] targets = e.getTargets(); for ( int i = 0; i < targets.length; i++ ) { InstructionTargeter[] targeters = targets[ i ].getTargeters(); for ( int j = 0; j < targeters.length; j++ ) { targeters[ j ].updateTarget( targets[ i ], handle ); } } } } } /** * Fix class literals. * * The 1.5 VM has had its ldc* instructions updated so that it knows how to deal with * CONSTANT_Class in addition to the other types. So, we have to search for uses of * ldc* that point to a CONSTANT_Class and replace them with calls to our runtime. * */ private void fixupClassLiterals( InstructionList list ) { final String searchTerm = "(LDC | LDC_W)"; InstructionFinder finder = new InstructionFinder( list ); InstructionFinder.CodeConstraint constraint = new InstructionFinder.CodeConstraint() { public boolean checkCode( InstructionHandle[] match ) { if ( match.length != 1 ) { // System.out.println( "Ignoring a match of length: " + match.length ); return false; } Instruction instruction = match[ 0 ].getInstruction(); if ( ! ( instruction instanceof LDC ) ) { // catches both LDC and LDC_W return false; } LDC ldc = ( LDC ) instruction; org.apache.bcel.classfile.Constant c = constantPool.getConstant( ldc.getIndex() ); return c instanceof ConstantClass; } }; Iterator it = finder.search( searchTerm, constraint ); if ( ! it.hasNext() ) { return; } while ( it.hasNext() ) { InstructionHandle[] instructions = ( InstructionHandle[] ) it.next(); InstructionHandle match = instructions[ 0 ]; LDC instruction = ( LDC ) match.getInstruction(); ConstantClass c = ( ConstantClass ) constantPool.getConstant( instruction.getIndex() ); ConstantUtf8 cUtf8 = ( ConstantUtf8 ) constantPool.getConstant( c.getNameIndex() ); String className = cUtf8.getBytes(); int classNameIndex = constantPool.addString( className ); // System.out.println( "Replacing an LDC for CONSTANT_Class " + className ); constantPool.addClass( CLASS_LITERAL_CLASS ); int methodIndex = constantPool.addMethodref( CLASS_LITERAL_CLASS, GET_CLASS_METHOD, GET_CLASS_SIGNATURE ); // TODO: Need to dispose newInstructions, but when is it safe??!! InstructionList newInstructions = new InstructionList(); newInstructions.append( new LDC_W( classNameIndex ) ); newInstructions.append( new INVOKESTATIC( methodIndex ) ); InstructionHandle handle = list.insert( match, newInstructions ); try { list.delete( match ); } catch( TargetLostException e ) { InstructionHandle[] targets = e.getTargets(); for ( int i = 0; i < targets.length; i++ ) { InstructionTargeter[] targeters = targets[ i ].getTargeters(); for ( int j = 0; j < targeters.length; j++ ) { targeters[ j ].updateTarget( targets[ i ], handle ); } } } finder.reread(); it = finder.search( searchTerm, constraint ); } } private boolean alreadyAspected() { // do nothing for now // I could add in an attribute to look for here return false; } public void setListener( WeaveListener listener ) { this.listener = listener; } public void setLazy( boolean lazy ) { this.lazy = lazy; } public static String getUsage() { /* StringBuffer buf = new StringBuffer(); buf.append( "Usage: RetroWeaver " ); buf.append( newLine ); buf.append( " <source path>" ); buf.append( newLine ); buf.append( " <output path>" ); return buf.toString(); */ return "Usage: RetroWeaver " + newLine + " <source path>" + newLine + " [<output path>]"; } public static void main( String[] args ) { if ( args.length < 1 ) { System.out.println( getUsage() ); return; } String sourcePath = args[ 0 ]; String outputPath = null; if ( args.length > 1 ) { outputPath = args[ 1 ]; } try { RetroWeaver weaver = new RetroWeaver( Weaver.VERSION_1_4 ); weaver.weave( sourcePath, outputPath ); } catch ( Throwable t ) { t.printStackTrace(); } } }