java.class-file

How to add/replace the SourceFile attribute in a .class file


I want to add/replace the SourceFile attribute in a compiled Java .class file. I have not noticed any obscure command-line options to the Java compiler to override the default value of SourceFile. Neither have I seen anything in the Java reflection API that would help me. Skim reading chapter 4 of the JVM specification indicates I could spend a few weeks writing a .class file parser/modifier to do the work. Before I invest the effort to write such a parser/modifier, I want to check if I am missing something. Is there anything in the standard JDK to help with adding/replacing the SourceFile attribute?

For anyone wondering why I would want to mess with the SourceFile attribute... I have a command-line tool that preprocesses a "Java enhanced with some syntactic sugar" file into Java syntax. The file-name extension for this type of file is .bi. Thus, the preprocessor converts Foo.bi into Foo.java. In addition, there is line-number correspondence between Foo.bi and Foo.java, so if a runtime error occurs on, say, line 42 of Foo.java, then the bug should really be fixed on line 42 of Foo.bi (and then run the preprocessor and compile the updated Foo.java file). To facilitate this, I would like the stack trace of the error to indicate Foo.bi rather than Foo.java, and my experiments suggest this can be achieved by ensuring the Foo.class file has a SourceFile attribute with a value of Foo.bi.


Solution

  • There is no such feature in the standard JDK API, but before trying to implement a bytecode processor yourself, consider using a 3rd party library like ASM. With that library, the task could be implemented like:

    public static void main(String[] args) throws IOException {
        Path in = Paths.get(URI.create("jrt:/java.base/java/lang/Object.class"));
        Path out = Files.createTempFile("Object", ".class");
    
        changeSourceAttr(in, out, "Object.bi");
    
        runJavaP(out);
    
        Files.delete(out);
    }
    
    private static void changeSourceAttr(Path in, Path out, String newValue)
        throws IOException {
    
        ClassReader cr = new ClassReader(Files.readAllBytes(in));
        ClassWriter cw = new ClassWriter(cr, 0);
        cr.accept(new ClassVisitor(Opcodes.ASM5, cw) {
            boolean sourceSeen;
            @Override
            public void visitSource(String source, String debug) {
                sourceSeen = true;
                super.visitSource(newValue, debug);
            }
    
            @Override
            public void visitEnd() {
                if(!sourceSeen) {
                    super.visitSource(newValue, null);
                }
                super.visitEnd();
            }
        }, 0);
        byte[] code = cw.toByteArray();
        Files.write(out, code);
    }
    
    private static void runJavaP(Path out) {
        ToolProvider.findFirst("javap")
            .ifPresent(tp -> tp.run(System.out, System.err, out.toString()));
    }
    
    Compiled from "Object.bi"
    public class java.lang.Object {
      public java.lang.Object();
      public final native java.lang.Class<?> getClass();
      public native int hashCode();
      public boolean equals(java.lang.Object);
      protected native java.lang.Object clone() throws java.lang.CloneNotSupportedException;
      public java.lang.String toString();
      public final native void notify();
      public final native void notifyAll();
      public final void wait() throws java.lang.InterruptedException;
      public final native void wait(long) throws java.lang.InterruptedException;
      public final void wait(long, int) throws java.lang.InterruptedException;
      protected void finalize() throws java.lang.Throwable;
    }
    

    The relevant part is the changeSourceAttr method. It will change the source file attribute if present or add a new one otherwise.

    The example changes the attribute for the java.lang.Object class using a temporary file and runs javap to show the result. The example code requires JDK9+, the actual transformation method should work on previous versions too. When the source file does not originate from the JDK library, source and target may be the same file.