跳转至

JNI(Java Native Interface)

示例见 Demo

通过使用 Java本地接口书写程序,可以确保代码在不同的平台上方便移植。

JNI接口指针,是一个指向指针的指针。

Interface pointer

JNI 接口指针,只在当前线程中有效。因此,本地方法不能在线程间传递JNI接口指针。

虚拟机保证在同一个Java线程中多次调用本地方法时会传递给本地方法同一个JNI接口指针,但不同Java线程调用本地方法可能会接收到不同的JNI接口指针。

Java VM是多线程的,本地库编译和连接时需要注意多线程事项。比如对于GNU gcc 编译,需要指定-D_REENTRANT等。

The VM internally maintains a list of loaded native libraries for each class loader.

  • A native library may be statically linked with the VM. The manner in which the library and VM image are combined is implementation dependent. A System.loadLibrary or equivalent API must succeed for this library to be considered loaded.
  • A library L whose image has been combined with the VM is defined as statically linked if and only if the library exports a function called JNI_OnLoad_L.
  • If a statically linked library L exports a function called JNI_OnLoad_L and a function called JNI_OnLoad, the JNI_OnLoad function will be ignored.
  • If a library L is statically linked, then upon the first invocation of System.loadLibrary("L") or equivalent API, a JNI_OnLoad_L function will be invoked with the same arguments and expected return value as specified for the JNI_OnLoad function.
  • A library L that is statically linked will prohibit a library of the same name from being loaded dynamically.
  • When the class loader containing a statically linked native library L is garbage collected, the VM will invoke the JNI_OnUnload_L function of the library if such a function is exported.
  • If a statically linked library L exports a function called JNI_OnUnLoad_L and a function called JNI_OnUnLoad, the JNI_OnUnLoad function will be ignored.

VM

所有的线程都是Linux线程,由内核统一调度。它们通常从Java中启动(如使用new Thread().start()),也可以在其他任何地方创建,然后连接(attach)到JavaVM。

例如,一个用pthread_create启动的线程能够使用JNI AttachCurrentThread 或 AttachCurrentThreadAsDaemon函数连接到JavaVM。在一个线程成功连接(attach)之前,它没有JNIEnv,不能够调用JNI函数。

连接一个Native创建的线程会触发构造一个java.lang.Thread对象,然后其被添加到主线程群组(main ThreadGroup),以让调试器可以检测到。对一个已经连接的线程使用AttachCurrentThread不做任何操作(no-op)。

创建JVM,涉及到第三方jar包时,通过"-Djava.class.path=" 指定,无论jar包还是class文件,不能是目录;

当在一个线程里面调用AttachCurrentThread后,如果不需要用的时候一定要DetachCurrentThread,否则线程无法正常退出。

Creating the VM

  • JNI_CreateJavaVM 函数加载和初始化JavaVM,返回指向JNI接口指针的指针。调用JNI_CreateJavaVM的线程认为是主线程。

Attaching to the VM

  • 每个线程多次AttachCurrentThread获取的JNIEnv指针是一样,但是不同线程Attach之后获取到的JNIEnv的指针不一样;
  • Attached的线程应该有足够的栈空间;
  • Attached到VM的线程,其上下文类加载器是bootstrap加载器。

Detaching from the VM

  • DetachCurrentThread函数;

Unloading the VM

  • JNI_DestroyJavaVM() 函数;

当在系统中调用System.loadLibrary函数时,该函数会找到对应的动态库,然后首先试图找到"JNI_OnLoad"函数,如果该函数存在,则调用它。

JNI_OnLoad可以和JNIEnv的registerNatives函数结合起来,实现动态的函数替换。

Java和C++类型映射

