Improving J2ME with the C Pre-Processor
At Duo Games, we've worked out a system that will assist us in coming up with portable code quicky, and help to overcome some of the shortcomings in Java J2ME and its various implementations. By using techniques well known to C programmers, we have a system that allows us to target multiple MIDP devices from the same code base. It's outlined in this page.
Shortcomings? What are you on about?
One of the slogans that used to be applied to Java was "write once, run anywhere". It was a nice idea, and when Sun was responsible for writing the Java Virtual Machine (JVM) and all the supporting classes, it was almost achievable, too. Fast-forward to today, with J2ME, and and we find that there are many different implementations, each with their own interpretations of (and some with extensions to) the J2ME standards.
Some companies, like Nokia, tried to work round the inadequacies of MIDP 1.0 by extending the class hierarchies with their own classes (Nokia have the Nokia UI API). In other systems, the delivery of events may be different from what you'd expect. In still others, there are bugs that cause the entire phone to reset for no apparent reason.
As game developers, we want to work around these differences as automatically as we can. But by their nature, the platforms that run J2ME are limited. Their processors may not be very fast; their RAM may be small; their maximum JAR size might be restricted. Additionally, in MIDP 1.0, at least, there's no way of determining what phone you're running on, and what workarounds you need to carry out. Even if we could, adding code that's going to be executed by every phone even when it's unnecessary will slow down execution and push the limits of the file sizes.
For these reasons, once we've discovered the different workarounds needed, we want to take the load off the mobile devices and put it on our development workstations. To accomplish this, we use the C Pre-Processor (CPP).
A Quick Backgrounder on CPP
The C Pre-Processor is the first phase of compilation of a C (or C++) file. It has 3 main jobs:
- remove comments
- import "header files" (essentially C files containing declarations) into C code
- perform macro substitutions within the code
Removal of comments is uninteresting to us, however the other two are useful.
There are some perceived problems with the use of (and the need for) the C pre-processor in C and, as a result, its use in C++ is diminished somewhat (macro substitutions are generally frowned upon and considered unnecessary, although it's still used for importing header files) and, of course, Java does away with it entirely.
CPP reads in a source file line-by-line, processes any directives it finds and then outputs the results to an intermediate file (again, line-by line). The CPP directives we're interested in are #define (and, in conjunction with that, #ifdef, #else and #endif) and #include. Let's imagine a hypothetical C file - we'll call it main.c:
#include <stdio.h>
#include "myheader.h"
int main (void)
{
int i;
for (i = 0; i < NUM_ITERATIONS; i++)
{
printf("iteration %u - result %u\n",i,my_function(i));
}
return 0;
}
together with myheader.h:
#define NUM_ITERATIONS 100 extern int my_function(int);
When CPP reads main.c, it sees the #include directives for <stdio.h> (a standard header, which is where the angle brackets come in) and "myheader.h" and determines that it needs to include the headers. It does this by pausing the reading of main.c and opening up stdio.h and repeating the pre-proccessing on that file. It may #include further headers. Once pre-processing has finished with stdio.h, the parsing returns to main.c, where the next line includes myheader.h. The process is repeated for that header, too.
When CPP sees the line:
#define NUM_ITERATIONS 100
it determines that it should create a macro symbol with name NUM_ITERATIONS and value 100. This is a
string value 100, and CPP has no concept of it being a number. From then on, any occurrences of
the token NUM_ITERATIONS (there's one in the for loop) will be
directly replaced with the string 100.
When the whole of main.c has been pre-processed, there will be an intermediate file, perhaps called main.i, containing the merged contents of stdio.h, myheader.h and main.c. It's this intermediate file that's passed to the compiler proper.
So why would I care about this for Java?
As I mentioned above, there are some other directives which can be used in conjunction with #define - #ifdef, #else and #endif - which allow us to do things at compile-time, based on the definition of a macro token. Consider the following block of code:
import javax.microedition.lcdui.*;
#ifdef NOKIA_UI_API
import com.nokia.mid.ui.*;
#endif
class MyCanvas
#ifdef NOKIA_UI_API
extends FullCanvas
#else
extends Canvas
#endif
What this is telling the C preprocessor is that, if NOKIA_UI_API is defined, it should write
into the intermediate file the code to import the package com.nokia.mid.ui.*
and to derive MyCanvas from Nokia's FullCanvas class. If NOKIA_UI_API isn't defined, then
the code output should not import the Nokia classes and should derive MyCanvas from the
regular MIDP Canvas class.
Another useful thing you can do is to create a header file containing a bunch of #define directives which define constants, and then #include that into your java file. A good reason to do that is if you have constants being shared across multiple classes, and you don't want the classes to have to know about what they're sharing, or where the constants are defined. Here, they're not really defined as constants anywhere - they're just numbers when it gets as far as the java compiler.
How do I implement such a system in my J2ME project?
You may be able to implement a similar system into your IDE of choice, but we use GNU make.
We started by renaming all our source files, from xxx.java to xxx.jav. The reason for that is that javac, the java compiler, likes its input source files to have an extension of .java. Since we're passing our original sources through CPP and it writes data out to the "intermediate" file at the same time as reading in the original source file, it's not possible to have the input and output files called the same thing. You could do some other name mangling, or perhaps output to a different directory. We chose the extension route.
We implemented a Makefile rule to convert from a .jav file to a .java file, by passing it through CPP. The one from Balls! looks like this:
CPPFLAGS = -P $(NOKIA)
.
.
.
$(SRCPATH)/%.java: $(SRCPATH)/%.jav
$(CPP) $(CPPFLAGS) $< $@
Fairly simple stuff, really. The first line says that an identifier called CPPFLAGS should contain
the switch -P (do not generate #line directives) and whatever the environment variable NOKIA is set
to. When I'm doing a build for Nokia phones, I set this environment variable like so:
> set NOKIA=-DNOKIA_UP_API
and when I'm doing a generic MIDP build, I set it to nothing:
> set NOKIA=
When make picks it up, it either plugs in -DNOKIA_UI_API (define NOKIA_UI_API as a macro) or not.
The make rule says that in order to generate a .java file from a .jav one, it should run CPP with
whatever the value of CPPFLAGS is, on the input filename, and produce the output filename.
What next?
Do you write your games for platforms other than J2ME? I don't know about your games, but ours typically consist of an engine that handles the game logic and some peripheral stuff that handles the rendering, key input, game lifecycle, etc.
We're working on an intermediate language, again based on CPP, that allows us to port this game logic code easily between Java for J2ME and C++ for Mophun and Symbian OS developments. Details will appear in Part 2.