One of the interesting things I’ve learnt about the Java is that, however much under the illusion that 'java' (or 'java.exe' for windows) is perceived as the JVM itself, the actual fact, is that it is actually not, but rather a very thin front-end for the JVM. The actual code that provides the functioning core of the JVM actually resides in the library (like 'libjvm.so' or 'jvm.dll'), and that the 'java' executable is just a thin veneer on top of the virtual machine.

To demonstrate that this is the case, I’ll write a custom loader that invokes the JVM to load a simple Java class file. The code for the simple class file is as follows:

/**  Hello world app. */
public class HelloWorld {
public static void main(String args[]) {
System.out.println("Hello World");
}

public static void execute() {
System.out.println("Executed from launcher!");
}
}


The details to the custom loader are documented in Java’s Invocation API, which is provided at the end of this article. The code for the loader is down to the bare minimum just for the example to work:

#include <stdlib.h>
#include <stdio.h>
#include <jni.h>

/* This is the program's "main" routine. */
int main (int argc, char *argv[]) {

JavaVM *jvm;                    /* denotes a Java VM */
JNIEnv *env;                    /* pointer to native method interface */
JavaVMInitArgs vm_args;
JavaVMOption options[1];

jint res;
jclass cls;
jmethodID mid;

/* IMPORTANT: need to specify vm_args version especially if you are not using JDK1.1.
*            Otherwise, will the compiler will revert to using the 'JDK1_1InitArgs' struct.
*/
vm_args.version = JNI_VERSION_1_4;

/* This option doesn't do anything, just to illustrate how to pass args to JVM. */
options[0].optionString = "-verbose:none";
vm_args.nOptions = 1;
vm_args.options = options;
vm_args.ignoreUnrecognized = JNI_FALSE;

/* load and initialize a Java VM, return a JNI interface pointer in env */
res = JNI_CreateJavaVM(&jvm,(void**)&env,&vm_args);
if (res < 0) {
fprintf(stderr, "Can't create Java VM\n");
exit(1);
}

jclass ver;
jmethodID print;

ver = (*env)->FindClass(env, "sun/misc/Version");
if (ver == 0) {
fprintf(stderr, "Can't find Version");
}
print = (*env)->GetStaticMethodID(env, ver, "print", "()V");
(*env)->CallStaticVoidMethod(env, ver, print);

/* invoke the Main.test method using the JNI */
cls = (*env)->FindClass(env, "HelloWorld");
if (cls == 0) {
fprintf(stderr, "Can't find HelloWorld.class\n");
exit(1);
}

mid = (*env)->GetStaticMethodID(env, cls, "execute", "()V");
if (mid==0) {
fprintf(stderr, "No such method!\n");
exit(1);
}
// otherwise execute this method
(*env)->CallStaticVoidMethod(env, cls, mid);

/* We are done. */
(*jvm)->DestroyJavaVM(jvm);

return 0;
}


What remains is just compiling and executing it. I’m very rusty on using 'make', and have really little experience with any of the gnu build tools (auto{make,conf} and family), but since the compilation is rather straightforward, you can pass it something like:

gcc -g -Wall -I/opt/jdk1.6.0_02/include/ -I/opt/jdk1.6.0_02/include/linux/ -L./jre/lib/i386/client/ -ljvm -o invoker invoker.c


Just change /opt/jdk1.6.0_02/include/{,linux} to where ever your java header files reside. One of the funny things I’ve found with gcc, was that no matter how I force the linker to link it with the java library I’ve provided, it always seems to link with the original libraries that came installed on my computer. So at the first execution the output comes out like this:

% invoker
java version "1.4.2-03"
Java(TM) 2 Runtime Environment, Standard Edition (build Blackdown-1.4.2-03)
Java HotSpot(TM) Server VM (build Blackdown-1.4.2-03, mixed mode)
Can't find HelloWorld.class


Not only it could not find HelloWorld.class, which simply resides in the same directory as 'invoker', it’s telling me that it’s running the original Blackdown 1.4 JVM that came default on my linux distro as well! Unfortunately that’s not what I wanted. The way to remedy that is either to dynamically link to 'libjvm.so' (see [1] for details) or just cheat by modifying the LD_LIBRARY_PATH variable in linux:

% LD_LIBRARY_PATH=./jre/lib/i386/client/ ./invoker
java version "1.5.0_03"
Java(TM) 2 Runtime Environment, Standard Edition (build 1.5.0_03-b07)
Java HotSpot(TM) Client VM (build 1.5.0_03-b07, mixed mode)
Executed from launcher!


By mangling LD_LIBRARY_PATH, I’ve just swapped out the JVM without even recompiling my invoker application, which surprised me just how trivial the 'java' executable actually is, even that’s what everybody uses all the time.