JNI程序规范和指南7——在本地代码中嵌入JVM

这是一个关于JNI的系列文章。

本文主要介绍如何将JVM嵌入到本地代码中。JVM可以看作是一个本地库,本地程序可以链接这个库,使用“调用接口”(invocation interface)来加载JVM。实际上,JDK标准的启动器就是一段简单的链接了JVM的C代码,启动器解析命令行,加载JVM,然后通过“调用接口”(invocation interface)执行Java程序。

创建Java虚拟机

为了解释“调用接口”,让我们看一下下面一段加载JVM并执行Prog.main的C代码:

public class Prog {    
    public static void main(String[] args) {
        System.out.println("Hello World " + args[0]);    
    } 
}
#include <jni.h>
#include <stdlib.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,该结构体特定于JDK 1.1中的虚拟机实现。尽管JDK 1.2引入了称为JavaVMInitArgs的通用虚拟机初始化结构体,但它仍支持JDK1_1InitArgs。常量JNI_VERSION_1_2在JDK 1.2版中定义,在JDK 1.1版中未定义。

在JDK 1.1中,C代码会调用JNI_GetDefaultJavaVMInitArgs来获取默认的JVM设置。JNI_GetDefaultJavaVMInitArgs会在vm_args中返回heap size, stack size和默认类路径等信息。然后我们将Prog类的路径添加到vm_args.classpath中。

在JDK 1.2中,C代码会创建一个JavaVMInitArgs结构体,虚拟机的初始化参数都存储在JavaVMOption数组中。可以设置直接对应于Java命令行选项的常用选项(例如-Djava.class.path =.)和实现特定的选项(例如-Xmx64m)。将ignoreUnrecognized字段设置为JNI_TRUE会导致虚拟机忽略无法识别的实现特定选项。

在设置完虚拟机初始化结构体后,C代码调用JNI_CreateJavaVM来加载JVM,JNI_CreateJavaVM有两个返回值:

  • jvm接口指针指向新创建的JVM。
  • 当前线程的JNIEnv接口指针env。本地代码通过env指针访问JNI函数。

JNI_CreateJavaVm成功执行后,当前的本地线程会将程序控制权交给JVM,这时,它会像本地方法一样执行,然后通过JNI函数调用Prog.main
程序的最后,通过DestroyJavaVM来释放JVM。
程序执行:

cl -I"%JAVA_HOME%\include" -I"%JAVA_HOME%\include\win32" -c Prog.c
link Prog.obj "%JAVA_HOME%\lib\jvm.lib"
# 然后将jvm.dll所在路径添加到系统环境变量
Prog.exe

不知道为什么要将jvm.dll添加到系统环境变量而拷贝到当前路径就不行,但最后程序的结果:

Hello World  from C!

将JVM链接到本地代码

“调用接口”需要你将JVM的实现链接到比如invoke.c,如何链接JVM取决于本地程序是需要一个具体的JVM实现还是多个未知的实现方式的JVM一起工作。

链接一个已知的JVM

本地程序可能需要和一个特定的JVM协作,那么你需要链接实现了JVM的本地库。比如,在Solaris中:

cc -I<jni.h dir> -L<libjava.so dir> -lthread -ljava invoke.c

在Windows中(还是参考上一节的方法吧…):

cl -I<jni.h dir> -MD invoke.c -link <javai.lib dir>\javai.lib 

一旦链接成功,就可以执行。执行过程可能会报错:在Solaris上,如果错误消息指示系统无法找到共享库libjava.so(或JDK 1.2以上版本中的libjvm.so),则需要将包含虚拟机库的目录添加到LD_LIBRARY_PATH变量中。在Windows上,错误消息可能表明它无法找到动态链接库javai.dll(或JDK 1.2以上版本中的jvm.dll)。在这种情况下,请将包含DLL的目录添加到系统环境变量中。

链接未知的JVM

如果本地程序需要连接多个未知的JVM,就不能指定某个特定的实现了JVM的本地库名(比如javai.dll, jvm.dll等)。一个解决办法是在运行时动态加载JVM库。下面的例子通过给定的路径加载JNI_CreateJavaVM的入口:

/* Win32 version */ 
void *JNU_FindCreateJavaVM(char *vmlibpath) 
{    
    HINSTANCE hVM = LoadLibrary(vmlibpath);    
    if (hVM == NULL) {        
        return NULL;    
    }    
    return GetProcAddress(hVM, "JNI_CreateJavaVM"); 
}

LoadLibraryGetProcAddress是Windows上动态链接的API,最好传递本地库的绝对路径给JNI_FindCreateJavaCM
Solaris版本类似:

/* Solaris version */ 
void *JNU_FindCreateJavaVM(char *vmlibpath) 
{    
    void *libVM = dlopen(vmlibpath, RTLD_LAZY);    
    if (libVM == NULL) {        
        return NULL;    
    }    
    return dlsym(libVM, "JNI_CreateJavaVM"); 
}

附加本地线程

假设你有一个多线程应用,比如用C实现的web服务器,当多个HTTP请求进来时,服务器会创建多个线程来同步的处理这些HTTP请求。为了使得多个线程都能同时操作JVM,我们将JVM嵌入到服务器中,如下图所示。 Embedding the Java virtual machine in a web server

服务器创建的本地方法生命周期一般比JVM要短。因此,我们需要将本地线程附加到正在运行的JVM中去,然后在这个本地方法中执行JNI调用,最后在不影响其他线程的情况下与JVM分离。

下面的例子attach.c说明了如何使用“调用接口”将本地线程附加到JVM中去,只适用于Windows平台。

/* 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); 
}

本地代码开启了五个线程,线程开启后,会等待1s让线程运行完毕,完后调用DestroyJavaVM。每一个线程都将自己附加到JVM中,然后调用Prog.main,最后将自己与JVM分离。
JNI_AttachCurrentThread的第三个参数为NULL,JDK 1.2以上版本引入JNI_ThreadAttachArgs这个结构体,它允许你添加额外的参数,如线程组等。这个结构体会在后面的文章中介绍。
当程序执行DetachCurrentThread时,它会释放当前线程的所有局部引用。 以下是运行结果(顺序是随机的):

Hello World  from Thread 1
Hello World  from Thread 4
Hello World  from Thread 0
Hello World  from Thread 3
Hello World  from Thread 2

建议不要用于商业用途, 转载请注明原文地址: https://Soo-Q6.github.io/blog/2019-10-10-JNI-guides-and-specifications-7/


© 2019. All rights reserved.

Powered by shouqin v1.0