Atom Feed SITE FEED   ADD TO GOOGLE READER

Using m4 as a macro processor for Java, the nice way

For reasons I won't disclose, I've come to need a macro processor to generate a bunch of .java files. Somebody at work is using m4 for a similar task, so I decided to try it. Unfortunately, using macro processors with Java brings a huge problem - you typically cannot use an IDE like IntelliJ to edit the raw files. For example, this trivial macro program cannot be parsed by IntelliJ:
public class HelloNTimes {
public static void main(String[] args) {
forloop(`i', 1, HELLO_COUNT, `System.out.println("Hello i");
')
}
}

After running it through m4 (first adding the standard m4 forloop), the output is compilable:
public class HelloNTimes {
public static void main(String[] args) {
System.out.println("Hello 1");
System.out.println("Hello 2");
System.out.println("Hello 3");
System.out.println("Hello 4");
}
}


But I'd like to have my cake and eat it too. I'd like to be able to edit a .java file that is both well-formed and contains macros! To make this possible, I need alternate code for every piece of macro code that I write. This alternate code serves many purposes:
  • it allows me to reference symbols defined only by macros
  • it gives me something to verify the generated code against

    Including the alternate code, here's what the HelloNTimes app looks like:
    public class HelloNTimes {
    public static void main(String[] args) {
    /* BEGIN_M4_MACRO
    forloop(`i', 1, HELLO_COUNT, `System.out.println("Hello i");
    ')
    END_M4_MACRO */
    // BEGIN_M4_ALTERNATE

    System.out.println("Hello 1");
    System.out.println("Hello 2");
    // END_M4_ALTERNATE
    }
    }

    As you can see, the 'alternate' section tells you what the macro could generate. Of course, if the parameters passed to the macro change (in this case HELLO_COUNT), the alternate will not equal what's generated. Due to the careful use of commenting, /* and */, this code will compile fine! That means I can edit this file just fine in my IDE.

    Then I add m4 definitions, to remove my markers when the file is processed:
    define(`BEGIN_M4_MACRO', ` BEGIN M4 MACRO GENERATED CODE *'`/')
    define(`END_M4_MACRO', `/'`* END M4 MACRO GENERATED CODE ')
    define(`BEGIN_M4_ALTERNATE', `BEGIN M4 ALTERNATE CODE
    /'`* ')
    define(`END_M4_ALTERNATE', `END ALTERNATE CODE *'`/')


    Finally I can see what the whole works generates. Note that it keeps the alternate code inside a comment, so I can verify that it's generating what I expect:
    public class HelloNTimes {
    public static void main(String[] args) {
    /* BEGIN M4 MACRO GENERATED CODE */
    System.out.println("Hello 1");
    System.out.println("Hello 2");
    System.out.println("Hello 3");
    System.out.println("Hello 4");

    /* END M4 MACRO GENERATED CODE */
    // BEGIN M4 ALTERNATE CODE
    /*
    System.out.println("Hello 1");
    System.out.println("Hello 2");
    // END ALTERNATE CODE */

    }
    }

    Note that the generated code has 4 hellos, wheras the alternate has only 2. This is because I'm running m4 with a parameter, -DHELLO_COUNT=4.

    Preprocessor code is inconvenient, but when I can edit in my IDE, it loses a lot of its pain! If you're interested, please look at the example HelloNTimes.jh source file, or my real-world usage in NodeN.