You are on page 1of 12

A Free Java Virtual Machine for Embedded Systems

by Michael Barr Embedded Systems Conference San Jose, CA November 1-5, 1998 Course #308

In order to execute Java programs in an embedded system, you must integrate a Java runtime environment into your software. Although several commercial runtime environments are now for sale or in beta release, a less expensive and more widely available option is the freeware Kaffe Virtual Machine. In this presentation, we'll discuss the hardware and software requirements of Kaffe and the work involved in porting it to an embedded platform. Along the way, we'll also discuss what to look for in a commercial Java runtime environment, should you decide to go that route instead. As you probably already know, Java is an easy-to-use, object-oriented programming language designed for platform-independence and protection from common programming mistakes. These and other characteristics help make programming in Java a downright pleasure. But the behind-the-scenes work necessary to support the language at runtime is a lot more than for traditional high-level languages like C, or even C++. For instance, Java is an interpreted language, which means that the equivalent of a runtime compile cycle must be executed on the target processor. Other features that require significant runtime support are garbage collection, dynamic linking, and exception handling. JAVA USAGE MODELS It is currently unrealistic to consider implementing an entire embedded software project in Java. For one thing, Java does not include a mechanism for directly accessing memory or hardware registers. So there will always be a need for device drivers and other pieces of supporting software written in C/C++ or assembly. This other software might either be called from Javain which case it is said to be a native methodor run as a separate thread of execution, in parallel with the Java runtime environment. Before preparing your system for Java, it is important to think about how the Java programs you write will fit into your overall architecture. Many Java usage models have been proposed for embedded systems, but it seems that each of them falls into one of four categories: No Java, Embedded Web Server Java, Embedded Applet Java, or Application Java. These four usage models are distinguished by two binary variables: (1) the location of the stored Java bytecodes and (2) the processor on which they are executed. Each of these variables can take one of two values: target (the embedded system) or host (a computer attached to the embedded system). For example, the category No Java includes all scenarios in which the bytecodes are stored on and executed by a host computer; although Java is used in this scenario, it is never actually on the embedded system. All four usage models are illustrated in Figure 1.

Host
Java

Host

Host
Java

Host

Target

Target
Java

Target

Target
Java

No Java

Embedded Web Server

Embedded Applet

Application

Figure 1. The Four Java Usage Models for Embedded Systems In the Embedded Web Server usage model the Java bytecodes are stored on the target system (usually in Flash memory or ROM), but executed by the host processor. This model is useful for networked embedded systems that require a graphical interface. A Java-enabled web browserrunning on the host workstationexecutes a set of Java bytecodes uploaded from the embedded system. In addition to the Java bytecodes, the embedded system in this scenario must store at least one HTML file and execute a piece of software called an embedded web server. However, since Java is not actually executed on the embedded system, a Java runtime environment is not required there. The third and fourth usage models are the most interesting from the viewpoint of this discussion. These are the ones in which Java bytecodes are actually executed on the target processor and for which an embedded Java runtime environment is, therefore, required. In the Embedded Applet scenario, the Java bytecodes are stored on the host workstation and sent to the embedded system over a network. The embedded system executes the bytecodes and sends the results back to the host. Embedded applets could be used to implement network management functionality (as a replacement for SNMP, for example) or to off-load computations from one processor to another. In the Application model, Java comprises some or all of the actual embedded software. The Java bytecodes are stored in a nonvolatile memory device and executed by the Java runtime environment in much the same way that native machine code is fetched and executed by the processor itself. This use of Java is most similar to the way C and C++ are used in embedded systems todayto implement large pieces of the overall software. However, because Java lacks the ability to directly access hardware, it may still be necessary to rely on native methods written in C or C++. This is not unlike the way C

