把一個JVM嵌入到本地程式中

weixin_33976072發表於2016-03-04

本章講述如何把一個JVM嵌入到你的本地程式當中去。一個JVM可以看作就是一個本地庫。本地程式可以連結這個庫,然後通過“呼叫介面”(invocation interface)來載入JVM。實際上,JDK中標準的啟動器也就是一段簡單的連結了JVM的C程式碼。啟動器解析命令、載入JVM、並通過“呼叫介面”(invocation interface)執行JAVA程式。
7.1 建立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程式碼建立了一個JavaVMInitArgs的structure。VM的初始化引數被存放在一個JavaVMOption陣列中。
設定完VM初始化structure後,C程式呼叫JNI_CreateJavaVM來載入和初始化JVM,傳入的前兩個引數:
1、 介面指標jvm,指向新建立的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的本地庫連結在一起。編譯連結成功後,你就可以執行得到的可執行檔案。執行時,你可能會得到一個錯誤資訊,比如“無法找到共享庫或者動態連結庫”,在Windows下,錯誤資訊可能會指出無法發現動態連結庫javai.dll(JDK1.1)或者jvm.dll(JDK1.2),這時,你需要把DLL檔案載入到你的PATH環境變數中去。
7.2.2 和未知的多個JVM連結到一起
這種情況下,你就不能把本地程式直接和一個特定的庫連結在一起了。比如,JDK1.1的庫是javai.dll,而JDK1.2的庫是jvm.dll。
解決方案是根據本地程式的需要,用執行時動態連結來載入不同的VM庫。例如,下面的win32程式碼,根據給定的VM庫的路徑找到JNI_CreateJavaVM函式的入口。
LoadLibrary和GetProcAddress是Win32平臺上用來動態連結的API。雖然LoadLibrary可以實現了JVM的本地庫的名字(如“jvm”)或者路徑(如“C:\jdk1.2\jre\bin\classic\jvm.dll”)。最好把本地庫的絕對路徑傳遞給JNU_FindCreateJavaVM,讓LoadLibrary去搜尋jvm.dll,這樣程式就不怕環境變數被改變了。
7.3 附加本地執行緒
假設,你有一個用C寫的伺服器這樣的多執行緒程式。當HTTP請求進來的時候,伺服器建立許多本地執行緒來並行的處理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節裡面,作為JNI_AttachCurrentThread的規範的一部分被提到。
當程式執行函式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
上面這些輸出根據不同的執行緒除錯策略,可能會出現不同的順序。

相關文章