JCC - A Java to C converter

Nik Shaylor
Version 0.02
8-May-97


Contents


Introduction

JCC is a direct Java to C translator. Unlike other translators JCC does not convert Java class files, but instead processes Java source code directly. It converts whole programs at a time, which can cause the conversion process to be quite lengthy, but has the advantage that a number of optimisations can be performed that would be very difficult to achieve with other techniques. When used with a good optimising C compiler it produces programs that are typically between 10 to 20 times faster than the 1.0.2 JVM. One example sort program has been speeded up 55 times.

A beta level evaluation copy of the program may be downloaded from the Internet, that will convert command line applications such as Sun's javac on WIN32 systems. JCC may produce code that is compatible with other platforms, but this has not been tried, and a certain amount of porting of the run time library would be necessary.

The main changes since the last version are:


The Conversion Process

Java programs are normally compiled to a virtual machine code that is then interpreted. This is a very flexible way of producing machine independent programs, but carries with it a severe overhead. The use of so called "Just In Time" compilation techniques can help a great deal to speed things up, but ultimately a traditional compilation process is probably going to result in the fastest results.

The idea behind JCC is to convert Java source code into C in such a way that a good optimising C compiler will be able to build an efficient program. To this end the translated program looks like a normal C program which means the typical optimisation techniques used in most modern C compilers are effective.

Java software built with JCC is efficient for the following reasons:

  1. The resulting program is not interpreted.
  2. Native Java types (like ints chars etc.) are directly converted into their equivalent C types so formulaic expressions will execute with exactly the same level of efficiency that they would have in C.
  3. Object fields are converted to C structure fields which are typically accessed using only one machine instruction.
  4. Most methods are invoked using a normal call instruction. This is largely because methods can automatically be made final if they are not overridden in a subclass. This is done on a translation by translation basis, so if, for instance, java.util.Hashtable was not subclasses in a particular program all its methods would automatically be regarded as final and could be called directly. Even if it were subclassed, only the overridden methods would require calling through a virtual function table. This technique typically results in 85% of all method calls being done directly, which can lead to a further optimisation if the C compiler can "inline" small functions which are very common in object oriented programming.
  5. Exceptions are handled using setjmp() and longjump() which typically are very efficient routines.
  6. No runtime checking is made in the executing program.
The last point here is of great importance, the normal Java execution environment performs a large number of checks for things like invalid object references, and array bound exceptions. The programs generated by JCC do not include any such checks and so must be fully debugged before the translation process is performed.

The conversion process is started by specifying just one class name that must contain the normal static main() method. This method is translated first and from this any other classes that are needed are marked and subsequently translated, which will lead to further classes being translated etc. Only the methods and fields actually used in a program are produced making the resulting source code as small as possible.

The resulting set of .c and .h files are then all compiled together in one go, and linked to the C runtime library in the normal way.


Hardware Requirements

The translation software is entirely written in Java and should run on all Java platforms. The program is quite large, and currently requires virtual memory that is proportional to the size of the program being converted. Large programs like javac can be converted in about 20 minutes on a 100Mhz Pentium with about 32MB of RAM. Converting programs with less than this amount of memory should be possible, but may be very slow.


Software Requirements

The output C code currently makes use of a number of non standard extensions to the C language. These are:
  1. Identifiers are very long indeed. This causes no problems to Microsoft Visual C++ version 4.0, but other compilers may object. A future version should provide the option to bring these in line with the ANSI standard.
  2. If the Java long is used, 64 bit integer support is required.
  3. Some sort of garbage collection software is needed. I am currently using the Boehm-Demers-Weiser garbage collector.


Installation

The distribution jcc002.zip file must be unpacked preserving the directory structure into a part of the class hierarchy tree. The simplest way of doing this is to unpack it into the root of your hard disk and add the root into the CLASSPATH environment variable.

JCC used a couple of the "sun.*" classes so these must also be accessible from the class path.


Operation

JCC is run with the following command line options:
java nshaylor.jcc.Main [switches] qualified.class.Name

Switches:

        -v           Verbose mode
        -e           Copy output messages to System.err 
        -s           Output class summary table
        -p           Don't cast parameters in method definitions. 
                     This produces lots of compiler error messages
                     but makes debugging the C code easier.
        -t           Generate threaded code
        -sourcepath  Specify the source path 

The main parameter is the name of a class that contains the main() method. This class must be in the sourcepath which is the logical equivalent of the classpath except this is where the Java source for the program is to be found. If the -sourcepath switch is not specified the current classpath is used. For any normal programs the source of the java.* classes must be present on the source path. Sun's SDK for the WIN32 platform has these in a file called src.zip. This file must be unzipped and placed on the sourcepath along with the other directories that contain the program to be converted. JCC can read files from .zip containers, but unfortunately the src.zip file contains files that start in a directory called "src". The simplest solution is to unzip the contents into the root of a disk and include "\src;" in the sourcepath.

The converted files will be placed in the default directory.

WARNING - Any program being translated must be fully debugged and tested using a normal Java system. The error messages output by JCC are not always very explicit, and not many checks are made of a program's correctness. Additionally the odd compilation error may be present when the C code is compiled. None are known to exist that matter, and the others are being worked on at present.

The following may help to get things running:

  1. Install the zip file into the root of C: so that the 'nshaylor' directory is C:\NSHAYLOR
  2. Copy the file C:\NSHAYLOR\JCC\GC\GC.DLL into your windows directory.
  3. Install Sun's 1.0.2 JDK in such a way that it is in a directory C:\JAVA and the 'bin' directory is C:\JAVA\BIN
  4. Make PATH include C:\JAVA\BIN;C:\NSHAYLOR\BIN; and set CLASSPATH=.;C:\
  5. Unzip the file C:\JAVA\SRC.ZIP into the root on C: so you get a new directory called C:\SRC that contains subdirectories of 'JAVA' and 'SUN'
  6. Install MS VC++4.0 (anywhere you like)
  7. Type: CD \NSHAYLOR\JCC
  8. Type: javac *.java
  9. Type: CD \NSHAYLOR\JCC\TESTS\QUICKSORT4
  10. Type: BLD


Restrictions

A major principal in the design of this program was to favor speed of execution over strict language compatibility. This might seem like heresy, especially considering the fantastic specification of the language that is available, but it was seen that the main reason for the converter was to produce fast programs, and if this were compromised then there would be little point to it. Nevertheless the differences are small, and a great deal of code has now been successfully converted.

The following is a list of the known language restrictions. There are in addition many functions missing from the runtime library that will prevent some programs being built. Most notably there is no support for the AWT classes at present.

Packages

Only Java files that are in a package can be translated.

Case Expressions

Case expressions in Java may be a literal or a final variable. This means that constructions like:
    static final int x = foo();
result in a variable that can be used in a switch expression. Because JCC translates Java switch statement directly into a C switch statement this is not allowed. This restriction had not caused any programs I know of to fail to be translated.

Threads and Synchronisation

The current package now support for these features, but the implementation is a little unusual. The normal Java SDK uses operating system threads which are allowed to compete for execution in the normal way. Synchronisation is achieved using the normal operating system primitives.

My implementation of this (for WIN32 only) works a different way that has some advantages and some disadvantages. In the same way as above the threads used by JCC are normal operating system threads, but JCC only allows one thread at a time to execute 'within' the JCC system. There is a formal definition of being 'inside' or 'outside' the JCC environment. A thread is inside with it executes code that lies between calls to jcc_enterCriticalSection() and jcc_exitCriticalSection(). When a thread is started it immediately is placed inside the system where it is guaranteed to be the only thread executing in this state. When the thread has to perform I/O then it exits from the JCC environment (by calling jcc_exitCriticalSection()), performs the I/O and then re-enters (by calling jcc_enterCriticalSection()). If a thread tries to enter the system when another is already inside then is it blocked until the current thread exits.

The results of this system is that the threads are not preemptable like they are in the normal JDK. The is nothing actually in the Java spec that disallows this approach, but some programs may not execute correctly because of this.

The main advantage of this arrangement is that synchronisation comes virtually for free. The time penalty incurred entering and exiting synchronised code sections is little more that pushing an address on a special stack. If by the time the thread next exits from the system there is anything on this stack real monitor lock objects are created. This will never be necessary for synchronised methods that do not perform I/O. A good example here is java.lang.Hashtable.

The only other disadvantage is that programs may not run particularly efficiently on a symmetric multiprocessor (SMP) machine. An exception here maybe programs that are highly I/O bound, because I/O, and code outside the JCC environment, can occur simultaneously even if the code within it cannot.

