JNI Case Study: Java Launcher

Every Java developer who uses the Sun SDK is grateful for the C program known as
the launcher (its executable name is "java"), which uses the JNI Invocation API
to create a JVM, load a Java class into the JVM, and call its main() method, 
thereby launching a Java application on behalf of the caller. 

The launcher's source code is available from Sun and is packaged with the SDK 
(version 1.4.2_04, which can be downloaded from java.sun.com/j2se/1.4.2/download.html). 
In the base directory of the installed SDK is a file called src.zip. 
If you extract that file, the exploded "launcher" directory contains the four 
source files that constitute the launcher program: java.h, java.c, java_md.h, 
and java_md.c. The launcher's source code is unmistakably C: murky, idiomatic, 
and circuitous. On the other hand, the end result is a functional program 
that has been run successfully innumerable times by innumerable users. 
Understanding how it works is a case study in the use of JNI.

Suppose there is a class called Hi in the package com.mike that has a public 
main() method and thus can be started as a Java application through the launcher. 
The following is the source code:


1	package com.mike;
2	
3	Public class Hi
4	{
5	   public static void main(String args[])
6	   {
7	      ...
8	   }
9	}

The Hi application is started under SDK 1.4.2_04 on Windows 2000 with 
the following commands:

1 set JAVA_HOME=c:\j2sdk1.4.2_04
2 set _JAVA_LAUNCHER_DEBUG=true
3 %JAVA_HOME%\bin\java -classpath src -Xms32m -Xmx64m -Dmy.property=1 com.mike.Hi arg1 arg2

Line 3 calls the launcher executable Java in the bin directory of my SDK 
(which, as line 1 indicates, is c:\j2sdk1.4.2_04). 
The arguments passed to the launcher are:

    * -classpath src: Look for my "Hi" class in the directory src.
    * -Xms32m: Set its minimum heap size to 32MB.
    * -Xmx64m: Set the maximum heap size to 64MB.
    * -Dmy.property=1: Make a property available to the application with the key my.property and value 1.
    * com.mike.Hi: Run the application in this class.
    * arg1 arg2: Pass arguments "arg1" and "arg2" to the application's main method. 

Line 2 sets an environment variable called _JAVA_LAUNCHER_DEBUG, which causes 
the launcher to generate debugging output to the console at runtime:


1	----_JAVA_LAUNCHER_DEBUG----
2	JRE path is c:\j2sdk1.4.2_04\jre
3	jvm.cfg[0] = ->-client<-
4	jvm.cfg[1] = ->-server<-
5	jvm.cfg[2] = ->-hotspot<-
6	jvm.cfg[3] = ->-classic<-
7	jvm.cfg[4] = ->-native<-
8	jvm.cfg[5] = ->-green<-
9	1306 micro seconds to parse jvm.cfg
10	JVM path is c:\j2sdk1.4.2_04\jre\bin\client\jvm.dll
11	5571 micro seconds to LoadJavaVM
12	JavaVM args:
13	    version 0x00010002, ignoreUnrecognized is JNI_FALSE, nOptions is 6
14	    option[ 0] = '-Djava.class.path=.'
15	    option[ 1] = '-Djava.class.path=src'
16	    option[ 2] = '-Xms32m'
17	    option[ 3] = '-Xmx64m'
18	    option[ 4] = '-Dmy.property=1'
19	    option[ 5] = '-Dsun.java.command=com.mike.Hi arg1 arg2'
20	125113 micro seconds to InitializeJVM
21	Main-Class is 'com.mike.Hi'
22	Apps' argc is 2
23	    argv[ 0] = 'arg1'
24	    argv[ 1] = 'arg2'
25	32937 micro seconds to load main class
26	----_JAVA_LAUNCHER_DEBUG----

