__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