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:
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.