The launcher begins by finding the JRE (line 2) and the right JVM (lines 3-10), 
and then loads the JVM dynamically (line 11); as we'll see, this logic is 
platform dependent. In this case, because we didn't name a specific JVM when calling the launcher, the launcher defaults to "client" (more on this below). Lines 12-19 show the arguments that the launcher will pass to the JVM; these correspond to the arguments passed to the launcher. In line 20, the launcher starts the JVM. The class whose main method that launcher will call is given on line 21; the arguments passed to it are shown in lines 22-24.

The launcher's logical design (as of version 1.4.2_04) is depicted in Figure 4.

The launcher consists of two modules: java.c, which contains the main function of the launcher program as well as platform-independent helper functions, and java_md.c, which houses platform-specific functions. The modules share a bidirectional dependency: func-tions in java.c call java_md.c and vice versa; for example, main() in java.c calls CreateExecutionEnvironment() in java_md.c, which in turn calls ReadKnownVMs() in java.c.

The source code in java_md.c is different for each SDK platform release. For example, the Windows SDK has the Windows version of java_md.c but does not have the Solaris version. If you want to see both (as I did as I was writing this article), you must download both releases.

The main steps in the launcher's processing are the following:

1.  Create the execution environment: The CreateExecutionEnvironment() function, implemented in java_md.c, is a platform-specific search for the JRE path, JVM path, and JVM type for use by the launcher. The Windows version looks up the JRE path in the registry (on my machine, the registry key HKEY_LOCAL_MACHINE\Software\JavaSoft\Java Runtime Edition\1.4\JavaHome is C:\Program Files\Java\j2re1.4.2_04), and then checks whether the JVM type selected by the caller (corresponding to "-client" or "-server" launcher command-line options) is one of the allowable types listed in the file JREPath\lib\Arch\jvm.cfg (on my machine "Arch" is "i386"). If the caller did not specify a JVM type, the launcher defaults to the first type listed in jvm.cfg (on my machine it's "client"). The JVM path on Windows is JREPath\bin\JVMType\jvm.dll (e.g., JREPath\bin\client\jvm.dll for the client JVM).

2.  Load the JVM dynamically: Whereas most programs let the operating system implicitly link shared libraries to their processes, the launcher, which allows the user to specify at runtime which version of the JVM library to use (the "-client" or "-server" command-line arguments to the launcher), explicitly loads the JVM library using a platform-specific interface. The logic resides in java_md.c's LoadJavaVM() function. On Windows, this function calls the Win32 LoadLibrary() to load the JVM DLL and link it to the launcher process, and then calls the Win32 GetProcAddress() function to get a pointer to the invocation API function JNI_CreateJavaVM() used in step 4.

3.  Prepare JVM runtime options based on command-line options passed to the launcher: Command-line options such as -D, -X, and -classpath are assembled into an array to be passed to the JVM. The launcher adds an additional property for use by the JVM of the form -Djava.sun.command=class arg1 arg2 ..., where class is the fully qualified name of the target class and "arg1 arg2 ..." is the list of command-line arguments to be passed to its main method.

4.  Create the JVM: The launcher calls the JVM's JNI_Create-JavaVM() function, passing the options prepared above.

5.  Load the target class into the JVM by calling the JNI FindClass() method: The launcher first replaces dots with slashes in the class name (e.g., it converts com.mike.Hi to com/mike/Hi) because the JVM expects slashes instead of dots.

6.  Call the main method of the target class: First, the launcher gets a reference to the main() method by calling the JNI GetStaticMethodID() function, passing the class reference acquired in step 5, the method name ("main"), and the signature ("([Ljava/lang/String;)V", the JVM's peculiar representation of a void method that accepts an array of java.lang.String objects). Second, the launcher calls the method via CallStaticVoidMethod(). The launcher prepares the string array method input using some of the JNI's array functions (NewObjectArray(), SetObjectArrayElement()), and handles exceptions in the main method using JNI's ExceptionOccurred(), ExceptionDescribe(), and ExceptionClear().

7.  Shutdown the JVM: This is done by calling the JNI DetachCurrent-Thread() and DestroyJavaVM() functions.

A mystery to many Java developers, the launcher is nothing more than a little C program that uses the JNI to initiate a Java application. 