Wednesday, July 04, 2007

Joint Compilation in Groovy

Note: This is not implemented since today, it is already some weeks old, but there was no information about it on the net... So I wrote this

When working with Languages like Groovy you naturally mix Groovy and Java all the time. But the problem then arises to compile the resulting monster. Especially the Eclipse plugin for Groovy makes you sometimes think your project will compile outside as nice as inside Eclipse. But it might not. Consider for example this case

class A {
B b;
}
class B extends A {}
And think A is written in Groovy, but B is written in Java, or the other way, A is written in Java, but B is written in Groovy. It is clear, that when you compile B, you need class A as well, because the class might be final or abstract or other things that need to be checked. But when you compile A, you will see that it refers B, which means you need to compile B as well. Now if this is one compiler we have no problem, but A and B are written in different languages with compilers not sharing their class information. That means we are stuck. And while this example looks a bit artificial, you get very fast into this situation in a larger project. Who would keep track of not referencing Groovy classes from the Java side to get the compilation done? It's disturbing.

Solutions?

Now the one solution would be to let the compiler share their class data. The Groovy compiler is able to do that, the eclipse compiler is able to do that, we might see a bright future for this in the future. But for example JavaC is not able to do this. At last I don't know how.

Another way is to create stubs for the Groovy classes, run the Java compiler with these stubs, then run the Groovy compiler and overwrite the generated stubs. Alex Tkachman was so free to show us Groovy people how to do this and provided a patch, we could us as base to get this version of the compilation running.

How it Works:

The details of the stub generation are not so important I think, they are created as Java files from the parse tree the Groovy compiler provides in a temporary directory and then feed to JavaC along with the normal Java files. You don't need to start the compiler yourself, the Groovy compiler will do this for you right after the parse phase and then continue with its normal compilation process.

Controlling JavaC:

Since we now have a combined compiler we of course want to use it in our build, but the problem is that javac would by default created a wrong bytecode version for us, we need 1.4 compatible bytecode, so source and target options are needed at last. I then decided to forward the options to the compiler from the command line of Groovy. So if you do
groovyc *.groovy *.java -j -Jsource=1.4 -Jtarget=1.4
You will get the java files compiled for 1.4. -j turns the joint compilation on , the -J parts are gving key-value pairs to the compiler. Using Options without value is also possible using the -F option, just without the equals part like -Fdeprecation. Anything the JavaC compiler supports can be dropped in there... Of course some special options like the VM memory size would not make sense since the VM is already created.

For the GroovyC Ant task the picture is a bit different. first I thought about a way to generically define attributes for the GroovyC task I can forward to JavaC... But me not being the Ant expert I gave this up and decided to do the following work around
<echo message="Groovyc of test code."/>
<java classname="org.codehaus.groovy.ant.Groovyc" fork="true" maxmemory="128M">
<classpath>
<pathelement path="${mainClassesDirectory}"/>
<pathelement path="${testClassesDirectory}"/>
<path refid="testPath"/>
</classpath>
<arg value="${testClassesDirectory}"/>
<arg value="${testSourceDirectory}"/>
<arg value="-j"/>
<arg value="-Jsource=1.4"/>
<arg value="-Jtarget=1.4"/>
</java>
Oh, that reminds me that the GroovyC task needs a fork ability. Anyway, that's when using the ant task from the command line. If you want to use it normally, then
<groovyc
srcdir="${mainSourceDirectory}" destdir="${mainClassesDirectory}"
classpathref="groovyMainCompileDependencies"
jointCompilationOptions="-j -Jsource=1.4 -Jtarget=1.4"
/>
can be used. Same game as on the command line.

What this solution can't do:

Yes, there is a downside. Ok, I think it is already a downside that we have to use a temporary directory, but another one is that we need to know all files we want to compiler before compilation. I don't think that is a problem when running a ant or maven based built, but for the typical usage on the command line, where you just compile your main class and the compiler will get a hold on all further classes will not work. To be more specific, it will not work when the Groovy compiler needs to get an additional Groovy class and the Java compiler would need that class too. That's because in this case no stub will be created and thus the java compiler will fail telling you it can't find a that class. On the other hand GroovyC works with the resulting class files, so if a Groovy class refers a Java class and JavaC did not compile it, then GroovyC won't be able to compile it either... well, ok, just give the compiler all needed files ;)

Future Work:

The current implementation uses JavaC directly a nice framework would be nice here to have more than just this compiler. And there is for example JCI, but JCI seems not to support options... well we need to take another look at it, maybe it supports enough. On the other hand I am thinking about integrating the JavaC task from ant. in that case we could maybe use the normal task as nested element (with some tweaks) and have all the abstraction to different compilers ant allows. Of course by directly using the Eclipse compiler (it is usable outside the IDE) we could let the compiler share class data and then compile files that are not part of the file list given at runtime.

But I think the ant task version will make it. the work around with the "-j -J -F" options might then vanish.


But none the less, have fun with the upcoming
Groovy 1.1 beta 2

No comments: