本章講述如何把一個JVM嵌入到你的本地程序當中去。一個JVM可以看作就是一個本地庫克蚂。本地程序可以鏈接這個庫,然后通過“調用接口”(invocation interface)來加載JVM。實際上膛虫,JDK中標準的啟動器也就是一段簡單的鏈接了JVM的C代碼格粪。啟動器解析命令躏吊、加載JVM、并通過“調用接口”(invocation interface)運行JAVA程序帐萎。
7.1 創(chuàng)建JVM
我們用下面這段C代碼來加載一個JVM并調用Prog.main方法來演示如何使用調用接口比伏。
public class Prog {
public static void main(String[] args) {
System.out.println("Hello World " + args[0]);
}
}
下面是啟動器:
include <jni.h>
define PATH_SEPARATOR ';' /* define it to be ':' on Solaris */
define USER_CLASSPATH "." /* where Prog.class is */
main() {
JNIEnv *env;
JavaVM *jvm;
jint res;
jclass cls;
jmethodID mid;
jstring jstr;
jclass stringClass;
jobjectArray args;
ifdef JNI_VERSION_1_2
JavaVMInitArgs vm_args;
JavaVMOption options[1];
options[0].optionString =
"-Djava.class.path=" USER_CLASSPATH;
vm_args.version = 0x00010002;
vm_args.options = options;
vm_args.nOptions = 1;
vm_args.ignoreUnrecognized = JNI_TRUE;
/* Create the Java VM */
res = JNI_CreateJavaVM(&jvm, (void**)&env, &vm_args);
else
JDK1_1InitArgs vm_args;
char classpath[1024];
vm_args.version = 0x00010001;
JNI_GetDefaultJavaVMInitArgs(&vm_args);
/* Append USER_CLASSPATH to the default system class path */
sprintf(classpath, "%s%c%s",
vm_args.classpath, PATH_SEPARATOR, USER_CLASSPATH);
vm_args.classpath = classpath;
/* Create the Java VM */
res = JNI_CreateJavaVM(&jvm, &env, &vm_args);
endif /* JNI_VERSION_1_2 */
if (res < 0) {
fprintf(stderr, "Can't create Java VM\n");
exit(1);
}
cls = (*env)->FindClass(env, "Prog");
if (cls == NULL) {
goto destroy;
}
mid = (*env)->GetStaticMethodID(env, cls, "main",
"([Ljava/lang/String;)V");
if (mid == NULL) {
goto destroy;
}
jstr = (*env)->NewStringUTF(env, " from C!");
if (jstr == NULL) {
goto destroy;
}
stringClass = (*env)->FindClass(env, "java/lang/String");
args = (*env)->NewObjectArray(env, 1, stringClass, jstr);
if (args == NULL) {
goto destroy;
}
(*env)->CallStaticVoidMethod(env, cls, mid, args);
destroy:
if ((env)->ExceptionOccurred(env)) {
(env)->ExceptionDescribe(env);
}
(jvm)->DestroyJavaVM(jvm);
}
上面的代碼有條件地編譯一個初始化JDK1_1InitArgs這個structure。這個structure是JDK1.1下特有的疆导,盡管JDK1.2也會支持赁项,但JDK1.2引入了一個更通用的叫作JavaVMInitArgs的VM初始化structure。
常量JNI_VERSION_1_2在JDK1.2下定義澈段,JDK1.1下是沒有的悠菜。
當目標平臺是1.1時,C代碼首先調用JNI_GetDefaultJavaVMInitArgs來獲得默認的VM設置败富。這個調用會返回heap size悔醋、stack size、默認類路徑等信息兽叮,并把這些信息存放在參數vm_args中芬骄。然后我們把Prog.class所在的目錄附加到vm_args.classpath中。
當平臺目標是1.2時充择,C代碼創(chuàng)建了一個JavaVMInitArgs的structure德玫。VM的初始化參數被存放在一個JavaVMOption數組中。
設置完VM初始化structure后椎麦,C程序調用JNI_CreateJavaVM來加載和初始化JVM宰僧,傳入的前兩個參數:
1、 接口指針jvm,指向新創(chuàng)建的JVM琴儿。
2段化、 當前線程的JNIEnv接口指針env。本地代碼通過env指針訪問JNI函數造成。
當函數JNI_CreateJavaVM函數成功返回時显熏,當前本地線程已經把自己的控制權交給JVM。這時晒屎,它會就像一個本地方法一樣運行喘蟆。以后就可以通過JNI函數來啟動Prog.main方法。
接著鼓鲁,程序調用DestroyJavaVM函數來unloadJVM蕴轨。不幸的是,在JDK1.1和JDK1.2中你不能unloadJVM骇吭,它會一直返回一個錯誤碼橙弱。
運行上面的程序,產生如下輸出:
Hello World from C!
7.2 把本地程序和JVM鏈接在一起
通過調用接口燥狰,你可把invoke.c這樣的程序和一個JVM鏈接到一起棘脐。怎么樣鏈接JVM取決于本地程序是要和一個特定的VM一起工作,還是要和多個具體實現方式未知的不同VM一起工作龙致。
7.2.1 和一個己知的JVM鏈接到一起
這種情況下蛀缝,你可以把你的本地程序和實現了JVM的本地庫鏈接在一起。編譯鏈接成功后目代,你就可以運行得到的可執(zhí)行文件内斯。運行時,你可能會得到一個錯誤信息像啼,比如“無法找到共享庫或者動態(tài)鏈接庫”俘闯,在Windows下,錯誤信息可能會指出無法發(fā)現動態(tài)鏈接庫javai.dll(JDK1.1)或者jvm.dll(JDK1.2)忽冻,這時真朗,你需要把DLL文件加載到你的PATH環(huán)境變量中去。
7.2.2 和未知的多個JVM鏈接到一起
這種情況下僧诚,你就不能把本地程序直接和一個特定的庫鏈接在一起了遮婶。比如,JDK1.1的庫是javai.dll湖笨,而JDK1.2的庫是jvm.dll旗扑。
解決方案是根據本地程序的需要,用運行時動態(tài)鏈接來加載不同的VM庫慈省。例如臀防,下面的win32代碼,根據給定的VM庫的路徑找到JNI_CreateJavaVM函數的入口。
LoadLibrary和GetProcAddress是Win32平臺上用來動態(tài)鏈接的API袱衷。雖然LoadLibrary可以實現了JVM的本地庫的名字(如“jvm”)或者路徑(如“C:\jdk1.2\jre\bin\classic\jvm.dll”)捎废。最好把本地庫的絕對路徑傳遞給JNU_FindCreateJavaVM,讓LoadLibrary去搜索jvm.dll致燥,這樣程序就不怕環(huán)境變量被改變了登疗。
7.3 附加本地線程
假設,你有一個用C寫的服務器這樣的多線程程序嫌蚤。當HTTP請求進來的時候辐益,服務器創(chuàng)建許多本地線程來并行的處理HTTP請求。為了讓多個線程可以同時操作JVM脱吱,我們可能需要把一個JVM植入這個服務器荷腊。
圖7.1 把JVM嵌入WEB服務器
服務器上的本地方法的生命周期一般會比JVM要短,因此我們需要一個方法把本地線程附加到一個已經在運行的JVM上面急凰,然后在這個本地方法中進行JNI調用,最后在不打擾其它連接到JVM上的線程的情況下把這個本地線程和JVM分離猜年。
下面這個例子中抡锈,attach.c演示了怎么樣使用調用接口(invocation interface)把本地線程附加到VM上去,這段程序使用的是Win32線程API乔外。
/ Note: This program only works on Win32 */
include <windows.h>
include <jni.h>
JavaVM jvm; / The virtual machine instance */
define PATH_SEPARATOR ';'
define USER_CLASSPATH "." /* where Prog.class is */
void thread_fun(void *arg)
{
jint res;
jclass cls;
jmethodID mid;
jstring jstr;
jclass stringClass;
jobjectArray args;
JNIEnv env;
char buf[100];
int threadNum = (int)arg;
/ Pass NULL as the third argument */
ifdef JNI_VERSION_1_2
res = (*jvm)->AttachCurrentThread(jvm, (void**)&env, NULL);
else
res = (*jvm)->AttachCurrentThread(jvm, &env, NULL);
endif
if (res < 0) {
fprintf(stderr, "Attach failed\n");
return;
}
cls = (*env)->FindClass(env, "Prog");
if (cls == NULL) {
goto detach;
}
mid = (*env)->GetStaticMethodID(env, cls, "main",
"([Ljava/lang/String;)V");
if (mid == NULL) {
goto detach;
}
sprintf(buf, " from Thread %d", threadNum);
jstr = (*env)->NewStringUTF(env, buf);
if (jstr == NULL) {
goto detach;
}
stringClass = (*env)->FindClass(env, "java/lang/String");
args = (*env)->NewObjectArray(env, 1, stringClass, jstr);
if (args == NULL) {
goto detach;
}
(*env)->CallStaticVoidMethod(env, cls, mid, args);
detach:
if ((env)->ExceptionOccurred(env)) {
(env)->ExceptionDescribe(env);
}
(*jvm)->DetachCurrentThread(jvm);
}
main() {
JNIEnv *env;
int i;
jint res;
ifdef JNI_VERSION_1_2
JavaVMInitArgs vm_args;
JavaVMOption options[1];
options[0].optionString =
"-Djava.class.path=" USER_CLASSPATH;
vm_args.version = 0x00010002;
vm_args.options = options;
vm_args.nOptions = 1;
vm_args.ignoreUnrecognized = TRUE;
/* Create the Java VM */
res = JNI_CreateJavaVM(&jvm, (void**)&env, &vm_args);
else
JDK1_1InitArgs vm_args;
char classpath[1024];
vm_args.version = 0x00010001;
JNI_GetDefaultJavaVMInitArgs(&vm_args);
/* Append USER_CLASSPATH to the default system class path */
sprintf(classpath, "%s%c%s",
vm_args.classpath, PATH_SEPARATOR, USER_CLASSPATH);
vm_args.classpath = classpath;
/* Create the Java VM */
res = JNI_CreateJavaVM(&jvm, &env, &vm_args);
endif /* JNI_VERSION_1_2 */
if (res < 0) {
fprintf(stderr, "Can't create Java VM\n");
exit(1);
}
for (i = 0; i < 5; i++)
/* We pass the thread number to every thread */
_beginthread(thread_fun, 0, (void )i);
Sleep(1000); / wait for threads to start /
(jvm)->DestroyJavaVM(jvm);
}
上面這段attach.c代碼是invoke.c的一個變形床三。與在主線程中調用Prog.main不同,本地代碼開啟了五個線程杨幼。開啟線程完成以后撇簿,它就會等待1秒鐘讓線程可以運行完畢,然后調用DestroyJavaVM來銷毀JVM差购。而每一個線程都會把自己附加到JVM上面四瘫,然后調用Prog.main方法,最后斷開與JVM的連接欲逃。
JNI_AttachCurrentThread的第三個參數需要傳入NULL找蜜。JDK1.2引入了JNI_ThreadAttachArgs這個structure。它允許你向你要附加的線程傳遞特定的信息稳析,如線程組等洗做。JNI_ThreadAttachArgs這個structure的詳細描述在13.2節(jié)里面,作為JNI_AttachCurrentThread的規(guī)范的一部分被提到彰居。
當程序運行函數DetachCurrentThread時诚纸,它釋放屬于當前線程的所有局部引用。
運行程序陈惰,輸出如下:
Hello World from thread 1
Hello World from thread 0
Hello World from thread 4
Hello World from thread 2
Hello World from thread 3
上面這些輸出根據不同的線程調試策略畦徘,可能會出現不同的順序。