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

目录
相关文章
|
6天前
|
Java
在 Java 中捕获和处理自定义异常的代码示例
本文提供了一个 Java 代码示例,展示了如何捕获和处理自定义异常。通过创建自定义异常类并使用 try-catch 语句,可以更灵活地处理程序中的错误情况。
|
21天前
|
XML 安全 Java
Java反射机制:解锁代码的无限可能
Java 反射(Reflection)是Java 的特征之一,它允许程序在运行时动态地访问和操作类的信息,包括类的属性、方法和构造函数。 反射机制能够使程序具备更大的灵活性和扩展性
33 5
Java反射机制:解锁代码的无限可能
|
17天前
|
jenkins Java 测试技术
如何使用 Jenkins 自动发布 Java 代码,通过一个电商公司后端服务的实际案例详细说明
本文介绍了如何使用 Jenkins 自动发布 Java 代码,通过一个电商公司后端服务的实际案例,详细说明了从 Jenkins 安装配置到自动构建、测试和部署的全流程。文中还提供了一个 Jenkinsfile 示例,并分享了实践经验,强调了版本控制、自动化测试等关键点的重要性。
48 3
|
22天前
|
存储 安全 Java
系统安全架构的深度解析与实践:Java代码实现
【11月更文挑战第1天】系统安全架构是保护信息系统免受各种威胁和攻击的关键。作为系统架构师,设计一套完善的系统安全架构不仅需要对各种安全威胁有深入理解,还需要熟练掌握各种安全技术和工具。
61 10
|
18天前
|
分布式计算 Java MaxCompute
ODPS MR节点跑graph连通分量计算代码报错java heap space如何解决
任务启动命令:jar -resources odps-graph-connect-family-2.0-SNAPSHOT.jar -classpath ./odps-graph-connect-family-2.0-SNAPSHOT.jar ConnectFamily 若是设置参数该如何设置
|
16天前
|
Java
Java代码解释++i和i++的五个主要区别
本文介绍了前缀递增(++i)和后缀递增(i++)的区别。两者在独立语句中无差异,但在赋值表达式中,i++ 返回原值,++i 返回新值;在复杂表达式中计算顺序不同;在循环中虽结果相同但使用方式有别。最后通过 `Counter` 类模拟了两者的内部实现原理。
Java代码解释++i和i++的五个主要区别
|
24天前
|
搜索推荐 Java 数据库连接
Java|在 IDEA 里自动生成 MyBatis 模板代码
基于 MyBatis 开发的项目,新增数据库表以后,总是需要编写对应的 Entity、Mapper 和 Service 等等 Class 的代码,这些都是重复的工作,我们可以想一些办法来自动生成这些代码。
30 6
|
6月前
|
Java
使用Java代码打印log日志
使用Java代码打印log日志
315 1
|
Java BI API
在Java代码中打日志需要注意什么?
日志是什么?日志是你在代码运行时打印出来的一些数据和记录,是快速排查问题的好帮手,是撕逼和甩锅的利器!
701 0
|
缓存 Java 网络架构
别在 Java 代码里乱打日志了,这才是正确的打日志姿势!
别在 Java 代码里乱打日志了,这才是正确的打日志姿势!
167 0