Wednesday, January 16, 2008

Writing your own custom loader for Java

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.

Links
[1] The Invocation Interface: http://java.sun.com/docs/books/jni/html/invoke.html
[2] The Invocation API: http://java.sun.com/j2se/1.5.0/docs/guide/jni/spec/invocation.html

7 comments:

Anonymous said...

Dear Vince,
I am trying to run a custom JVM loader written in native code. However when I compile the code I receive the following error (in the creation of the JVM)...

ERROR: Undefined symbol: .JNI_CreateJavaVM

Kindly Help...

Thanks and Regards

Sadat

Vincent Liu said...

Hi Sadat,

Did you include <jni.h>? This is where the definition of JNI_CreateJavaVM is located, and from the sounds of it, you might have missed including that.

Hope it helps.

Anonymous said...

Hi Vince,
I have included jni.h in my code... Is the code not able to locate the file ? Also can u kindly throw some light on what the following statement does
options[0].optionString...

Another thing is when i comment out the JNI_CreateJavaVM method call the program runs uptil the point where it is called and then stops generating a core file...
Kindly help... thanks a lot in advance...

Regards

Sadat

Anonymous said...

Hi Vince,
I just commented the jni header file from my code and after compilation it immediately threw errors that clearly show that the code is picking the jni.h alright.... Is jni.h enough for JNI_CreateJavaVM or do i need to include any other header file ?

Thanks again in advance for all ur help...

Regards

Sadat

Vincent Liu said...

jni.h by itself should work. It did for me, so I'm not certain why it didn't compile for you.

options[0].optionString allows you to pass some command line flags like you'll normally pass to 'javac' on the command line. It is safe to omit it, if you wish to.

If you do not create your JavaVM, it is expected that your loader will segmentation fault. You need to make the call before anything else will work.

Anonymous said...

Hi there Vince,
I have seen my jni.h and it does contain the definition of JNI_CreateJavaVM() method. What then could be the problem ? Also can u throw a bit more light on the parameters to be passed to it ?
thanks a lot for ur help....

Regards
Sadat

Vincent Liu said...

JNI_CreateJavaVM(&jvm,(void**)&env,&vm_args);

It basically passes the address of the jvm's pointer for the function call to return the 'live' instance of the JVM, the second argument passes the environment to the JVM instance to be created, where the last argument is the options that you normally pass to the 'java' command on the command line.

Post a Comment