programmers use assembly-language to perform processor-specific tasks. JAVA RUNTIME ENVIRONMENTS A typical Java runtime environment for embedded systems contains the following components: A Java Virtual Machine to translate Javas platform-independent bytecodes into the native machine code of the target processor and to perform dynamic class loading. This can take the form of either an interpreter or a just-in-time compiler (JIT). The only real difference between the two is the speed with which the bytecodes are executed; a JIT is faster because it avoids reinterpreting previously executed sections of the program. A standard set of Java class libraries, in bytecode form. If your application doesnt reference any of these classes, they are not strictly required. However, most Java runtime environments are likely to conform to one of Suns standard APIs, such as PersonalJava or EmbeddedJava. Any native methods required by the class libraries or virtual machine. These are functions that are written in some other language, precompiled, and linked with the Java virtual machine. They are primarily required to perform functions that are either platform-specific or unable to be implemented directly in Java. A multitasking operating system to provide the underlying implementation of Javas threading and thread synchronization mechanisms. A garbage collection thread. The garbage collector runs periodicallyor whenever the available pool of dynamic memory is unable to satisfy an allocation requestto reclaim memory that has been allocated but is no longer being used by the application.

Class Libraries

Java Threads

C/C++ Tasks

Java Virtual Machine (with Garbage Collector) Native Methods Multitasking Operating System Processor and Other Hardware Figure 2. The Components of a Java Runtime Environment

The relationship of these components to the other software and hardware that makes up a typical embedded system is illustrated in Figure 2. A dotted line surrounds the components of the Java runtime environment. KAFFE Kaffe is a freeware Java runtime environment that can be downloaded from http://www.kaffe.org. The virtual machine (source code for both an interpreter and a JIT are included), garbage collector, and native methods that comprise Kaffe are themselves written in C and assembly. So, although Kaffe was not written with embedded systems in mind, it is possible to port it to just about any computer platform for which there exists an ANSI-compatible C compiler. Kaffes list of currently supported processors reads like a whos who of the 32-bit world: 386/486/Pentium, SPARC, Alpha, PowerPC, 68k, and MIPS. These are many of the same processors supported by the GNU C Compiler (GCC supports several others). If your embedded processor is from one of these families, your Kaffe port should be pretty simple. Otherwise, a bit more effort will be required to get Kaffe up and running. As for memory, a typical combination of the interpreter, garbage collector, and native methods requires less than 100-kbytes of code space. Add to that the size of your application and any class libraries it requires (both stored as Java bytecodes) to calculate the overall ROM requirements for your Java program. Youll also need a large heap for dynamic memory allocation. The precise amount of heap space youll need is dependent upon your application. However, a good rule of thumb is that you shouldnt try to use Java in a system with less than 1-Mbyte of RAM. Kaffe can be used with or without an operating system, a feature that is somewhat unique among Java virtual machines. This is possible because Kaffe contains its own internal threads implementation that requires very little support from the underlying software environment. By default, it uses this package to create and multitask Java threads. THE PORTING PROCESS It would be impossible to give detailed, step-by-step instructions for porting Kaffe to any and every embedded system imaginable. So I will attempt only to provide an overview of the steps involved, the details of which are taken from the latest release of Kaffe, version 1.0.0. By following these directions, it should be possible for an embedded software engineer to complete a Kaffe port within about a monthlonger if the JIT needs to be ported to a new processor and/or it is desired to integrate Kaffe with an RTOS.

REAL-TIME JAVA As you may be aware, Java is not ideally suited for use in real-time systems. Functions written in Java execute more slowly and less deterministically than similar functions written in C or C++. However, that does not mean you should avoid Java altogether in such systems. When writing Java programs, it is common practice to partition ones software into multiple threads of execution. These threads are analogous to the tasks provided by a real-time operating system. And, in fact, many Java virtual machines rely on the underlying operating system to make scheduling decisions and perform context switching. So, it is not unreasonable to consider partitioning an embedded application into a set of Java threads and a set of real-time tasks, the latter written in C/C++ or assembly. By setting the task priorities such that all of the Java threads (including the garbage collector) have a lower priority than the least-important of the real-time tasks, it is possible to benefit from Java without risking a missed deadline. Because Java is such a nice language for programmers, a lot of work is being done to make it more suitable for real-time software development. This includes more deterministic garbage collection algorithms and language extensions to allow direct implementation of real-time tasks.

