本章講述如何把一個(gè)JVM嵌入到你的本地程序當(dāng)中去。一個(gè)JVM可以看作就是一個(gè)本地庫(kù)。本地程序可以鏈接這個(gè)庫(kù),然后通過(guò)“調(diào)用接口”(invocation interface)來(lái)加載JVM。實(shí)際上,JDK中標(biāo)準(zhǔn)的啟動(dòng)器也就是一段簡(jiǎn)單的鏈接了JVM的C代碼。啟動(dòng)器解析命令、加載JVM、并通過(guò)“調(diào)用接口”(invocation interface)運(yùn)行JAVA程序。
7.1 創(chuàng)建JVM
我們用下面這段C代碼來(lái)加載一個(gè)JVM并調(diào)用Prog.main方法來(lái)演示如何使用調(diào)用接口。
public class Prog {
public static void main(String[] args) {
System.out.println("Hello World " + args[0]);
}
}
下面是啟動(dòng)器:
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);
}
上面的代碼有條件地編譯一個(gè)初始化JDK1_1InitArgs這個(gè)structure。這個(gè)structure是JDK1.1下特有的,盡管JDK1.2也會(huì)支持,但JDK1.2引入了一個(gè)更通用的叫作JavaVMInitArgs的VM初始化structure。
常量JNI_VERSION_1_2在JDK1.2下定義,JDK1.1下是沒(méi)有的。
當(dāng)目標(biāo)平臺(tái)是1.1時(shí),C代碼首先調(diào)用JNI_GetDefaultJavaVMInitArgs來(lái)獲得默認(rèn)的VM設(shè)置。這個(gè)調(diào)用會(huì)返回heap size、stack size、默認(rèn)類路徑等信息,并把這些信息存放在參數(shù)vm_args中。然后我們把Prog.class所在的目錄附加到vm_args.classpath中。
當(dāng)平臺(tái)目標(biāo)是1.2時(shí),C代碼創(chuàng)建了一個(gè)JavaVMInitArgs的structure。VM的初始化參數(shù)被存放在一個(gè)JavaVMOption數(shù)組中。
設(shè)置完VM初始化structure后,C程序調(diào)用JNI_CreateJavaVM來(lái)加載和初始化JVM,傳入的前兩個(gè)參數(shù):
1、 接口指針jvm,指向新創(chuàng)建的JVM。
2、 當(dāng)前線程的JNIEnv接口指針env。本地代碼通過(guò)env指針訪問(wèn)JNI函數(shù)。
當(dāng)函數(shù)JNI_CreateJavaVM函數(shù)成功返回時(shí),當(dāng)前本地線程已經(jīng)把自己的控制權(quán)交給JVM。這時(shí),它會(huì)就像一個(gè)本地方法一樣運(yùn)行。以后就可以通過(guò)JNI函數(shù)來(lái)啟動(dòng)Prog.main方法。
接著,程序調(diào)用DestroyJavaVM函數(shù)來(lái)unloadJVM。不幸的是,在JDK1.1和JDK1.2中你不能unloadJVM,它會(huì)一直返回一個(gè)錯(cuò)誤碼。
運(yùn)行上面的程序,產(chǎn)生如下輸出:
Hello World from C!
7.2 把本地程序和JVM鏈接在一起
通過(guò)調(diào)用接口,你可把invoke.c這樣的程序和一個(gè)JVM鏈接到一起。怎么樣鏈接JVM取決于本地程序是要和一個(gè)特定的VM一起工作,還是要和多個(gè)具體實(shí)現(xiàn)方式未知的不同VM一起工作。
7.2.1 和一個(gè)己知的JVM鏈接到一起
這種情況下,你可以把你的本地程序和實(shí)現(xiàn)了JVM的本地庫(kù)鏈接在一起。編譯鏈接成功后,你就可以運(yùn)行得到的可執(zhí)行文件。運(yùn)行時(shí),你可能會(huì)得到一個(gè)錯(cuò)誤信息,比如“無(wú)法找到共享庫(kù)或者動(dòng)態(tài)鏈接庫(kù)”,在Windows下,錯(cuò)誤信息可能會(huì)指出無(wú)法發(fā)現(xiàn)動(dòng)態(tài)鏈接庫(kù)javai.dll(JDK1.1)或者jvm.dll(JDK1.2),這時(shí),你需要把DLL文件加載到你的PATH環(huán)境變量中去。
7.2.2 和未知的多個(gè)JVM鏈接到一起
這種情況下,你就不能把本地程序直接和一個(gè)特定的庫(kù)鏈接在一起了。比如,JDK1.1的庫(kù)是javai.dll,而JDK1.2的庫(kù)是jvm.dll。
解決方案是根據(jù)本地程序的需要,用運(yùn)行時(shí)動(dòng)態(tài)鏈接來(lái)加載不同的VM庫(kù)。例如,下面的win32代碼,根據(jù)給定的VM庫(kù)的路徑找到JNI_CreateJavaVM函數(shù)的入口。
LoadLibrary和GetProcAddress是Win32平臺(tái)上用來(lái)動(dòng)態(tài)鏈接的API。雖然LoadLibrary可以實(shí)現(xiàn)了JVM的本地庫(kù)的名字(如“jvm”)或者路徑(如“C:\jdk1.2\jre\bin\classic\jvm.dll”)。最好把本地庫(kù)的絕對(duì)路徑傳遞給JNU_FindCreateJavaVM,讓LoadLibrary去搜索jvm.dll,這樣程序就不怕環(huán)境變量被改變了。
7.3 附加本地線程
假設(shè),你有一個(gè)用C寫的服務(wù)器這樣的多線程程序。當(dāng)HTTP請(qǐng)求進(jìn)來(lái)的時(shí)候,服務(wù)器創(chuàng)建許多本地線程來(lái)并行的處理HTTP請(qǐng)求。為了讓多個(gè)線程可以同時(shí)操作JVM,我們可能需要把一個(gè)JVM植入這個(gè)服務(wù)器。
圖7.1 把JVM嵌入WEB服務(wù)器
服務(wù)器上的本地方法的生命周期一般會(huì)比JVM要短,因此我們需要一個(gè)方法把本地線程附加到一個(gè)已經(jīng)在運(yùn)行的JVM上面,然后在這個(gè)本地方法中進(jìn)行JNI調(diào)用,最后在不打擾其它連接到JVM上的線程的情況下把這個(gè)本地線程和JVM分離。
下面這個(gè)例子中,attach.c演示了怎么樣使用調(diào)用接口(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的一個(gè)變形。與在主線程中調(diào)用Prog.main不同,本地代碼開啟了五個(gè)線程。開啟線程完成以后,它就會(huì)等待1秒鐘讓線程可以運(yùn)行完畢,然后調(diào)用DestroyJavaVM來(lái)銷毀JVM。而每一個(gè)線程都會(huì)把自己附加到JVM上面,然后調(diào)用Prog.main方法,最后斷開與JVM的連接。
JNI_AttachCurrentThread的第三個(gè)參數(shù)需要傳入NULL。JDK1.2引入了JNI_ThreadAttachArgs這個(gè)structure。它允許你向你要附加的線程傳遞特定的信息,如線程組等。JNI_ThreadAttachArgs這個(gè)structure的詳細(xì)描述在13.2節(jié)里面,作為JNI_AttachCurrentThread的規(guī)范的一部分被提到。
當(dāng)程序運(yùn)行函數(shù)DetachCurrentThread時(shí),它釋放屬于當(dāng)前線程的所有局部引用。
運(yùn)行程序,輸出如下:
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
上面這些輸出根據(jù)不同的線程調(diào)試策略,可能會(huì)出現(xiàn)不同的順序。