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

目录
相关文章
|
2天前
|
安全 Java 编译器
深入理解Java中synchronized三种使用方式:助您写出线程安全的代码
`synchronized` 是 Java 中的关键字,用于实现线程同步,确保多个线程互斥访问共享资源。它通过内置的监视器锁机制,防止多个线程同时执行被 `synchronized` 修饰的方法或代码块。`synchronized` 可以修饰非静态方法、静态方法和代码块,分别锁定实例对象、类对象或指定的对象。其底层原理基于 JVM 的指令和对象的监视器,JDK 1.6 后引入了偏向锁、轻量级锁等优化措施,提高了性能。
12 3
|
28天前
|
Java
java小工具util系列4:基础工具代码(Msg、PageResult、Response、常量、枚举)
java小工具util系列4:基础工具代码(Msg、PageResult、Response、常量、枚举)
50 24
|
10天前
|
前端开发 Java 测试技术
java日常开发中如何写出优雅的好维护的代码
代码可读性太差,实际是给团队后续开发中埋坑,优化在平时,没有那个团队会说我专门给你一个月来优化之前的代码,所以在日常开发中就要多注意可读性问题,不要写出几天之后自己都看不懂的代码。
48 2
|
24天前
|
Java 编译器 数据库
Java 中的注解(Annotations):代码中的 “元数据” 魔法
Java注解是代码中的“元数据”标签,不直接参与业务逻辑,但在编译或运行时提供重要信息。本文介绍了注解的基础语法、内置注解的应用场景,以及如何自定义注解和结合AOP技术实现方法执行日志记录,展示了注解在提升代码质量、简化开发流程和增强程序功能方面的强大作用。
64 5
|
24天前
|
存储 算法 Java
Java 内存管理与优化:掌控堆与栈,雕琢高效代码
Java内存管理与优化是提升程序性能的关键。掌握堆与栈的运作机制,学习如何有效管理内存资源,雕琢出更加高效的代码,是每个Java开发者必备的技能。
49 5
|
26天前
|
Java API 开发者
Java中的Lambda表达式:简洁代码的利器####
本文探讨了Java中Lambda表达式的概念、用途及其在简化代码和提高开发效率方面的显著作用。通过具体实例,展示了Lambda表达式如何在Java 8及更高版本中替代传统的匿名内部类,使代码更加简洁易读。文章还简要介绍了Lambda表达式的语法和常见用法,帮助开发者更好地理解和应用这一强大的工具。 ####
|
23天前
|
安全 Java API
Java中的Lambda表达式:简化代码的现代魔法
在Java 8的发布中,Lambda表达式的引入无疑是一场编程范式的革命。它不仅让代码变得更加简洁,还使得函数式编程在Java中成为可能。本文将深入探讨Lambda表达式如何改变我们编写和维护Java代码的方式,以及它是如何提升我们编码效率的。
|
Java Android开发
深入解析Java重写 - 返回值、修饰符必须一致?
深入解析Java重写 - 返回值、修饰符必须一致?
419 0
|
2天前
|
安全 Java Kotlin
Java多线程——synchronized、volatile 保障可见性
Java多线程中,`synchronized` 和 `volatile` 关键字用于保障可见性。`synchronized` 保证原子性、可见性和有序性,通过锁机制确保线程安全;`volatile` 仅保证可见性和有序性,不保证原子性。代码示例展示了如何使用 `synchronized` 和 `volatile` 解决主线程无法感知子线程修改共享变量的问题。总结:`volatile` 确保不同线程对共享变量操作的可见性,使一个线程修改后,其他线程能立即看到最新值。
|
2天前
|
消息中间件 缓存 安全
Java多线程是什么
Java多线程简介:本文介绍了Java中常见的线程池类型,包括`newCachedThreadPool`(适用于短期异步任务)、`newFixedThreadPool`(适用于固定数量的长期任务)、`newScheduledThreadPool`(支持定时和周期性任务)以及`newSingleThreadExecutor`(保证任务顺序执行)。同时,文章还讲解了Java中的锁机制,如`synchronized`关键字、CAS操作及其实现方式,并详细描述了可重入锁`ReentrantLock`和读写锁`ReadWriteLock`的工作原理与应用场景。