JNI内形参从C代码中获取返回值并返回到Java层使用

Posted nanke_yh

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了JNI内形参从C代码中获取返回值并返回到Java层使用相关的知识,希望对你有一定的参考价值。

目录

一、标题介绍

二、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层写了两个方法分别模拟这个需求,在底层对arg1arg2参数做操作,之后将结果存入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层进行使用。如果有其他的或者更好的方法可在评论区中提出来呀,文中有错误也可指正。

以上是关于JNI内形参从C代码中获取返回值并返回到Java层使用的主要内容,如果未能解决你的问题,请参考以下文章

JNI内两种方式从C/C++中传递一维二维三维数组数据至Java层详细梳理

我的C/C++语言学习进阶之旅JNI开发之转换C层返回的结构体为Java实体Bean

我的C/C++语言学习进阶之旅JNI开发之转换C层返回的结构体为Java实体Bean

我的C/C++语言学习进阶之旅JNI开发之转换C层返回的结构体为Java实体Bean

如何使用 JNI 从本机 c 库将 double 和 unsigned int 返回到 java

将参数从 C/C++ JNI 传递到 Java 并获取修改后的值