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