Java 类型 本地类型 描述
boolean jboolean C/C++8位整型
byte jbyte C/C++带符号的8位整型
char jchar C/C++无符号的16位整型
short jshort C/C++带符号的16位整型
int jint C/C++带符号的32位整型
long jlong C/C++带符号的64位整型e
float jfloat C/C++32位浮点型
double jdouble C/C++64位浮点型
Object jobject 任何Java对象,或者没有对应java类型的对象
Class jclass Class对象
String jstring 字符串对象
Object[] jobjectArray 任何对象的数组
boolean[] jbooleanArray 布尔型数组
byte[] jbyteArray 比特型数组
char[] jcharArray 字符型数组
short[] jshortArray 短整型数组
int[] jintArray 整型数组
long[] jlongArray 长整型数组
float[] jfloatArray 浮点型数组
double[] jdoubleArray 双浮点型数组

常用的函数:

  • jclass FindClass`(const char* clsName):通过类的名称(类的全名,这时候包名不是用.号,而是用/来区分的)来获取jclass;
  • jclass GetObjectClass(jobject obj):通过对象实例来获取jclass,相当于java中的getClass方法;
  • jclass GetSuperClass(jclass obj):通过jclass可以获取其父类的jclass对象;
  • GetFieldID,GetMethodID,GetStaticFieldID,GetStaticMethodID等

JNIEnv

JNIEnv类型实际上代表了Java环境,通过这个JNIEnv* 指针,就可以对Java端的代码进行操作。例如创建Java类中对象,调用Java对象的方法,获取Java对象中的属性等等。

JNIEnv的指针会被JNI传入到本地方法的实现函数中来对Java端的代码进行操作。

Method

一、对象操作

1. jobject AllocObject(jclass clazz)

说明:不调用构造方法创建实例

参数:

  • clazz:指定对象的类

2. jobject NewObject(jclass clazz, jmethodID methodID, …)

jobject NewObjectA(jclass clazz, jmethodID methodID, jvalue* args)
jobject NewObjectV(jclass clazz, jmethodID methodID, va_list args)

说明:使用指定的构造方法创建类的实例,唯一不同的是输入参数的传入形式不同

参数:

  • clazz:指定对象的类
  • methodID:指定的构造方法
  • args:输入参数列表

3. jclass GetObjectClass(jobject obj)

说明:根据对象获取所属类

参数:

  • obj:某个 Java 对象实例,不能为 NULL

4. jobjectRefType GetObjectRefType(jobject obj)

说明:获取到对象的引用类型,JNI 1.6 新增的方法

参数:

  • obj:某个 Java 对象实例

返回:

  • JNIInvalidRefType = 0 // 该 obj 是个无效的引用
  • JNILocalRefType = 1 // 该 obj 是个局部引用
  • JNIGlobalRefType = 2 // 该 obj 是个全局引用
  • JNIWeakGlobalRefType = 3 // 该 obj 是个全局的弱引用

二、方法操作

1. jmethodID GetMethodID(jclass clazz, const char name, const char sig)

说明:获取类中某个非静态方法的ID

参数:

  • clazz:指定对象的类
  • name:这个方法在 Java 类中定义的名称,构造方法为 ““
  • sig:这个方法的类型描述符,例如 “()V”,其中括号内是方法的参数,括号后是返回值类型

示例:

Java 的类定义如下:

package com.afei.jnidemo;

class Test {

    public Test(){}

    public int show(String msg, int number) {
        System.out.println("msg: " + msg);
        System.out.println("number: " + number);
        return 0;
    }

}

JNI 调用如下:

jclass clazz = env->FindClass("com/afei/jnidemo/Test");
jmethodID constructor_method = env->GetMethodID(clazz, "<init>", "()V");
jmethodID show_method=env->GetMethodID(clazz,"show", "(Ljava/lang/String;I)I");

签名时其中括号内是方法的参数,括号后是返回值类型。例如 show 方法,第一个参数是 String 类,对应 Ljava/lang/String;(注意后面有一个分号),第二个参数是 int 基本类型,对应的类型描述符是 I,返回值也是 int,同样是 I,所以最终该方法的签名为 (Ljava/lang/String;I)I

2. NativeType CallMethod(jobject obj, jmethodID methodID, …)

NativeType CallMethodA(jobject obj, jmethodID methodID, jvalue* args)

NativeType CallMethodV(jobject obj, jmethodID methodID, va_list args)

说明:调用对象的某个方法,唯一不同的是输入参数的传入形式不同,这里 type 表示的是一系列方法。

参数:

  • obj:某个 Java 对象实例
  • methodID:指定方法的ID
  • args:输入参数列表

示例:

jclass clazz = env->FindClass("com/afei/jnidemo/Test");
jmethodID show_method = env->GetMethodID(clazz, "show", "(Ljava/lang/String;I)I");
jint result = env->CallIntMethod(clazz, show_method, "Hello JNI!", 0);

3. jmethodID GetStaticMethodID(jclass clazz, const char name, const char sig)

说明:同 GetMethodID,只不过操作的是静态方法

4. NativeType CallStaticMethod(jclass clazz, jmethodID methodID, …)

NativeType CallStaticMethodA(jclass clazz, jmethodID methodID, jvalue* args)

NativeType CallStaticMethodV(jclass clazz, jmethodID methodID, va_list args)

说明:同 NativeType CallMethod,只不过操作的是静态方法,参数也由 jobject 变成了 jclass。

三、字符串操作

1. jstring NewString(const jchar* unicodeChars, jsize len)

说明:以 UTF-16 的编码方式创建一个 Java 的字符串(jchar 的定义为 uint16_t)

参数:

  • unicodeChars:指向字符数组的指针
  • len:字符数组的长度

2. jstring NewStringUTF(const char* bytes)

说明:以 UTF-8 的编码方式创建一个 Java 的字符串

参数:

  • bytes:指向字符数组的指针

3. jsize GetStringLength(jstring string)

jsize GetStringUTFLength(jstring string)

说明:获取字符串的长度,GetStringLength 是UTF-16 编码,GetStringUTFLength 是 UTF-8 编码

参数:

  • string:字符串

4. const jchar GetStringChars(jstring string, jboolean isCopy)

const char GetStringUTFChars(jstring string, jboolean isCopy)

说明:将 Java 风格的 jstring 对象转换成 C 风格的字符串,同上一个是 UTF-16 编码,一个是 UTF-8 编码

参数:

  • string:Java 风格的字符串
  • isCopy:是否进行拷贝操作,0 为不拷贝

5. void ReleaseStringChars(jstring string, const jchar* chars)

void ReleaseStringUTFChars(jstring string, const char* utf)

说明:释放指定的字符串指针,通常来说,Get 和 Release 是成对出现的

参数:

  • string:Java 风格的字符串
  • chars/utf:对应的 C 风格的字符串

四、数组操作

1. jobjectArray NewObjectArray(jsize length, jclass elementClass, jobject initialElement)

说明:创建引用数据类型的数组

参数:

  • length:数组的长度
  • elementClass:数组的元素所属的类
  • initialElement:使用什么样的对象来初始化,可以选择 NULL

2. jsize GetArrayLength(jarray array)

说明:获取数组的长度 参数:

  • array:指定的数组对象。jarray 是 jbooleanArray、jbyteArray、jcharArray 等的父类。

3. jobject GetObjectArrayElement(jobjectArray array, jsize index)

说明:获取引用数据类型数组指定索引位置处的对象

参数:

  • array:引用数据类型数组
  • index:目标索引值

4. void SetObjectArrayElement(jobjectArray array, jsize index, jobject value)

说明:设置引用数据类型数组指定索引位置处的值

参数:

  • array:需要设置的引用数据类型数组
  • index:目标索引值
  • value:需要设置的值

5. NativeType GetArrayElements(ArrayType array, jboolean isCopy)

说明:获取基本数据类型数组的头指针

参数:

  • array:基本数据类型数组
  • isCopy:是否进行拷贝操作,0 为不拷贝

6. void ReleaseArrayElements(ArrayType array, NativeType* elems, jint mode)

说明:释放基本数据类型数组指针。通常来说,Get 和 Release 是成对出现的

参数:

  • array:基本数据类型数组
  • elems:对应的 C 风格的基本数据类型指针
  • mode:释放模式,通常我们都是使用 0

7. void GetArrayRegion(ArrayType array, jsize start, jsize len, NativeType* buf)

说明:返回基本数据类型数组的部分副本。

参数:

  • array:基本数据类型数组
  • start:起始的索引值
  • len:拷贝的长度
  • buf:拷贝到的目标数组

8. void SetArrayRegion(ArrayType array, jsize start, jsize len, const NativeType* buf)

说明:设置基本数据类型数组元素。类型和上面的表类似。

参数:

  • array:需要设置的基本数据类型数组
  • start:起始的索引值
  • len:需要设置的 buf 的长度
  • buf:需要设置的值数组

References

JNI 支持3中不透明的引用:局部(local)引用全局(global)引用弱全局引用

  • 局部和全局引用,有着各自不同的生命周期。局部引用能够被自动释放,而全局引用和弱全局引用在被程序员释放之前,是一直有效的。
  • 一个局部或者全局引用,使所提及的对象不能被垃圾回收。而弱全局引用,则允许提及的对象进行垃圾回收。
  • 不是所有的引用都可以在所有上下文中使用的。例如:在一个创建返回引用native方法之后,使用一个局部引用,这是非法的。

局部引用失效,有两种方式:

​ 1)系统会自动释放局部变量

​ 2)程序员可以显示地管理局部引用的生命周期,例如调用DeleteLocalRef

注意事项:

  • 局部对象只属于创建它们的线程,只在该线程中有效。一个线程想要调用另一个线程创建的局部引用是不被允许的。
  • Java传递给本地方法的对象是局部引用;
  • 所有JNI函数返回的Java对象是局部引用;
  • 本地方法返回给VM可能是局部或全局引用;

基本数据类型是不需要释放,如 jint , jlong , jchar 等等。

需要释放的是引用数据类型,当然也包括数组。如:jstring, jobject, j***Array, jclass,jmethodID等。

局部引用只有当本地函数返回Java(当Java调用native)调用线程detach JVM(native调用Java)时才会被GC,因此,当有长时间运行的本地函数或者创建很多局部引用时需要调用DeleteLocalRef进行引用删除。

对于C++ call Java时创建的global reference和local reference 创建,需要定义其释放之处;

注:HotSpotVM:-XX:MaxJNILocalCapacity flag (default: 65536)。当前没有测试出来,Local references 溢出的情况;(JDK 8)

Exception

本地代码中调用某个JNI接口时如果发生了异常,后续的本地代码不会立即停止执行,而会继续往下执行后面的代码。

// 如果这里出现问题,则会出现问题,后续执行异常
jauthority = env.newStringUTF(authority, "authority"); 
env->DeleteLocalRef(jauthority);

JNI提供了两种检查异常的方法:

  • 检查上一次 JNI函数调用的返回值是否为NULL。
  • 通过调用JNI函数ExceptionOccurred()来判断是否发生异常。

如果发生了异常,本地方法再调用其它JNI调用时必须先清除异常信息

libhdfs中封装jnienv的函数,并且对函数进行异常检查,返回errorno,并且使用goto的方式,进行统一释放。

jthr = env.newStringUTF(scheme, "scheme", &jscheme);
if (jthr ) { goto done;}
jthr = env.newStringUTF(authority, "authority", &jauthority);
if (jthr ) { goto done;}
jthr = env.newStringUTF(path, "path", &jpath);
if (jthr ) { goto done;}

retObj = env.newObject("alluxio/AlluxioURI",
"(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V", jscheme, jauthority, jpath);

done:
 env->DeleteLocalRef(jscheme);
 env->DeleteLocalRef(jauthority);
 env->DeleteLocalRef(jpath);

After an exception has been raised, the native code must first clear the exception before making other JNI calls. When there is a pending exception, the JNI functions that are safe to call are:

  • ExceptionOccurred、ExceptionDescribe、ExceptionClear、ExceptionCheck、ReleaseStringChars、ReleaseStringUTFChars

  • ReleaseStringCritical、ReleaseArrayElements、ReleasePrimitiveArrayCritical、DeleteLocalRef、DeleteGlobalRef、

  • DeleteWeakGlobalRef、MonitorExit、PushLocalFrame、PopLocalFrame