程序员社区

Native连接Java世界的JavaVM和JNIEnv

JavaVM 和 JNIEnv

JNI 定义了两个关键数据结构,即JavaVMJNIEnv。两者本质上都是指向函数表的二级指针。在 C++ 版本中,它们是一些类,这些类具有指向函数表的指针,并具有每个通过该函数表间接调用的 JNI 函数的成员函数。掌握 JavaVMJNIEnv 这两个结构体就是关键,这两个结构体就是通往 Java 世界的大门,重要性不言而喻。

JavaVM

JavaVM 这个结构体指针在简单的 JNI 开发中很少使用到,它是虚拟机的代表,从 JDK 1.2 开始,一个进程只允许创建一个虚拟机

当 Java 层访问 Nativce 层的时候会自动在 JNI 层创建一个 JavaVM 指针,而我们在 JNI 层通常所使用的都是从 JavaVM 中获取的 JNIEnv 指针。那么现在我们来看下 JavaVM 这个结构体

/*
 * C++ version.
 */
struct _JavaVM {
    const struct JNIInvokeInterface* functions;

#if defined(__cplusplus)
    jint DestroyJavaVM()
    { return functions->DestroyJavaVM(this); }
    jint AttachCurrentThread(JNIEnv** p_env, void* thr_args)
    { return functions->AttachCurrentThread(this, p_env, thr_args); }
    jint DetachCurrentThread()
    { return functions->DetachCurrentThread(this); }
    jint GetEnv(void** env, jint version)
    { return functions->GetEnv(this, env, version); }
    jint AttachCurrentThreadAsDaemon(JNIEnv** p_env, void* thr_args)
    { return functions->AttachCurrentThreadAsDaemon(this, p_env, thr_args); }
#endif /*__cplusplus*/
};

JavaVM是虚拟机在JNI中的表示,一个JVM中只有一个JavaVM对象,这个对象是线程共享的。

通过JNIEnv我们可以获取一个Java虚拟机对象,其函数如下:

/**
 * 获取Java虚拟机对象
 * @param env JNIEnv对象
 * @param vm 用来存放获得的虚拟机的指针的指针
 * @return 成功返回0,失败返回其他
 */
jint GetJavaVM(JNIEnv *env, JavaVM **vm);

在加载动态链接库的时候,JVM会调用JNI_OnLoad(JavaVM* jvm, void* reserved)(如果定义了该函数),第一个参数会传入JavaVM指针。可以在该函数中保存JavaVM指针来供全局使用。

JavaVM *javaVM = NULL;

jint JNI_OnLoad(JavaVM *vm, void *reserved) {
    javaVM = vm;
    ...
}

创建JavaVM

从 Java 层到 Native 层的开发的时候,我们并不需要手动创建 JavaVM 对象,因此虚拟机自动帮我们完成了这些工作。然而,如果从 Native 层到 Java 层开发的时候,我们就需要手动创建 JavaVM 对象,创建的函数原型如下

#inlcude <jni.h>

jint JNI_CreateJavaVM(JavaVM **p_vm, void **p_env, void *vm_args);

JNI_CreateJavaVM 函数不属于任何结构体,方法声明在 jni.h 头文件中。

参数解释

  1. p_vm: 是一个指向 JavaVM * 的指针,函数成功返回时会给 JavaVM *指针赋值。
  2. p_env: 是一个指向 JNIEnv * 的指针,函数成功返回时会给 JNIEnv * 指针赋值。
  3. vm_args: 是一个指向 JavaVMInitArgs 的指针,是初始化虚拟机的参数。

如果函数执行成功,返回 JNI_OK(值为0),如果失败返回负值。

基本上可以这样理解 JNI_CreateJavaVM() 函数,它就是为了给 JavaVM *指针 和 JNIEnv *指针赋值。我们得到这两个指针便可以操纵"万物",这里的"万物"指的是 Java 世界的"万物"。

JNIEnv

_JavaVM 结构体中有一个函数 getEnv(),与之相对应的函数原型如下

jint GetEnv(JavaVM *vm, void **env, jint version);

参数说明

  1. vm: 虚拟机对象。
  2. env: 一个指向 JNIEnv 结构的指针的指针。
  3. version: JNI版本,根据jdk的版本,目前有四种值,分别为 JNI_VERSION_1_1, JNI_VERSION_1_2, JNI_VERSION_1_4, JNI_VERSION_1_6

这个函数执行结果有几种情况:

  1. 如果当前线程没有附着到虚拟机中,也就是没有调用 JavaVM 的 AttachCurrentThread() 函数,那么就会设置 *env 的值为 NULL,并且返回 JNI_EDETACHED (值为-2)。
  2. 如果参数version锁指定的版本不支持,那么就会设置 *env 的值为 NULL,并且返回 JNI_EVERSION(值为-3)。
  3. 除去上面的两种异常情况,就会给 *env 设置正确的值,并且返回 JNI_OK(值为0)。

JNIEnv类型是一个指向全部JNI方法的指针,JNIEnv 提供了大部分 JNI 函数。JNIEnv只在创建它的线程有效,<font color='red'>不能跨线程传递</font>,不能再线程之间共享 JNIEnv

所有的本地接口函数都会以 JNIEnv 作为第一个参数。不管是静态注册的本地C/C++函数接口,还是动态注册的本地函数接口,函数的第一个参数都是JNIEnv

如果一段代码无法通过其他方法获取自己的 JNIEnv,可以通过全局有效的 JavaVM,然后使用 GetEnv 来获取当前线程的 JNIEnv(如果该线程包含一个 JNIEnv)。

JNIEnv* env = NULL;
if (javaVM->GetEnv((void**) &env, JNI_VERSION_1_6) != JNI_OK) {
    return JNI_ERR;
}

GetEnv函数定义如下:

/**
 * 获取当前线程JNIEnv
 * @param env 用来存放获取JNIEnv对象的指针的指针
 * @param version JNI版本
 * @return 成功返回0,失败返回其他
 */
jint GetEnv(void** env, jint version)

对于本地库中创建的线程,需要使用AttachCurrentThread来附加到 JavaVM来获取一个可用的JNIEnv。线程退出或不再需要使用JNIEnv时,必须通过调用DetachCurrentThread来解除连接。

JNIEnv使用限制

函数执行结果的第一种情况来可以说明几个问题

  1. JNIEnv * env 是与线程相关,因此多个线程之间不能共享同一个 env
  2. 如果在Native层新建一个线程,要获取 JNIEnv * env,那么必须做到如下两点
    • 线程必须调用 JavaVMAttachCurrentThread() 函数。
    • 必须全局保存 JavaVM * mJavaVm,那么就可以在线程中通过调用 JavaVMgetEnv() 函数来获取 JNIEnv * env的值。

我们还记得 JNI_CreateJavaVM() 函数也设置 *env 的值吗?那么它肯定也会执行 AttachCurrentThread() 函数把当前线程附着到虚拟机中。这也就是解释了为何在没有明显调用 AttachCurrentThread() 的情况下,可以执行 JavaVMDetachCurrentThread() 函数。

C语言调用C++调用

JavaVMJNIEnv 在 C 语言环境下和 C++ 环境下调用是有区别的,以NewStringUTF函数为例:

C语言调用格式为:

(*env)->NewStringUTF(env, “Hellow World!”);

C++调用格式为:

env->NewStringUTF(“Hellow World!”);

建议使用 C++ 格式,这也是大部分代码使用的形式。但C++ 格式其实只是封装了 C 格式,使得调用更加简介方便。

赞(0) 打赏
未经允许不得转载:IDEA激活码 » Native连接Java世界的JavaVM和JNIEnv

一个分享Java & Python知识的社区