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编程指南

评论

您确定要删除吗?删除之后不可恢复