WARNING - The garbage collector used in this package is not thought to be 100% solid with threaded code. Personally I have had no problems, but the use of this feature (like everything else) is entirely at your own risk.

Finalisation

The current package has no support for this feature.

Floating point Numbers

Little work has been done on floating point number compatibility. They appear to work, and are very fast, but software that uses the NaN, and Infinite values will probably not work. Certainly expressions like:
    double POSITIVE_INFINITY = 1.0 / 0.0;
will not compile. Further work is needed in this area.

Initialization

Normal Java systems load class files "on demand" which means that the order of execution that a program may take cannot be determined statically. Unfortunately this is just what JCC has to manage, and it does the best approximation it can, by starting with all the static initialization code in the root class and following the methods it can call recursively. The order of class initialization is then taken to be the reverse of this. This may or mat not be the same as the order the JDK takes, but it mostly seems to work.

When there have been problems I have solved them by making dummy calls into the primary class. This can look something like:

public class Main
{

public static void main( String args[] )
    {
    if( false )
        {
        new Runtime();
        new System();
        }
    
    whatever...
}
This example will cause the class initialisers in Runtime() and System() to be called before the others. (The fact that the above code is not legal in the normal Java systems does not trouble JCC.)

Dynamically loaded classes

The JCC system does not really support dynamic class loading, but the Class.forName() function will work for all the classes included in a build. A same trick as above should be used to cause any required extra classes to be included.


Comparative Performance Results

Only a short amount time has been spent in looking at this, but I think you will find the results impressive. I expect these early results to be improved upon as JCC is further developed. Credit for these results also belong to the Microsoft compiler team, as well as Hans-Juergen Boehm and his colleagues for the garbage collector.

javac

The source code for JavaLex was used for this test. It is a 228KB source file that defines 19 classes. The following command was used to test both the interpreted and compiled versions of javac
    javac -verbose -classpath g:\java\lib\classes.zip JavaLex.java

Results using the 1.0.2 JDK:

[parsed JavaLex.java in 17030ms]
[loaded g:\java\lib\classes.zip(java/lang/Object.class) in 280ms]
[checking class CAccept]
[wrote CAccept.class]
[checking class CLexGen]
[loaded g:\java\lib\classes.zip(java/io/InputStream.class) in 60ms]
[loaded g:\java\lib\classes.zip(java/io/DataOutputStream.class) in 50ms]
[loaded g:\java\lib\classes.zip(java/util/Hashtable.class) in 110ms]
[loaded g:\java\lib\classes.zip(java/lang/String.class) in 160ms]
[loaded g:\java\lib\classes.zip(java/io/FileNotFoundException.class) in 0ms]
[loaded g:\java\lib\classes.zip(java/io/IOException.class) in 0ms]
[loaded g:\java\lib\classes.zip(java/lang/Exception.class) in 0ms]
[loaded g:\java\lib\classes.zip(java/io/FileInputStream.class) in 60ms]
[loaded g:\java\lib\classes.zip(java/io/FileDescriptor.class) in 0ms]
[loaded g:\java\lib\classes.zip(java/io/File.class) in 160ms]
[loaded g:\java\lib\classes.zip(java/lang/System.class) in 110ms]
[loaded g:\java\lib\classes.zip(java/io/PrintStream.class) in 50ms]
[loaded g:\java\lib\classes.zip(java/io/FilterOutputStream.class) in 0ms]
[loaded g:\java\lib\classes.zip(java/io/OutputStream.class) in 50ms]
[loaded g:\java\lib\classes.zip(java/io/FileOutputStream.class) in 60ms]
[loaded g:\java\lib\classes.zip(java/io/BufferedOutputStream.class) in 50ms]
[loaded g:\java\lib\classes.zip(java/util/Dictionary.class) in 60ms]
[loaded g:\java\lib\classes.zip(java/lang/Character.class) in 160ms]
[loaded g:\java\lib\classes.zip(java/lang/Integer.class) in 110ms]
[loaded g:\java\lib\classes.zip(java/lang/Number.class) in 0ms]
[loaded g:\java\lib\classes.zip(java/lang/Cloneable.class) in 0ms]
[loaded g:\java\lib\classes.zip(java/lang/Throwable.class) in 60ms]
[loaded g:\java\lib\classes.zip(java/io/DataOutput.class) in 0ms]
[loaded g:\java\lib\classes.zip(java/lang/StringBuffer.class) in 170ms]
[loaded g:\java\lib\classes.zip(java/util/Vector.class) in 110ms]
[loaded g:\java\lib\classes.zip(java/util/Enumeration.class) in 0ms]
[loaded g:\java\lib\classes.zip(java/util/BitSet.class) in 60ms]
[wrote CLexGen.class]
[checking class CEmit]
[wrote CEmit.class]
[checking class CNfaPair]
[wrote CNfaPair.class]
[checking class CAlloc]
[wrote CAlloc.class]
[checking class CSpec]
[wrote CSpec.class]
[checking class CDTrans]
[wrote CDTrans.class]
[checking class CError]
[loaded g:\java\lib\classes.zip(java/lang/Error.class) in 0ms]
[wrote CError.class]
[checking class CDfa]
[wrote CDfa.class]
[checking class CMakeNfa]
[wrote CMakeNfa.class]
[checking class CNfa]
[wrote CNfa.class]
[checking class CUtility]
[wrote CUtility.class]
[checking class JavaLex]
[wrote JavaLex.class]
[checking class CBunch]
[wrote CBunch.class]
[checking class CMinimize]
[wrote CMinimize.class]
[checking class CInput]
[wrote CInput.class]
[checking class CSet]
[wrote CSet.class]
[checking class CAcceptAnchor]
[wrote CAcceptAnchor.class]
[checking class CNfa2Dfa]
[loaded g:\java\lib\classes.zip(java/util/Stack.class) in 0ms]
[wrote CNfa2Dfa.class]
[done in 35970ms]
Results using JCC:
[parsed JavaLex.java in 1160ms]
[loaded g:\java\lib\classes.zip(java/lang/Object.class) in 0ms]
[checking class CNfa2Dfa]
[loaded g:\java\lib\classes.zip(java/lang/System.class) in 0ms]
[loaded g:\java\lib\classes.zip(java/util/Vector.class) in 0ms]
[loaded g:\java\lib\classes.zip(java/io/PrintStream.class) in 0ms]
[loaded g:\java\lib\classes.zip(java/lang/String.class) in 0ms]
[loaded g:\java\lib\classes.zip(java/io/FilterOutputStream.class) in 0ms]
[loaded g:\java\lib\classes.zip(java/io/OutputStream.class) in 0ms]
[loaded g:\java\lib\classes.zip(java/lang/Cloneable.class) in 0ms]
[loaded g:\java\lib\classes.zip(java/util/BitSet.class) in 0ms]
[loaded g:\java\lib\classes.zip(java/util/Stack.class) in 0ms]
[loaded g:\java\lib\classes.zip(java/util/Hashtable.class) in 0ms]
[loaded g:\java\lib\classes.zip(java/util/Dictionary.class) in 0ms]
[wrote CNfa2Dfa.class]
[checking class CError]
[loaded g:\java\lib\classes.zip(java/lang/Error.class) in 0ms]
[loaded g:\java\lib\classes.zip(java/lang/Throwable.class) in 0ms]
[loaded g:\java\lib\classes.zip(java/lang/StringBuffer.class) in 50ms]
[wrote CError.class]
[checking class CMakeNfa]
[loaded g:\java\lib\classes.zip(java/io/IOException.class) in 0ms]
[loaded g:\java\lib\classes.zip(java/lang/Exception.class) in 0ms]
[wrote CMakeNfa.class]
[checking class JavaLex]
[loaded g:\java\lib\classes.zip(java/io/FileNotFoundException.class) in 0ms]
[wrote JavaLex.class]
[checking class CMinimize]
[wrote CMinimize.class]
[checking class CBunch]
[wrote CBunch.class]
[checking class CSpec]
[loaded g:\java\lib\classes.zip(java/lang/Integer.class) in 60ms]
[loaded g:\java\lib\classes.zip(java/lang/Number.class) in 0ms]
[wrote CSpec.class]
[checking class CAcceptAnchor]
[wrote CAcceptAnchor.class]
[checking class CDfa]
[wrote CDfa.class]
[checking class CNfa]
[wrote CNfa.class]
[checking class CAlloc]
[wrote CAlloc.class]
[checking class CDTrans]
[wrote CDTrans.class]
[checking class CAccept]
[wrote CAccept.class]
[checking class CInput]
[loaded g:\java\lib\classes.zip(java/io/InputStream.class) in 170ms]
[wrote CInput.class]
[checking class CNfaPair]
[wrote CNfaPair.class]
[checking class CLexGen]
[loaded g:\java\lib\classes.zip(java/io/DataOutputStream.class) in 0ms]
[loaded g:\java\lib\classes.zip(java/io/FileInputStream.class) in 0ms]
[loaded g:\java\lib\classes.zip(java/io/FileDescriptor.class) in 0ms]
[loaded g:\java\lib\classes.zip(java/io/File.class) in 0ms]
[loaded g:\java\lib\classes.zip(java/io/FileOutputStream.class) in 0ms]
[loaded g:\java\lib\classes.zip(java/io/BufferedOutputStream.class) in 0ms]
[loaded g:\java\lib\classes.zip(java/lang/Character.class) in 0ms]
[loaded g:\java\lib\classes.zip(java/io/DataOutput.class) in 0ms]
[loaded g:\java\lib\classes.zip(java/util/Enumeration.class) in 0ms]
[wrote CLexGen.class]
[checking class CSet]
[wrote CSet.class]
[checking class CUtility]
[wrote CUtility.class]
[checking class CEmit]
[wrote CEmit.class]
[done in 3130ms]
It is interesting to note here that although the overall speed up was over eleven times, the parsing phase (which presumably does not involve any I/O) was 14.6 times faster.

Another test that involved a single class with 100 methods all containing the same code produced an overall speedup of nearly 14 times. This slightly contrived test was unusually CPU bound because only a few classes needed to be loaded. Some fine tuning of the garbage collection parameters was also involved that made the result about 22% faster than it would otherwise have been.

Results using the 1.0.2 JDK:

G:\jcc\Test>javac -verbose -classpath g:\java\lib\classes.zip Test.java
[parsed Test.java in 45580ms]
[loaded g:\java\lib\classes.zip(java/lang/Object.class) in 110ms]
[checking class Test]
[loaded g:\java\lib\classes.zip(java/lang/System.class) in 110ms]
[loaded g:\java\lib\classes.zip(java/io/PrintStream.class) in 110ms]
[loaded g:\java\lib\classes.zip(java/lang/String.class) in 170ms]
[loaded g:\java\lib\classes.zip(java/io/FilterOutputStream.class) in 0ms]
[loaded g:\java\lib\classes.zip(java/io/OutputStream.class) in 60ms]
[loaded g:\java\lib\classes.zip(java/lang/StringBuffer.class) in 1260ms]
[wrote Test.class]
[done in 185870ms]
Results using the JCC:
G:\jcc\Test>cjavac -verbose -classpath g:\java\lib\classes.zip Test.java
** GC_free_space_divisor = 2 **
** GC_expand_hp( 4000000 ) **
[parsed Test.java in 2910ms]
[loaded g:\java\lib\classes.zip(java/lang/Object.class) in 0ms]
[checking class Test]
[loaded g:\java\lib\classes.zip(java/lang/System.class) in 60ms]
[loaded g:\java\lib\classes.zip(java/io/PrintStream.class) in 0ms]
[loaded g:\java\lib\classes.zip(java/lang/String.class) in 50ms]
[loaded g:\java\lib\classes.zip(java/io/FilterOutputStream.class) in 0ms]
[loaded g:\java\lib\classes.zip(java/io/OutputStream.class) in 0ms]
[loaded g:\java\lib\classes.zip(java/lang/StringBuffer.class) in 0ms]
[wrote Test.class]
[done in 13510ms]
Note - In order to convert javac one needs the source code from Sun. This is free of charge, but you must sign a licence agreement. Because of the current incompatibility of floating point numbers the isInfinite() methods of java.lang.Float and java.lang.Double must be changed to simply return false. The next release should have this problem solved.

JavaLex

JavaLex is a very CPU intensive program and so is a good example of what JCC can do. The version running here is one I used to build JCC. It contains a few minor changes, the most important one here being the addition of output buffering.

In this case the converted program is running 22.36 times faster.

Results using the 1.0.2 JDK:

12:51:30.32 - G:\jcc\JavaLex>
12:51:30.38 - G:\jcc\JavaLex>java nshaylor.jlx.JavaLex Yylex.tmp
Processing first section -- user code.
Processing second section -- Java-Lex declarations.
Processing third section -- lexical rules.
Creating NFA machine representation.
NFA comprised of 1026 states.
Creating DFA transition table.
Working on DFA states...........................................................
................................................................................
................................................................................
................................................................................
................................................................................
..........................................................
Minimizing DFA transition table.
340 states after removal of redundant states.
Outputting lexical analyzer code.

13:00:11.84 - G:\jcc\JavaLex>
Results using JCC:
12:48:30.28 - G:\jcc\JavaLex>
12:48:30.39 - G:\jcc\JavaLex>javalex Yylex.tmp
Processing first section -- user code.
Processing second section -- Java-Lex declarations.
Processing third section -- lexical rules.
Creating NFA machine representation.
NFA comprised of 1026 states.
Creating DFA transition table.
Working on DFA states...........................................................
................................................................................
................................................................................
................................................................................
................................................................................
..........................................................
Minimizing DFA transition table.
340 states after removal of redundant states.
Outputting lexical analyzer code.

12:48:55.38 - G:\jcc\JavaLex>

Integer quicksort

Perhaps not the most realistic test, but certainly a dramatic one. This is a simple quicksort program for integers shows a speed improvement of 55 times.

Results using the 1.0.2 JDK:

10:57:00.35 - G:\jcc\quicksort>java nshaylor.jcc.tests.quicksort4.QuickSort 3000000
Sorting integers
Finished

11:09:53.26 - G:\jcc\quicksort>
Results using JCC:
10:54:50.34 - G:\jcc\quicksort>quicksort 3000000
Sorting integers
Finished

10:55:04.13 - G:\jcc\quicksort>


Changes

Version 0.2
  1. Allow D or d qualifier at the end of a floating point number.
  2. Correct obscure bug where static variables that were referred from local variables (not particularly good programming practice) sometimes got misinterpreted as instance variables.
  3. Correct bug where calling an inherited static method would result in the wrong code generation.
  4. Add -p option.
  5. Fix bug where statically declared arrays were badly formed if they were made of literal and non literal elements.
  6. Change object format.
  7. Move interface calls into general virtual function table.
  8. Fix several bugs where interface types in either the definition or invocation of a function were incorrectly handled.
  9. Add support for Class.newInstance().
  10. Add support for Class.getInterfaces() and improve the way interface definitions are handled.
  11. Fix bug where label was missing for labelled continue statement.
  12. Add thread support.


Conclusions

Because JCC uses C as an intermediate stage in the build process, it has the potential to be used with any computer system that supports the C language. This works well because it can take advantage of all the optimisations employed by today's established C compilers.

There are many cases where building Java programs in this way is inappropriate, or indeed impossible, but as the language becomes popular there will probably be a growing body of software where performance or program size becomes an issue. I do not see programs like JCC changing greatly the way most Java software is run, but rather extending the use of the language into areas where in cannot currently be used.

I can see no reason why an operating system device driver could not be built with JCC using some custom native methods. Such a project would only require two issues to be addressed: the extra memory required for object headers, and the cost of garbage collection. Modern garbage collection techniques are very sophisticated and often impose a very small performance overhead, but if this proved to be too high, an option might exist to design the program not to generate any garbage. This would mean the program managing its use of memory manually. It could be argued this represents the loss of a very important feature, but it may be an acceptable compromise in such a case. The object header overhead is only eight bytes plus whatever the garbage management software requires.

Software for imbedded microprocessor systems can be subject to similar restrictions in space and time. The programs produced by JCC are fast, small, uncomplicated in there requirements, and easily interfaces to other software written in C. There is also no reason in principal why ROMable 16 bit code could not also be produced.


Notice

This is an early version of the program which is intended for evaluation purposes only. Further development is needed for it to become a mature system.

Thanks to Hans-Juergen Boehm for his kind advice, and Jonathan Souster for his help with the run time library.

Please report any bugs to me, Nik Shaylor, at nshaylor@tcp.co.uk You are visitor: Counter Image


Java is a trademark of Sun Microsystems Inc.
Visual C++ is a trademark of Microsoft Corporation