After downloading Kaffe and unarchiving the tared and gziped file, you will see that the source code is organized into the following major subdirectories: kaffe - platform-independent parts of the interpreter and JIT and source code for the garbage collector, dynamic class loader and other pieces of the Java runtime environment. config - platform-dependent parts of the interpreter and JIT. This is organized into a set of subdirectories for supported processors, with operating system-specific directories below those. libraries - the Java class libraries and native methods they depend on. Only the native methods are provided in the standard Kaffe distribution. For various legal reasons, the actual class libraries must be obtained from Sun Microsystems or another vendor. include - interface definitions for the native methods provided in the previous directory. Some of this information is required by the platform-independent files.

The partitioning of the Kaffe source code into platform-independent and platformdependent subdirectories is intended to simplify the process of porting it to new

platforms. In most cases, only files in the config directory require modification. However, embedded systems differ somewhat from other computers in that they only rarely have filesystems or network connections. So we will see that there may be changes required in the platform-independent code as well. kaffe-1.0.0 config alpha ... sparc include kaffe main.c kaffe kaffevm intrp jit libraries

Figure 3. The Organization of the Kaffe Source Code Supporting Software Like most other software written in C, Kaffe depends on routines from the standard C library and its accompanying math library. However, this software is not always available for an embedded platform. If it is not available for yours, you will need to either provide it yourself or modify the appropriate Kaffe source code. Another option is to obtain the newlib package from Cygnus. This version of the C library is provided in source code form (under the GNU Public License) and designed specifically for portability to embedded systems. The majority of the functions in the standard C library will compile easily for all embedded systems. These are the functions like memcpy, strcmp, and atoi that we rely on regularly. However, some of the other library routines on which Kaffe depends must be ported to your specific target platform. Here is a list of the supporting software that Kaffe requires: Dynamic memory allocation. Although Java programmers never call malloc and free, the Kaffe virtual machine does use these memory allocation routines to request large blocks of memory from the underlying software. Signals. Kaffe relies on a POSIX-compliant signals implementation to perform the equivalent of software interrupts. These are used to awake sleeping threads and handle exceptions as they occur. Setjmp/Longjmp. Kaffe's internal threads package is written in C and relies heavily on the functions setjmp and longjmp to perform its context switches. In addition,

Kaffe won't know the specifics of your processor's context without details from you. Bytecode Interpreter Kaffes bytecode interpreter is an incredible piece of software. Rather than mapping Java bytecodes to blocks of processor-dependent assembly code, the authors of Kaffe have cleverly implemented each bytecode in C. As a result, not a single line of the interpreter source code is processor-specific. This makes porting the basic (non-JIT) virtual machine easy: simply use your cross-compiler to build the files in the directory kaffe/kaffevm. When you compile the files in this directory, you will also be building the garbage collector, dynamic class loader, and other parts of the Java runtime environment that are either independent of the processor or rely on the functions in the processor-dependent parts of the Kaffe source code. At this point, be sure to compile and link the contents of the kaffevm/intrp directory rather than those in kaffevm/jit. Internal Threads As I stated earlier, Kaffe has its own internal threads package. In other words, it maintains its own thread-specific data structures and performs scheduling and context switching at the appropriate times. This functionality is separate from and invisible to the underlying operating system (if any). In older versions of Kaffe, this internal threads package was written in assembly language and located in the config subdirectory. However, more recent implementations use the C library's setjmp and longjmp functions to perform the actual context switch in a processor-independent manner. The following constants can be used to alter the behavior of the internal threads package. These should be defined in the file config/<processor>/threads.h. USE_INTERNAL_THREADS should be defined to enable use of the internal threads package. THREADSTACKSIZE is a constant that sets the size of each threads stack, in bytes.

