Archive for August, 2010

__LINE__ __FILE__ macro for java. Part II.

Second part of this post

How does it work?

As the jvm loads each class, it passes it to each instrumentation which registers a ClassFileTransformers. This gives a chance to alter its byte code. I am using this system to remove the reference to the __LINE__ field, and change it with the current line.
Let’s see the byte code of a simple class, to see how we can manipulate it

System.err.println( __FILE_LINE__ );

This simple code is compiled to the following bytecode.

   0:   getstatic       #8; //Field java/lang/System.err:Ljava/io/PrintStream;
   3:   getstatic       #14; //Field com/dg/ab/use/instument/CodeLocation.__FILE
_LINE__:Ljava/lang/String;
   6:   invokevirtual   #20; //Method java/io/PrintStream.println:(Ljava/lang/String;)V 

This can be easily generated using javap –c, which is included in any jdk

At line 3, we can see the actual instruction, which is loading the content of the __FILE__ into the stack. At this point, if you can just change this instruction to load a string which contains the actual line position, we solve our problem.
So we expect to convert the previous bytecode into this one

   0:   getstatic       #8; //Field java/lang/System.err:Ljava/io/PrintStream;
   3:   ldc     #14; //String Test.java:7
   5:   invokevirtual   #16; //Method java/io/PrintStream.println:(Ljava/lang/St
ring;)V

And doing that is extremely easy using the ASM bytecode manipulation library.
We just have to load the class, and ask a ClassReader to process it.
During the process, the ClassReader will produce some visiting events, that will be passed to our ClassAdator which will slightly change the content, and pass the event to a ClassWriter that will write the visiting event to bytecode.

Schema
To come

Code

public class CodeLocationClassTransformer  implements ClassFileTransformer {
	@Override
	public byte[] transform(ClassLoader loader, String className, Class redefiningClass, ProtectionDomain domain, byte[] bytes) throws IllegalClassFormatException {
		System.out.println("CodeLocation to Transform Class: " + className);
		ClassReader cr = new ClassReader(bytes);
		ClassWriter cw = new ClassWriter(cr, 0);
		CodeLocationClassAdapter adapter = new CodeLocationClassAdapter(cw);
		cr.accept(adapter, 0);
	    byte[] result = cw.toByteArray();
		return result;
	}

	public class CodeLocationClassAdapter extends ClassAdapter {
		private String source;
		public CodeLocationClassAdapter(ClassVisitor cv) {
			super(cv);
		}
		
		@Override
		public void visitSource(String source, String debug) {
			this.source = source;
			cv.visitSource(source, debug);
		}

		@Override
		public MethodVisitor visitMethod(int access, String name, String desc,
				String signature, String[] exceptions) {
			MethodVisitor mv;
			mv = cv.visitMethod(access, name, desc, signature, exceptions);
			if (mv != null) {
				mv = new CodeLocationMethodAdapter(this, mv);
			}
			return mv;
		}
	}

	public class CodeLocationMethodAdapter extends MethodAdapter {
		private Integer lineSeen;
		private CodeLocationClassAdapter owner;
		public CodeLocationMethodAdapter(CodeLocationClassAdapter owner, MethodVisitor mv) {
			super(mv);
			this.owner = owner;
		}

		@Override
		public void visitFieldInsn(int opcode, String owner, String name, String desc) {
			if ( opcode == Opcodes.GETSTATIC ) {
				// replace the instruction 
				// getstatic       #23; //Field com/dg/ab/use/instument/CodeLocation.__FILE_LINE__:Ljava/lang/String;
				if ( owner.equals(CodeLocation.class.getName().replace(".", "/"))) {
					if ( name.equals("__LINE__" ) ) {
						// by
						// ldc #xx // "linenumber"
						super.visitLdcInsn(String.valueOf(lineSeen));
						return;
					} else 
					if ( name.equals("__FILE__" ) ) {
						// replace with a ldc
						// ldc #xx // "source file"
						super.visitLdcInsn(this.owner.source);
						return;
					} else 
					if ( name.equals("__FILE_LINE__" ) ) {
						// replace with a ldc
						// ldc #xx // "source file:linenumber"
						super.visitLdcInsn(this.owner.source + ":" + String.valueOf(lineSeen));
						return;
					} 
				}
			}

			super.visitFieldInsn(opcode, owner, name, desc);
		}
		
        public void visitLineNumber(int line, Label paramLabel) {
        	this.lineSeen = line; 
        	super.visitLineNumber( line, paramLabel );
        }
	}

}

As you can see there are 3 main steps used during the byte code manipulation.

  • CodeLocationClassTransformer

    Class call by the jvm, in order to instument the classes. Transform the class by processing the bytecode through a CodeLocationClassAdapter
  • CodeLocationClassAdapter

    Will That store the current filename and process the Method bytecode through a CodeLocationMethodAdaptor.
  • CodeLocationMethodAdapter

    store the current line information, as provided by the ASM library. And convert during the visiting instruction events, the GETSTATIC on our fields ( __LINE__, __FILE__ ) to the LDC instruction with the appropriate parameters.

The only thing which is still missing is the code to register the CodeLocationClassTransformer.
This is done in the class PremainCodeLocaiton

public class PremainCodeLocation {
	public static void premain(String agentArguments, Instrumentation instrumentation) {	
		instrumentation.addTransformer(new CodeLocationClassTransformer());
	}
}

The agent premain class is called whe the agent jar is provided in the command line of the java, and this jar contains the appropriate entry in the manifest.

Manifest-Version: 1.0 
Premain-Class: com.dg.ab.use.instument.PremainCodeLocation