# JNI(Java Native Interface)的参数传递和返回 ### Java数据类型与C/C++本地数据类型的映射关系 - Java的基本数据类型表示映射到C/C++的表示都是在原来的标识字符加上字符"j" > 例如:Java的int类型在C/C++中表示为jint类型 - 对象类型使用jobject表示 - "void"的表示在Java和C/C++中保持一致 在头文件"jni_md.h"中有如下定义 ``` typedef long jint; typedef __int64 jlong; typedef signed char jbyte; ``` > 头文件"jni_md.h"在JDk的"include/linux"目录下 在头文件"jni.h"中有如下定义 ``` typedef unsigned char jboolean; typedef unsigned short jchar; typedef short jshort; typedef float jfloat; typedef double jdouble; typedef jint jsize; ``` > 头文件"jni.h"在JDk的"include"目录下 映射关系如下表 | Java类型 | 本地类型(C/C++类型) | JNI中定义的别名 | 描述 | | -------- | ------------------- | --------------- | ---------------- | | int | long | jint | 有符号的32位表示 | | long | _int64 | jlong | 有符号的64位表示 | | byte | signed char | jbyte | 有符号的8位表示 | | boolean | unsigned char | jboolean | 无符号的8位表示 | | char | unsigned short | jchar | 无符号16位表示 | | short | short | jshort | 有符号16位表示 | | float | float | jfloat | 32位表示 | | double | double | jdouble | 64位表示 | | Object | _jobject* | jobject | 对象类型 | | void | void | void | | Java基本数据类型中除了boolean类型,其他数据类型都可以直接转换为C/C++的基本数据类型 在C/C++中使用常量"JNI_TRUE"表示"true",常量"JNI_FALSE"表示"false" > ``` > //jint类型可以直接当作int类型使用 > int a; > jint b = a; > long c = b; > //jboolean使用unsigned char表示 > //常量"JNI_TRUE"表示"true" > //常量"JNI_FALSE"表示"false" > jboolean ztrue = JNI_TRUE; > jboolean zfasle = JNI_FALSE; > ``` ### Java数据类型的类型签名表示(与Java VM Type Signatures规范有关) > JNI中进行对象相关的操作时需要用到Java数据类型的符号表示 Java数据类型与类型签名映射关系表 | Type Signature(类型签名) | Java Type(Java类型) | | :-----------------------: | :-------------------: | | Z | boolean | | B | byte | | C | char | | S | short | | I | int | | J | long | | F | float | | D | double | | L fully-qualified-class ; | fully-qualified-class | | [ type | type[] | | ( arg-types ) ret-type | method type` | | V | void | #### 类型签名案例 - `int`类型的类型签名为:`I` - `boolean`类型的类型签名为:`D` - `java.lang.String`类型的类型签名为:`Ljava/lang/String` > "L"表示为对象类型,"Ljava/lang/String"表示具体的对象类型为"java.lang.String" - `int[]`类型的类型签名为:`[I` > 一个左中括号"["表示为一维数组,完整"[int"表示为int类型的一维数组 - `int[][]`类型的类型签名为:`[[I` > 两个左中括号"[["表示为二维数组,完整"[[nt"表示为int类型的二维数组 > > 总结:有多少个左中括号就表示是多少维数组 - `java.lang.String[]`类型的类型签名为:`[Ljava/lang/String` - 例如,有如下方法`public static void main(String[] agrs);`对应的方法类型签名为:`([Ljava/lang/String;)V` > - "()":一对小括号表示方法参数列表 > - "[":一个左中括号表示一维数组 > - "L":表示对象类型 > - "java/lang/String":表示类"java.lang.String" > - "Ljava/lang/String;":表示"java.lang.String"这个对象类型 > - "V":在这里表示返回值,返回类型为void > > 小括号外的类型签名表示方法的返回值类型 > > 组合在一起的完整含义为,接收一个参数为字符串数组,返回值为void类型的方法 - 再例如`int sum(int a,int b);`对应的类型签名为`(II)I` > 含义为有两个参数类型为int,返回值类型为int的方法 - `String method(String str1,String str2);`对应的类型签名为`(Ljava/lang/String;Ljava/lang/String)Ljava/lang/String` > 参数列表有多个对象类型的参数时,对象类型的类型签名结尾需要使用分号分割 ### 案例:基本类型的参数传递和返回 - Java代码 ``` package space.anyi.jni.parameter; public class Main { static { System.load("/home/yangyi/JNI-learn/c/parameter/JNI_parameter.so"); } public static void main(String[] args) { BaseTypeTest baseTypeTest = new BaseTypeTest(); int sum = baseTypeTest.sum(1, 2); System.out.println(sum); boolean flag = baseTypeTest.booleanTypeTest(false); System.out.println(flag); } } class BaseTypeTest { //定义一个有参数和返回值的native方法 native int sum(int val1,int val2); //boolean类型的测试 native boolean booleanTypeTest(boolean flag); } ``` - 生成的头文件内容 生成头文件的命令:`javac -h c/parameter src/main/java/space/anyi/jni/parameter/Main.java` ``` /* DO NOT EDIT THIS FILE - it is machine generated */ #include /* Header for class space_anyi_jni_parameter_BaseTypeTest */ #ifndef _Included_space_anyi_jni_parameter_BaseTypeTest #define _Included_space_anyi_jni_parameter_BaseTypeTest #ifdef __cplusplus extern "C" { #endif /* * Class: space_anyi_jni_parameter_BaseTypeTest * Method: sum * Signature: (II)I */ JNIEXPORT jint JNICALL Java_space_anyi_jni_parameter_BaseTypeTest_sum (JNIEnv *, jobject, jint, jint); /* * Class: space_anyi_jni_parameter_BaseTypeTest * Method: booleanTypeTest * Signature: (Z)Z */ JNIEXPORT jboolean JNICALL Java_space_anyi_jni_parameter_BaseTypeTest_booleanTypeTest (JNIEnv *, jobject, jboolean); #ifdef __cplusplus } #endif #endif ``` - 本地方法的实现 ``` #include "space_anyi_jni_parameter_BaseTypeTest.h" #include int sum(int a,int b){ return a+b; } JNIEXPORT jint JNICALL Java_space_anyi_jni_parameter_BaseTypeTest_sum(JNIEnv *env, jobject object, jint val1, jint val2){ //jint类型直接当作int类型使用 int temp = sum(val1,val2); jint result = temp; return result; } JNIEXPORT jboolean JNICALL Java_space_anyi_jni_parameter_BaseTypeTest_booleanTypeTest(JNIEnv *env, jobject object, jboolean flag){ //jboolean类型当作unsigned char类型使用 unsigned char val = flag; printf("%d\n",val); //在C/C++中使用常量"JNI_TRUE"表示"true",常量"JNI_FALSE"表示"false" return val ? JNI_FALSE : JNI_TRUE; } ``` - 编译为动态链接库 中间编译 ``` gcc -c -fPIC -I"$JAVA_HOME/include" -I"$JAVA_HOME/include/linux" -o c/parameter/JNI_parameter.o c/parameter/JNI_parameter.c ``` 动态链接库编译 ``` gcc -shared -fPIC -o c/parameter/JNI_parameter.so c/parameter/JNI_parameter.o -lc ``` - 运行测试:`java -cp src/main/java space.anyi.jni.parameter.Main` 输出结果 ``` yangyi@debain12:~/JNI-learn$ java -cp src/main/java space.anyi.jni.parameter.Main 3 0 true ``` ### 案例:引用类型(对象类型)的参数传递和返回 JNI中关于对象的操作与Java的类反射机制非常相似 JNI中对象抽象与Java类反射机制抽象的映射表 | Java 类型反射抽象 | JNI 类型/句柄 | 说明 | | -------------------------- | ------------------------- | ------------------------------------------------------------ | | `java.lang.Class` | `jclass` | 表示 Java 类对象,可通过 `FindClass`、`GetObjectClass` 等获取 | | `java.lang.reflect.Method` | `jmethodID` | 表示 Java 方法,可通过 `GetMethodID`、`GetStaticMethodID` 获取 | | `java.lang.reflect.Field` | `jfieldID` | 表示 Java 字段,可通过 `GetFieldID`、`GetStaticFieldID` 获取 | | `java.lang.Object` | `jobject` | 表示 Java 对象实例的通用引用 | | `java.lang.String` | `jstring` | 表示 Java 字符串对象,是 `jobject` 的子类型 | | `java.lang.Throwable` | `jthrowable` | 表示 Java 异常对象,是 `jobject` 的子类型 | | 数组类型(如 `Object[]`) | `jarray` / `jobjectArray` | 通用数组句柄,具体类型如 `jintArray`、`jobjectArray` 等 | - Java代码,通过本地方法获取一个对象(对象类型的返回),通过本地方法输出一个对象的信息(对象类型的入参) ``` package space.anyi.jni.parameter; import java.util.Arrays; public class Main { static { System.load("/home/yangyi/JNI-learn/c/parameter/JNI_object_parameter.so"); } public static void main(String[] args) { //JNI对象数据类型的传递 ObjectTest objectTest = new ObjectTest(); Student student = objectTest.getStudent(); System.out.println(student); objectTest.printStudentInfo(student); System.out.println(student); // objectTest.free(student); } } class ObjectTest{ /** * 通过JNI获取对象,返回的是全局的JNI引用,需要手动管理,有内存泄露的风险 * @return */ native Student getStudent(); native void printStudentInfo(Student student); /** * 释放JNI创建的全局引用 * @param student */ native void free(Student student); } class Student{ private String name; private int age; private float height; private int[] source; private String[] hobby; public Student(String name) { this.name = name; } public int getAge() { return age; } public void setAge(int age) { this.age = age; } public float getHeight() { return height; } public void setHeight(float height) { this.height = height; } public int[] getSource() { return source; } public void setSource(int[] source) { this.source = source; } public String[] getHobby() { return hobby; } public void setHobby(String[] hobby) { this.hobby = hobby; } public String getName() { return name; } public void setName(String name) { this.name = name; } public void info(){ System.out.println("My name is %s,i am %d year old,height:%f".formatted(this.name,this.age,this.height)); } @Override public String toString() { return "Student{" + "name='" + name + '\'' + ", age=" + age + ", height=" + height + ", source=" + Arrays.toString(source) + ", hobby=" + Arrays.toString(hobby) + '}'; } } ``` - 编译头文件:`javac -h c/parameter/ src/main/java/space/anyi/jni/parameter/Main.java` 得到文件"space_anyi_jni_parameter_ObjectTest.h" ``` /* DO NOT EDIT THIS FILE - it is machine generated */ #include /* Header for class space_anyi_jni_parameter_ObjectTest */ #ifndef _Included_space_anyi_jni_parameter_ObjectTest #define _Included_space_anyi_jni_parameter_ObjectTest #ifdef __cplusplus extern "C" { #endif /* * Class: space_anyi_jni_parameter_ObjectTest * Method: getStudent * Signature: ()Lspace/anyi/jni/parameter/Student; */ JNIEXPORT jobject JNICALL Java_space_anyi_jni_parameter_ObjectTest_getStudent (JNIEnv *, jobject); /* * Class: space_anyi_jni_parameter_ObjectTest * Method: printStudentInfo * Signature: (Lspace/anyi/jni/parameter/Student;)V */ JNIEXPORT void JNICALL Java_space_anyi_jni_parameter_ObjectTest_printStudentInfo (JNIEnv *, jobject, jobject); /* * Class: space_anyi_jni_parameter_ObjectTest * Method: free * Signature: (Lspace/anyi/jni/parameter/Student;)V */ JNIEXPORT void JNICALL Java_space_anyi_jni_parameter_ObjectTest_free (JNIEnv *, jobject, jobject); #ifdef __cplusplus } #endif #endif ``` - 实现本地方法 > C语言与C++语言使用env指针的API不一样,C语言比C++语言版本的API多一对小括号和多传递一个参数 > > ``` > //C语言 > (*env)->functionName(env,args...) > //C++语言 > env->functionName(args...) > ``` native方法中创建的对象,如果需要在JVM中使用,需要创建全局引用,并将全局引用返回给JVM > 通过NewGlobalRefh函数创建全局引用 > > ``` > //创建JNI全局引用,以便JVM可以使用JNI中创建的对象 > jobject result = (*env)->NewGlobalRef(env,student); > return result; > ``` > > 通过DeleteGlobalRef函数释放全局引用 > > ``` > (*env)->DeleteGlobalRef(env,ref); > ``` ``` #include "space_anyi_jni_parameter_ObjectTest.h" JNIEXPORT jobject JNICALL Java_space_anyi_jni_parameter_ObjectTest_getStudent(JNIEnv *env, jobject this){ printf("start:getStudent\n"); //C语言与C++语言使用env指针的方式不一样,C语言比C++语言版本的api多一对小括号和多传递一个参数 //C语言:(*env)->functionName(env,args...) //C++语言:env->functionName(args...) //1.通过JNIEnv指针获取指定类的jclass,通过类的类型签名指定 jclass class_Student = (*env)->FindClass(env,"Lspace/anyi/jni/parameter/Student;"); //2.获取类的构造方法,第一个参数是类的jclass,第二个参数是方法名,第三个参数是方法的类型签名 //构造方法的名称为:,构造方法的返回值为void jmethodID method_constructor_Student = (*env)->GetMethodID(env,class_Student,"","(Ljava/lang/String;)V"); //3.调用构造方法创建对象,第一个参数是类的jclass,第二参数为jmethodID,从第三个参数开始为方法的入参 jstring nameStr = (*env)->NewStringUTF(env, "yangyi"); jobject student = (*env)->NewObject(env,class_Student,method_constructor_Student,nameStr); //4.获取类的普通方法,第一个参数是jclass,第二个参数是方法的名称,第三个参数方法的类型签名 jmethodID student_method_setAge = (*env)->GetMethodID(env,class_Student,"setAge","(I)V"); //5.调用类的普通方法,第一个参数是将要调用该方法实例对象的jobject,第二个参数是方法对应的jmethodID,从第三个参数开始为方法的入参 (*env)->CallObjectMethod(env,student,student_method_setAge,18); //6.获取类的普通字段,第一个参数是类的jcalss,第二个参数是字段的名称,第三个参数是字段的类型签名 jfieldID student_field_height = (*env)->GetFieldID(env,class_Student,"height","F"); //7.给类的普通字段设置值,第一个参数是类实例对应的jobject,第二个参数是字段对应的jfieldID,第三个参数是要设置的目标值 (*env)->SetFloatField(env,student,student_field_height,1.66); //8.释放局部引用资源 (*env)->DeleteLocalRef(env, class_Student); //创建JNI全局引用,以便JVM可以使用JNI中创建的对象 jobject result = (*env)->NewGlobalRef(env,student); printf("end:getStudent\n"); return result; } JNIEXPORT void JNICALL Java_space_anyi_jni_parameter_ObjectTest_printStudentInfo(JNIEnv *env, jobject this, jobject student){ printf("start:printStudentInfo\n"); //1.调用对象的普通方法 //1.1获取jclass jclass class_Student = (*env)->GetObjectClass(env,student); //1.2获取jmethodID jmethodID student_method_info = (*env)->GetMethodID(env,class_Student,"info","()V"); //1.3call (*env)->CallObjectMethod(env,student,student_method_info); //2.获取对象的字段值 //2.1获取jfieldID jfieldID student_field_height = (*env)->GetFieldID(env,class_Student,"height","F"); //2.2获取对象字段对应的值 jfloat height = (*env)->GetFloatField(env,student,student_field_height); printf("height:%f\n",height); //基本类型的数组操作 //3.1创建数组 jint size = 3; jintArray newArray = (*env)->NewIntArray(env, size); //3.2设置数组中的值 //第一个参数为数组的jarray,第二个参数为数组的默认值 jint *source = (*env)->GetIntArrayElements(env, newArray, NULL); source[0] = 90; source[1] = 91; source[2] = 92; //3.3获取数组中的值 jint val1 = source[0]; jint val2 = source[1]; jint val3 = source[2]; //更新数组的修改,第一个参数为数组的jarray,第二个参数为C/C++中数组,第三个参数为模式(0:将数组拷贝到jarray并释放C/C++中数组的内存;1:拷贝数组但不释放内存;2:不拷贝但释放内存) (*env)->ReleaseIntArrayElements(env, newArray, source,0); //获取数组的长度 jsize length = (*env)->GetArrayLength(env, newArray); jfieldID student_field_source = (*env)->GetFieldID(env,class_Student,"source","[I"); (*env)->SetObjectField(env,student,student_field_source,newArray); //对象类型的数组操作 jclass class_String = (*env)->FindClass(env,"Ljava/lang/String;"); //第一个参数为数组的大小,第二个参数为对象的jclass,第三个参数为数组默认值 jobjectArray objectArray = (*env)->NewObjectArray(env, size,class_String,NULL); //对象数组赋值 jint index = 0; jstring hobby = (*env)->NewStringUTF(env,"学习"); //第一个参数为jarray,第二个参数为索引,第三个参数为值的jobject (*env)->SetObjectArrayElement(env,objectArray,index,hobby); //对象数组取值,第一个参数为jarray,第二个参数为索引 jobject value = (*env)->GetObjectArrayElement(env,objectArray,index); jfieldID student_field_hobby = (*env)->GetFieldID(env,class_Student,"hobby","[Ljava/lang/String;"); (*env)->SetObjectField(env,student,student_field_hobby,objectArray); //释放局部引用资源,可选 (*env)->DeleteLocalRef(env, class_Student); printf("end:printStudentInfo\n"); } JNIEXPORT void JNICALL Java_space_anyi_jni_parameter_ObjectTest_free(JNIEnv *env, jobject this, jobject student){ printf("start:free\n"); if(student != NULL){ printf("释放全局引用\n"); (*env)->DeleteGlobalRef(env,student); } printf("end:free\n"); } ``` - 编译动态链接库 ``` gcc -c -fPIC -I"$JAVA_HOME/include" -I"$JAVA_HOME/include/linux" -o c/parameter/JNI_object_parameter.o c/parameter/JNI_object_parameter.c gcc -shared -fPIC c/parameter/JNI_object_parameter.so c/parameter/JNI_object_parameter.o -lc ``` > 得到动态链接库"JNI_object_parameter.so" - 测试输出:`java -cp src/main/java/ space.anyi.jni.parameter.Main` ``` yangyi@debain12:~/JNI-learn$ java -cp src/main/java/ space.anyi.jni.parameter.Main start:getStudent end:getStudent Student{name='yangyi', age=18, height=1.66, source=null, hobby=null} start:printStudentInfo My name is yangyi,i am 18 year old,height:1.660000 height:1.660000 end:printStudentInfo Student{name='yangyi', age=18, height=1.66, source=[90, 91, 92], hobby=[学习, null, null]} ``` ### 参考资料 [JNI 使用笔记](https://icejoywoo.github.io/2022/08/22/intro-to-jni.html) [案例代码仓库](https://git.anyi.space/yangyi/JNI_learn)