Step 4: Dynamic Class Loader One part of Kaffe's platform-independent source code that must usually be modified for use in an embedded system is the dynamic class loader. This is the part of the Java runtime environment that is responsible for loading classes and methods as they are accessed. In more traditional computing environments, the bytecodes associated with each class and method are stored in .class files. The dynamic class loader searches the directories and files in the CLASSPATH for a class or method of the given name. Unfortunately, very few embedded systems have filesystems, so the class loader

must be modified to search for classes and methods stored in memory (either RAM or ROM will do) instead. You basically have two options at this point. One is to create a filesystem-like structure (a RAMdisk, if you like) in memory and keep the dynamic class loader code unchanged. The other is to zip up all of the class files and turn the resulting data into a memory image. This image could then be placed at a particular memory location and a pointer to it provided to the dynamic class loader. Whatever you decide, most of the functions that you'll need to look at reside in the two files classMethod.c and lookup.c in the kaffe/kaffevm directory. Just-in-Time Compiler If Kaffes JIT has been ported to your processor, you may want to consider using it instead of the bytecode interpreter. To do so, rebuild the contents of kaffe/kaffevm, this time compiling and linking the files in kaffevm/jit rather than kaffevm/intrp. Note that many of these files depend on other files in the config/<processor> directory. If your processor is not currently supported by Kaffes JIT, Id recommend that you stick with the interpreter. A port of Kaffes JIT will probably require significant effort on your part and is probably better left in the hands of an experienced compiler writerparticularly if you are concerned about performance. If you should decide to attempt a port of the JIT to a new processor, take a look at the files in the processorspecific directories under config. The implementation for SPARC processors is particularly well documented. Native Threads (Optional) By default, Kaffe relies on its own internal threading mechanisms to initialize, track, and schedule each of the threads within a Java application. Kaffe accomplishes this by creating thread data structures that are separate from, and invisible to, the underlying multitasking operating system (if one is present at all). In other words, the Kaffe virtual machine is itself a task that subdivides its execution time and gives each slice to one of the Java threads. Figure 4 illustrates the relationship of Kaffes threads to the tasks of an underlying operating system.
Task A Operating System Task B Virtual Machine Thread 1 Thread 2 Garbage Collector

Figure 4. Kaffe Threads vs. Operating System Tasks

Some other Java runtime environments allow the underlying operating system to create and control their threads directly. This type of an implementation is said to use native threads, because the Java threads are each tasks managed by the native operating system. In this case, the Java runtime environment is itself broken up into several tasks: one for the garbage collector and one for each application thread. This allows Java threads to compete more fairly for execution time on the target processor. If you have a thorough understanding of your operating systems threading API, it is possible to have Kaffe use native threads instead. In fact, the 1.0.0 release of Kaffe includes a native threads interface that can be easily ported to new operating system APIs. You don't have to modify anything but the native thread interface routines in the directory kaffe/kaffevm/systems. Virtual Machine Startup As distributed, Kaffe expects to be compiled for a DOS or UNIX-like operating system and invoked from a command lineusually with a parameter telling it which Java class should be executed first. But we want to use Kaffe in an embedded system without a command line, so we need a less dynamic way to start the virtual machine and a mechanism to pass the name of the startup class to it. The initialization can be accomplished with a call to the routine initialiseKaffe. This will start the dynamic class loader, virtual machine, and garbage collector and would typically be done from within the embedded software's main. Once the Kaffe runtime environment has been initialized, it is ready to execute Java bytecodes. However, it will not yet know which class to execute. You must provide that information, by calling the routine do_execute_java_class_method. This routine implicitly calls the dynamic class loader, which will locate the actual bytecodes. In addition, a new thread will be automatically created for their execution. This call could be made from main or at a later time, possibly in response to a network request to execute an embedded applet. COMMERCIAL ALTERNATIVES If you want to integrate Java into your embedded environment, Kaffe is not your only option. A number of real-time operating system vendors are now offering complete Java runtime environments built upon their proprietary kernels. These packages have already been ported and are basically turn-key solutions. Some of the most prominent such vendors are Microware, Wind River Systems, and Accelerated Technology. When deciding if a commercial package is the right solution for you, consider the following advantages and disadvantages compared with Kaffe. The first advantage is that of shorter time-to-market; a prepackaged Java runtime environment may reduce your startup time by several weeks. In addition, if you run into any problems or bugs, technical support is just a phone call away. Another advantage is that most third-party

