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

简介: 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;
}

1.png

可以发现,经过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就会存在错误。(这个很致命,排查了很久发现)

image.png

其次,改变Integer的值后,也会引发后续Integer变量的使用发生越界情况,毕竟Integer的值范围是[-128,127],那么如果内部改变后,再使用越界就不可控,给后续带来错误。

1.png


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层进行使用。如果有其他的或者更好的方法可在评论区中提出来呀,文中有错误也可指正。

目录
相关文章
|
13天前
|
Java 测试技术 应用服务中间件
常见 Java 代码缺陷及规避方式(下)
常见 Java 代码缺陷及规避方式(下)
39 0
|
15天前
|
Java
Java中ReentrantLock释放锁代码解析
Java中ReentrantLock释放锁代码解析
25 8
|
13天前
|
Java
代码的魔法师:Java反射工厂模式详解
代码的魔法师:Java反射工厂模式详解
26 0
|
13天前
|
监控 安全 Java
常见 Java 代码缺陷及规避方式(中)
常见 Java 代码缺陷及规避方式(中)
27 1
|
15天前
|
设计模式 算法 Java
23种设计模式,模板方法模式的概念优缺点以及JAVA代码举例
【4月更文挑战第10天】模板方法模式是一种行为设计模式,它定义了一个操作中的算法的骨架,而将一些步骤延迟到子类中。模板方法使得子类可以在不改变算法结构的情况下,重新定义算法中的某些特定步骤。
15 0
|
16天前
|
设计模式 Java
23种设计模式,状态模式的概念优缺点以及JAVA代码举例
【4月更文挑战第9天】状态模式是一种行为设计模式,允许一个对象在其内部状态改变时改变它的行为,这个对象看起来似乎修改了它的类。
29 4
|
16天前
|
算法 安全 Java
java代码 实现AES_CMAC 算法测试
该代码实现了一个AES-CMAC算法的简单测试,使用Bouncy Castle作为安全提供者。静态变量K定义了固定密钥。`Aes_Cmac`函数接受密钥和消息,返回AES-CMAC生成的MAC值。在`main`方法中,程序对给定的消息进行AES-CMAC加密,然后模拟接收ECU的加密结果并进行比较。如果两者匹配,输出"验证成功",否则输出"验证失败"。辅助方法包括将字节转为16进制字符串和将16进制字符串转为字节。
|
1天前
|
缓存 Java
【Java基础】简说多线程(上)
【Java基础】简说多线程(上)
5 0