JNI编程笔记三
在使用JNI时最重要的是Java与C/C++如何传递数据,以及数据类型之间如何映射。本文重点需要了解的是JNI中类型和签名描述符的表示方式,以及它们与Java中数据类型的对应关系。要求可以借助Java虚拟机命令生成JNI签名。最后,列举了一些一般性示例,通过示例演示了如何在Java和C之间进行数据交互,例如基本数据类型之间和数组类型之间。
由于JNI在使用C/C++与Java交互时很相似,所以后续示例我们都以C语言的实现方式演示。
静态方法和实例方法
在Java层的native方法有的是static修饰静态方法,有的是实例方法,但是在JNI中对应的函数都是非静态函数,不同的地方是函数对应的参数类型不同。
JNI中的函数,前两个参数都是系统要求强制携带的参数,后面的参数才是开发人员自己定义的参数。第一个参数都是一样的,它是一个JNIEnv类型的指针变量,但是第二个参数会有所不同,如果Java层的方法是静态方法,那么第二的参数是一个jclass类型的变量,否则,第二个参数就是一个jobject类型的变量。
这里的jclass和jobject也好理解,我们知道Java中静态方法是属于类所有,而实例方法是属于对象的。JNI中jclass对应的就是一个Java层的Class实例,即Java层静态方法所在类的Class实例,而jobject对应的是Java层方法所在类的对象实例。
public native String getUsername(); public static native String getAddress(); /* * Class: com_sunny_demo_JNIMethod * Method: getUsername * Signature: ()Ljava/lang/String; */ JNIEXPORT jstring JNICALL Java_com_sunny_demo_JNIMethod_getUsername (JNIEnv *, jobject); /* * Class: com_sunny_demo_JNIMethod * Method: getAddress * Signature: ()Ljava/lang/String; */ JNIEXPORT jstring JNICALL Java_com_sunny_demo_JNIMethod_getAddress (JNIEnv *, jclass);
类型和签名描述符
JNI中类型和签名描述符很相似,但是类型描述符仅仅是Java类型在JNI中对应的类型表示方式,而签名不仅可以表示类型,还可以表明该类型是基本类型还是对象类型或者数组类型,其实签名中包含了类型名称。
类型名称
在JNI中有两个比较常用的获取jclass类型变量的函数,它们分别是GetObjectClass()和FindClass(),有关这两个函数在使用上的差异,当后续使用到时再做分析,这里先看一下FindClass()函数的声明:
jclass (JNICALL *FindClass) (JNIEnv *env, const char *name);
第二个参数是一个字符指针,在后续JNI相关介绍时,如不特殊说明,我们将字符指针都以字符串为别名称谓。这里的name其实就是Java层中对应的Class实例的全类名,只不过在Java中全类名是包名+类名,它们之间是以“.”做分割,但是在JNI中将“.”更改为了“/”。
例如:java.lang.String对应的JNI中的名称是java/lang/String,java.util.Date对应的是java/util/Date。
签名
JNI中使用签名的函数如GetFieldID()和GetMethodID(),这两个函数的最后一个参数就是对应的类型签名和函数签名,如下是两个函数的声明:
jfieldID (JNICALL *GetFieldID) (JNIEnv *env, jclass clazz, const char *name, const char *sig); jmethodID (JNICALL *GetMethodID) (JNIEnv *env, jclass clazz, const char *name, const char *sig);
在JNI中对应的签名,在虚拟机中其实叫描述符descriptor。
字段Field描述符一般分为三种类型:
- 基本类型,直接使用字符表示即可,如I表示整型数,常见的还有D表示double类型、F表示float类型、J表示long类型等。
- 对象类型,要使用对象标记L、对象的非限定名以及“;”,如Ljava/lang/String;
- 数组类型,使用[字符,如double[][][],描述符为[[[D。
Java数据类型 | 描述符 |
---|---|
byte | B |
char | C |
double | D |
float | F |
int | I |
long | J |
short | S |
boolean | Z |
特殊类型void | V |
对象类型 | L + 对象非限定名+“;”。如 Ljava/lang/String; 表示 String 类型 |
数组类型 | 每增加一个维度则在对应的字段描述符前增加一个 [ ,如一维数组 int[] 的描述符为 [I ,二维数组 String[][] 的描述符为 [[java/lang/String |
函数签名跟类型签名不同,因为函数不但需要标明它代表的是函数,同时还要标明它的返回值类型。
函数中每一个参数对应一种类型签名,函数返回值也对应的一种类型签名。函数签名的表示形式:先参数列表,后返回值,其中参数按照参数顺从放在一组小括号“()”之内。
String getStrData(String src) int getIntData(int i) int[] getIntArray()
如上几个方法对应的JNI中的函数签名如下:
(Ljava/lang/String;)Ljava/lang/String; (I)I ([I)[I
在JNI编程时签名有时候很容易出现错误,但是不需要担心,因为Java虚拟机提供相关命令获取签名信息。在项目中只需要拿到对应类的class文件,然后使用javap命令加–s选项即可快速生成签名信息。
Compiled from "JNIMethod.java" public class com.sunny.demo.JNIMethod { public static int age; descriptor: I public java.lang.String name; descriptor: Ljava/lang/String; public com.sunny.demo.bean.Animal animal; descriptor: Lcom/sunny/demo/bean/Animal; public com.sunny.demo.JNIMethod(); descriptor: ()V public native boolean getBooleanData(boolean); descriptor: (Z)Z public native byte getByteData(byte); descriptor: (B)B public static native int getIntData(int); descriptor: (I)I public native float getFloatData(float); descriptor: (F)F public native long getLongData(long); descriptor: (J)J public native java.lang.String getStrData(java.lang.String); descriptor: (Ljava/lang/String;)Ljava/lang/String; public native int[] getIntArray(); descriptor: ()[I //... }
JNI访问基本数据类型
JNI对基本数据类型和引用类型处理是不同的。基本类型的映射是一对一的,例如Java中的int就是对应的JNI中的jint。
public native boolean getBooleanData(boolean b); public native byte getByteData(byte b); public static native int getIntData(int i); public native float getFloatData(float f); public native long getLongData(long l); /* * Class: com_sunny_demo_JNIMethod * Method: getBooleanData * Signature: (Z)Z */ JNIEXPORT jboolean JNICALL Java_com_sunny_demo_JNIMethod_getBooleanData (JNIEnv * env, jobject jobj, jboolean b){ printf("boolean size:%d \n", sizeof(b)); return !b; } /* * Class: com_sunny_demo_JNIMethod * Method: getByteData * Signature: (B)B */ JNIEXPORT jbyte JNICALL Java_com_sunny_demo_JNIMethod_getByteData (JNIEnv * env, jobject jobj, jbyte b){ printf("byte size:%d \n", sizeof(b)); return b + 1; } /* * Class: com_sunny_demo_JNIMethod * Method: getIntData * Signature: (I)I */ JNIEXPORT jint JNICALL Java_com_sunny_demo_JNIMethod_getIntData (JNIEnv * env, jclass jcls, jint i){ printf("int size:%d \n", sizeof(i)); return i + 1; } /* * Class: com_sunny_demo_JNIMethod * Method: getFloatData * Signature: (F)F */ JNIEXPORT jfloat JNICALL Java_com_sunny_demo_JNIMethod_getFloatData (JNIEnv * env, jobject jobj, jfloat f){ printf("float size:%d \n", sizeof(f)); return f + 1; } /* * Class: com_sunny_demo_JNIMethod * Method: getLongData * Signature: (J)J */ JNIEXPORT jlong JNICALL Java_com_sunny_demo_JNIMethod_getLongData (JNIEnv *env, jobject jobj, jlong num){ printf("long size:%d \n", sizeof(num)); return num + 1; } // boolean size:1 // byte size:1 // int size:4 // float size:4 // long size:8
JNI访问引用类型
JNI把Java对象作为一个指针传递到本地代码的函数中,这个指针指向JVM内部的数据结构,而内部数据结构在内存中的存储方式是不可见的。本地代码必须通过在JNIEnv中的相应的函数操作JVM中的对象。
例如,对于java.lang.String对应的JNI类型是jstring,但本地代码只能通过GetStringUTFChars这样的JNI函数来访问字符串的内容。所有的JNI引用都是jobject 类型,对了使用方便和类型安全,JNI定义了一个引用类型集合,集合当中的所有类型都是jobject的子类型。这些子类型和JAVA中常用的引用类型相对应。例如,jstring表示字符串,jobjectArray表示对象数组。
本文主要介绍引用类型中的数组类型,在这里我们主要介绍2种情况:基本类型数组、引用类型数组。
基本类型数组
Java中基本类型数组在JNI都有对应的类型,如jintArray、jfloatArray等。
如下介绍一个示例,该示例演示了如何在C中访问Java中数组元素,并返回该数组的和给Java层。
public native int getSum(int[] array); JNIMethod method = new JNIMethod(); int[] array = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }; int sum = method.getSum(array); System.out.println(sum); /* * Class: com_sunny_demo_JNIMethod * Method: getSum * Signature: ([I)I */ JNIEXPORT jint JNICALL Java_com_sunny_demo_JNIMethod_getSum (JNIEnv * env, jobject jobj, jintArray array){ jint buf[10]; jint sum = 0; (*env)->GetIntArrayRegion(env, array, 0, 10, buf); for (int i = 0; i < 10; i++){ sum += buf[i]; } printf("sum:%d \n",sum); return sum; }
GetIntArrayRegion函数把一个Java层的int数组array中的所有元素复制到一个C缓冲区buf中,然后我们在本地代码中通过C缓冲区来访问这些元素。
JNI支持一个与GetIntArrayRegion相对应的函数SetIntArrayRegion,这个函数允许本地代码修改所有的基本类型数组中的元素。
如下示例是直接返回一个int类型数组给Java层。
JNIEXPORT jintArray JNICALL Java_com_sunny_demo_JNIMethod_getIntArray (JNIEnv * env, jobject jobj){ jintArray jArray = (*env)->NewIntArray(env, 10); jint *array = (*env)->GetIntArrayElements(env, jArray, NULL); for (int i = 0; i < 10; i++){ array[i] = i * 10; } (*env)->ReleaseIntArrayElements(env, jArray, array, JNI_OK); return jArray; }
在示例中NewIntArray和GetIntArrayElements这里不做过多解释,首先创建一个包含10个jint类型的数组,然后拿到jintArray中对应的C类型数据。
ReleaseIntArrayElements是释放本地代码array所占用的内存,但是它跟最后一个参数值有关系,最后一个参数取值包含了如下三种情况:
- JNI_OK:把数据复制回源数组并释放elems缓冲区(对应示例中C代码array指针);
- JNI_COMMIT:把数据复制回源数组但不释放elems缓冲区;
- JNI_ABORT:不把数据复制回源数组,释放elems缓冲区。
引用类型数组
JNI提供了一个函数对来访问对象数组。其实在JNI中,除了基本数据类型之外,其它涉及到引用类型的基本上都有相应的函数对,如NewXXX和ReleaseXXX,GetXXX和SetXXX。
在访问引用类型数组时,GetObjectArrayElement返回数组中指定位置的元素,而SetObjectArrayElement修改数组中指定位置的元素。与基本类型的数组不同的是,在这里,我们不能一次得到所有的对象元素或者一次复制多个对象元素。字符串和数组都是引用类型,我们要使用Get/SetObjectArrayElement来访问数组中单个引用。
JNIEXPORT jobjectArray JNICALL Java_com_sunny_demo_JNIMethod_getStrArray (JNIEnv * env, jobject jobj){ jclass jclazz = (*env)->FindClass(env, "Ljava/lang/String;"); char *data[5] = { "A", "B", "C", "D", "E" }; jobjectArray objArray = (*env)->NewObjectArray(env, 5, jclazz, NULL); for (int i = 0; i < 5; i++){ jstring elem = (*env)->NewStringUTF(env, data[i]); (*env)->SetObjectArrayElement(env, objArray, i, elem); } return objArray; }
小结
通过本文介绍,我们不仅知道了JNI中数据类型和Java数据类型的对应关系,还知道了Java中方法和字段描述符在JNI中的表示方式,比如Java中java.lang.String类型,在JNI中数据类型描述符是用“java/lang/String”表示,签名则是使用“Ljava/lang/String;”,注意如果表示对象类型,后面需要跟上分号“;”。最后结合了具体示例,演示了Java和C之间基本数据类型交互方式,还介绍了数组类型的交互方式。
后续还会继续JNI相关内容的介绍,下一篇博文中我们重点介绍有关字符串和编码格式的相关内容。
参看资料
JNI编程指南