Java runtime environments use the native threads of the underlying RTOS; this is a more robust and efficient implementation than the internal threads package included with Kaffe. There are also several reasons why a third-party solution may not be right for you. The most prominent disadvantage is cost. Not only are most commercial Java runtime environments expensive to license, but you may also be required to pay royalties to both the RTOS vendor and Sun Microsystems. In addition, third-party vendors often do not (or cannot, legally) provide the source code for their virtual machines. So you probably wont be able to muck around with the internals or tailor the performance of their virtual machine to your application. The third issueand its not clear if this is a disadvantage or merely an asideis compatibility. If youre currently using a home-grown operating system, or one for which commercial Java support is unavailable, you may have no choice but to use Kaffe. If you do decide to purchase a commercial Java runtime environment, here are some of the things you should look for: Support for native threads on the operating system API(s) of your choice. Compatibility with the latest release of Suns Java Development Kit (JDK). The ability to load class files directly from ROM. Beware of implementations that require a filesystem, as these may not be compatible with your target platform. A JIT that is supported on your processor. Unfortunately, none of the commercial vendors seem to have JITs as of this writing. Hopefully this situation will change in the near future. A modular design that can be scaled to balance the needs of your application with the constraints of your hardware.

JAVA CLASS LIBRARIES As stated earlier, a Java runtime environment includes a set of standard class libraries. But these Java classes are not strictly required unless your application actually uses them. In that sense, they are very similar to the standard C libraries. For example, if youve ever used strcmp or strlen in an embedded program, you were relying on the standard C library to be linked with your application. Similarly, if you want to manipulate strings in Java, you will need a class library called java.lang in your runtime environment. In order to promote and encourage the Write Once, Run Anywhere nature of Java, Sun has defined several standard groups of class libraries. Sun refers to these standard APIs as Java Application Environments. And so far, at least four such standards have been declared:

That said, Id like to reiterate that the minimum system requirements for accomplishing something useful with Java are currently a 32-bit processor, 1-Mbyte of RAM, and a similar amount of ROM. While it may be possible to port Kaffe to a 16-bit processor and/or a system with less memory, I dont know of anyone who has yet done that successfully. But perhaps if you wait a little while longer this too will seem like old news.

CoreJava - the full set of class libraries included in Suns JDK. These classes are appropriate for desktop workstations and servers and may require significant hardware and operating system resources. PersonalJava - a (not-quite proper) subset of the CoreJava API that is appropriate for set-top boxes, PDAs, network computers, and other networked embedded systems with a fairly large amount of processing power and memory. EmbeddedJava - a subset of the PersonalJava API that is better suited to the resourceconstrained environments typically found in non-networked and relatively inexpensive embedded devices. JavaCard - a specification for the use of Java in smart cards and other systems with very small amounts of memory. Sun claims that a JavaCard-compliant runtime environment can be created in systems with as little as 16-kbytes of ROM, 8-kbytes of EEPROM, and 256 bytes of RAM! Of course, that doesn't include any class libraries at all.

The intention of these standard APIs is to allow application developers to easily specify the type of platform on which their Java program will run. For example, a program written for use in a PersonalJava-compatible set-top box could also be run on a network computer or PDA. READY, SET, GO A lot of things about Java have changed in the last two years, and it now seems reasonable to start trying it out in embedded systems. If you have some time available, I highly recommend that you download the Kaffe source code and Suns JDK and start playing with themeven if you dont intend to actually port Kaffe to your embedded platform. There can be no substitute for first-hand experience, and the things you learn will no doubt help you make more informed decisions regarding your use of Java in future projects.

You might also like