目录
一、标题介绍
二、C/C++中修改形参值
1、使用&符号来实现修改值
2、外部数组,内部修改值
3、指针内存,内部修改值
三、JNI修改参数并返回到Java层使用
1、数组形参int[]
2、整型类形参Integer
3、自定义整型类MyInteger
一、标题介绍
这篇文章是基于项目中遇到的问题写的。要求如题所示,这里具体详细说明一下。工程的流程是基于C/C++语言进行底层的算法开发,再通过JNI获取结果,并拿到Java层运用。那么这里就要涉及到三层数据或值得传递了。作为中间层JNI如何将底层结果传出来呢?
大家肯定会说拿到结果直接return出来呀。是的,这种情况是可以;但是当return已经被其他需要返回值占用了呢,此时又需要再增加一个返回值用于java层的使用判断。当然还是有人会说把return的数据类型改变一下,多存一个返回值,然后再在Java层解析一下。这样的确能解决问题,但也就不会有这篇文了。
之所以有这篇文,还是因为有这样的需求和有这样的操作。在不改变已有的接口形式的前提下,如何增加一个形参然后通过形参将值传递到Java层呢。
二、C/C++中修改形参值
为给本次问题带来启示,我们先从C/C++编程中的通过形参传递值得方式来说明。对于基础的数据类型,c/c++编程实现形参值传递的方式如下:
1、使用&符号来实现修改值
int test1(int &rvalue) { rvalue = 3;//内部修改 return 0; }
2、外部数组,内部修改值
int test2(int rvalue[]) { rvalue[0] = 4;//内部修改 return 0; }
3、指针内存,内部修改值
int test3(int *rvalue) { *rvalue = 5;//内部修改 return 0; }
那么上述各自的测试结果:
int main() { //1、使用&符号来实现修改值 int rnt = 0; int a = 0; printf("value:%d\n",a); rnt = test1(a); printf("use & get value:%d\n",a); //2、外部数组,内部修改值 int b[1] = {0}; printf("value:%d\n",b[0]); rnt = test2(b); printf("use [] get value:%d\n",b[0]); //3、指针内存,内部修改值 int *c = new int[1]; memset(c,0,sizeof(int)); printf("value:%d\n",*c); rnt = test3(c); printf("use * get value:%d\n",*c); delete[]c; c=NULL; getchar(); return 0; }
可以发现,经过test函数的运行,三种方法都将初始value=0的值改变了。这三种方式的修改值方法,在c语言中其实都是存在内存值的写入。第一种int值实际还是在堆上存在4字节的内存,然后函数test1中改写该内存地址上的值。第二种int数组也是在堆上存在1个元素的4字节的内存,那么在函数test2中改写赋值这块内存的数据。第三种指针创建内存,在函数test3中去写入值。所以这三种方法都能实现形参修改值并传回主函数打印使用。当然这个对于二级指针,多级指针等也是一样的。那么对于JNI中这些方法能否使用呢?
三、JNI修改参数并返回到Java层使用
首先参考一文章中的案例,在java层和jni内分别模拟一下。用Demo来解释下需求和对应解决方案。
在Java层写了两个方法分别模拟这个需求,在底层对arg1和arg2参数做操作,之后将结果存入result中,希望能在Java层使用result,至于方法的返回值,则是模拟表示方法执行成功与否的标志位。
public class LibraryManager { static{ System.load("/home/linux_assist_navi/libassistnavi.so"); } public native int add1(int arg1, int arg2, int result); public native int add2(int arg1, int arg2, int result); }
下面分别在jni中用两种方式实现该需求,当然这两种都是典型错误的。
JNIEXPORT jint JNICALL Java_com_xxx_LibraryManager_add1( JNIEnv *jenv, jclass jcls, jint jarg1, jint jarg2, jint jarg3){ jarg3=jarg1+jarg2; LOGI("add1 arg3=%d", jarg3); return 1; } JNIEXPORT jint JNICALL Java_com_xxx_LibraryManager_add2( JNIEnv *jenv, jclass jcls, jint jarg1, jint jarg2, jint *jarg3){ int result=jarg1+jarg2; jarg3=&result; LOGI("add2 arg3=%d", *jarg3); return 2; }
java中没有指针的,从Java层模拟的接口中传入的参数都是int类型是没法获取内部值的。如果还记得jni的运行原理的话,应该很容易理解这么写只是修改在c线程里边的参数值(add1()中)和参数地址(add2()中),至于Java层对应的参数没有变化,也就是说jni中的基本类型作为参数时只是形参传入的,对于上层没有任何影响。
真正解决问题的方法是什么呢,请看下面吧。
在接口中增加几个新的方法:
public native int addConfirm1(int arg1, int arg2, int[] result); public native int addConfirm2(int arg1, int arg2, Integer result); public native int addConfirm3(int arg1, int arg2, MyInteger result);
主要做法就是需要传入的参数改为int[]和int对应的整型类,然后使用jni中的JNIEnv获取到方法来修改参数值。
1、数组形参int[]
JNIEXPORT jint JNICALL Java_com_bob_testlib_LibraryManager_addConfirm1( JNIEnv *jenv, jclass jcls, jint jarg1, jint jarg2, jintArray jarg3){ int result=jarg1+jarg2; int *arg3 = jenv->GetIntArrayElements(jarg3, 0); *arg3=result; jenv->ReleaseIntArrayElements(jarg3, arg3, 0); return 3; }
将int[]的参数传入,这样可以通过JNIEnv的GetIntArrayElements()获取到传入参数的地址并绑定到int*变量中,在修改变量之后,通过ReleaseIntArrayElements()通过第三个参数mode=0更新Java层jintArray的参数,并释放JNI层的int*变量。
2、整型类形参Integer
JNIEXPORT jint JNICALL Java_com_bob_testlib_LibraryManager_addConfirm2( JNIEnv *jenv, jclass jcls, jint jarg1, jint jarg2, jobject jarg3){ int result=jarg1+jarg2; jclass intClass = jenv->FindClass("java/lang/Integer"); jfieldID intId = jenv->GetFieldID(intClass, "value", "I"); jenv->SetIntField(jarg3, intId, result); return 4; }
将jobject参数传入,通过JNIEnv的FindClass()找到Java层Integer类对应jni层的jclass,再根据jclass通过JNIEnv的GetFiledID()找到Java层Integer类的value对应jni层的jclass的jfieldID,最后通过JNIEnv的SetIntField()将要更新的int值存入到Java层的jobject中即可。这个流程就是把Java层的Integer看成自定义的类,之后就是更新自定义类中的变量。
这个方法是有问题,在工程实践中会影响后续别的功能调用,如果单独功能可以使用,但是在工程中有后续使用和操作建议不用。排查发现主要是因为Integer是java自带的类,我们在接口中使用后改变了其内部值并返回,这里面会访问修改其private数据value,然后后续如果在使用中调用了Integer就会存在错误。(这个很致命,排查了很久发现)
其次,改变Integer的值后,也会引发后续Integer变量的使用发生越界情况,毕竟Integer的值范围是[-128,127],那么如果内部改变后,再使用越界就不可控,给后续带来错误。
3、自定义整型类MyInteger
在方法2的启示下,那么不使用java自带的类就行了,自己创建一个再使用不就好了嘛,如是就有:
public static class MyInteger{ private int value = 0; public MyInteger(int value){ this.value = value; } public int getValue(){ return value; } public void setValue(int value){ this.value = value; } }
然后jni函数
JNIEXPORT jint JNICALL Java_com_bob_testlib_LibraryManager_addConfirm2( JNIEnv *jenv, jclass jcls, jint jarg1, jint jarg2, jobject jarg3){ int result=jarg1+jarg2; jclass intClass = env->GetObjectClass(jarg3); jfieldID intId = jenv->GetFieldID(intClass, "value", "I"); jenv->SetIntField(jarg3, intId, result); return 5; }
解释一下(参考):
(1)调用GetObjectClass方法来获取Jclass,GetObjectClass的参数就是jarg3;
(2)调用GetFieldID方法来获取jfieldID,这里要说明一下Jni的全部操作,其实就是操作方法或者是操作属性两种。操作方法时须要根据方法的ID(jmethodID)来操作,能够理解为jmethodID标识了这个方法,也就是经过这个jmethodID能够找到你要找的方法。同理操作属性时也要根据该属性的ID(jfieldID )来操作。GetFieldID须要3个参数:第一个是上一步获取的Jclass,第二个参数是Java中的变量名(上面代码里我们就将变量message的值改为了“value”),最后一个参数是变量签名(int 的变量签名是”I“)。
至此,本文的问题就解决了。目前也是才总结出这三种方法方式来实现Jni修改参数值并返回到java层进行使用。如果有其他的或者更好的方法可在评论区中提出来呀,文中有错误也可指正。