(JNI/JNA)java 调用c/c++ 动态链接库 全套操作+踩坑集锦

第一篇文章终于写完…跨行三年,一直都是看别人的文章…今天咱终于自己写了一篇,自己总结的,希望能给你一点点帮助,如有错误,希望指出,立马改正。

0 前言

Java代码是跨平台的,其与硬件环境彻底“隔离”,为了实现这个目的,JDK1.0开始就包含了一个本地方法接口,它允许JAVA程序调用C/C++写的程序,许多第三方的程序和JAVA类库。如:java.lang,java.io,java.net等都依赖于本地方法来访问底层系统环境的特征。但是这有两个问题:

1、本地方法想访问C中的结构(structures)一样访问对象中的字段。尽管如此,JVM规范并没有定义对象怎么样在内存中实现。如果一个给定的JVM实现布局对象时,和本地方法假设的不一样,那你就不得不重新编写本地方法库。
2、因为本地方法可以保持对JVM中对象的直接指针,所以,JDK1.0中的本地方法采用了一种保守的GC策略。

为了解决这两个问题,JDK1.1中出现了JNI,这就为java代码调用c/c++动态链接库提供了一种方法。

1 JNI

若调用本地的C/C++的动态链接库,我们首先要通过java实现自己的本地方法,我们只需要利用JNI提供的关键字native把方法声明为是本地方法(由其他语言是实现的方法),然后再利用c/c++实现一个有相同方法名的动态链接库,并放在指定目录下即可供Java调用即可。
Window环境下生成动态链接库为.dll,需要配置头文件.h,详细实现过程看这篇文章,非常详细,博主亲测可实现https://blog.csdn.net/weixin_51763233/article/details/122205288
Linux环境下生成动态链接库为.so,和windw环境差不多,详细过程看这篇文章,博主亲测可实现:https://zhuanlan.zhihu.com/p/465601205

总结一下调用分为五部:

  1. java类中申请一个本地方法
  2. 运行javah以获取包含该方法的C声明的头文件
  3. c/c++实现本地方法
  4. 讲dll/so放在共享类库中
  5. java程序加载该类库

(JNI/JNA)java 调用c/c++ 动态链接库 全套操作+踩坑集锦_第1张图片
注意:如果用c++实现本地方法,需要用extern ”C“来声明,这样是为了不让使用c++编译器来编译本地方法,因为c++编译器编译可能会给方法加上后缀,导致Java无法找到本地方法的实现。

extern "C" {
   void externC(int a ,int b){}
}

其实Java和c/c++不能互通的原因主要是数据类型的问题,jni解决了这个问题,jni定义了一系列JNI数据类型,Java和c/c++数据类型通过JNI定义的数据类型进行转换:
例如:在进行数字传递的时候,我们知道在不同的平台c中int型所占字节数是不一样的,所以JNI定义了jint;
又例如:那个c文件中的jstring数据类型就是java传入的String对象,经过jni函数的转化就能成为c的char*。

对应关系类型(部分):

Java类型 JNI类型 描述
boolean jboolean unsigned char
byte jbyte signed 8 bits
char jchar unsigned 16 bits
short jshort signed 16 bits
int jint signed 32 bits
long jlong signed 64 bits
float jfloat 32 bits
double jdouble 64 bits
void void N/A

到此关于JNI,博主就介绍到这里,声明一点JNI是一个Java和其他语言互调的技术,也就是说你当然可以用JNI实现其他语言调用java程序,想继续深究的同学可以参考《Java核心技术(卷2)》最后一章”本地方法“。

2 JNA

JNI虽然实现了Java调用C/C++动态链接库,但是还是比较复杂的,尤其各种是数据类型的映射,于是乎,它来了-JNA(Java Native Access)。

JNA(Java Native Access )提供一组Java工具类用于在运行期间动态访问系统本地库(native library:如Window的dll)而不需要编写任何Native/JNI代码。开发人员只要在一个java接口中描述目标native library的函数与结构,JNA将自动实现Java接口到native function的映射。JNA(Java Native Access)框架是一个开源的Java框架,是SUN公司主导开发的,建立在经典的JNI的基础之上的一个框架。

JNA可以让你像调用一般java方法一样直接调用本地方法。就和直接执行本地方法差不多,而且调用本地方法还不用额外的其他处理或者配置什么的,也不需要多余的引用或者编码,使用很方便。具体使用方法不在赘述,入门使用看这篇文章:https://blog.csdn.net/zhan107876/article/details/121051129

JNA虽然使用起来方便,但坑也着实的多:
JNA指针
java是值传递的,没有指针(地址)的概念,但是c/c++是有指针的,有时候我们需要把变量传递过去然后获取变量新的值,这个时候我们必须引入指针的概念。好在JNA中引入了Pointer(com.sun.jna.Pointer)。Pointer是JNA中引入的类,用来表示native方法中的指针。
native方法中的指针实际上就是一个地址,这个地址就是真正对象的内存地址。

举个例子(参考https://blog.csdn.net/zhan107876/article/details/121056384):如果我们需要调用一个动态链接库,其实现这样的一个函数:

/**
 * 返回a+b的值
 * 同时c和msg通过参数返回
 */
int add(int a, int b, int *c, char **msg) {
    *c = (a + b) * 2;
    char *string = "hello world!";
    *msg = string;
    return a + b;
}

如果我们使用jna这么调用:

public class HelloJNA {

    /**
     * 定义一个接口,默认的是继承Library ,如果动态链接库里的函数是以stdcall方式输出的,那么就继承StdCallLibrary
     * 这个接口对应一个动态链接(SO)文件
     */
    public interface LibraryAdd extends Library {
        // 这里使用绝对路径加载
        LibraryAdd LIBRARY_ADD = Native.load("/program/cpp/libhello.so", LibraryAdd.class);
        int add(int a, int b, int c, String msg);
    }

    public static void main(String[] args) {
        int c = 0;
        String msg = "start";
        // 调用so映射的接口函数
        int add = LibraryAdd.LIBRARY_ADD.add(10, 15, c, msg);
        System.out.println("相加结果:" + add);
    }
}

那么不管add函数对c和msg做了何种改变,返回java中,值都不会被变更。

正确的调用方法是:

/**
 * 一个java类
 * 演示指针传输指针变量
 */
public class HelloJNA_Pointer {

    /**
     * 定义一个接口,默认的是继承Library ,如果动态链接库里的函数是以stdcall方式输出的,那么就继承StdCallLibrary
     * 这个接口对应一个动态链接(SO)文件
     */
    public interface LibraryAdd extends Library {

        LibraryAdd LIBRARY_ADD = Native.load("/program/cpp/libhello.so", LibraryAdd.class);

        /**
         * 指针变量,用Pointer类型定义
         * c是int*
         * msg是char**
         */
        int add_c(int a, int b, Pointer c, Pointer msg);

    }

    public static void main(String[] args) {

        Pointer c = new Memory(50);
        Pointer msg = new Memory(8);
        // 调用so映射的接口函数
        int add = LibraryAdd.LIBRARY_ADD.add_c(10, 15, c, msg);
        System.out.println("相加结果:" + add);

        // 指针变量
        System.out.println("c的值:" + c.getInt(0));
        // 这样才能拿到
        System.out.println("msg的值:" + msg.getPointer(0).getString(0));

        Native.free(Pointer.nativeValue(c));   //手动释放内存
        Pointer.nativeValue(c, 0);      //避免Memory对象被GC时重复执行Nativ.free()方法

        Native.free(Pointer.nativeValue(msg));   //手动释放内存
        Pointer.nativeValue(msg, 0);      //避免Memory对象被GC时重复执行Nativ.free()方法
    }
}

注意:在使用jna指针的时候需要首先申请一块内存

Pointer c = new Memory(50);
Pointer msg = new Memory(50);

最后在使用完之后手动的释放这片内存

Native.free(Pointer.nativeValue(c));     //手动释放内存
Pointer.nativeValue(c, 0);      		//避免Memory对象被GC时重复执行Nativ.free()方法
Native.free(Pointer.nativeValue(msg));   
Pointer.nativeValue(msg, 0);      

JNA结构体
java中没有结构体的概念,但是c/c++中结构体的使用非常频繁,为了解决这个问题,jna引入了类Structure ,如果在Java中定义一个结构体,只需要集成这个Structure 即可。Structure 初次使用,细节比较多,话不多说直接上代码,需要注意的地方会有注释:

//1:使用FieldOrder注解标注结构体成员的顺序,切记顺序一定不能弄错,
//不然结构体取出来的值会路透不对马嘴,因为结构体成员在内存中是依次排列的,
//如果顺序弄错jvm照着这个顺序按成员变量的类型取值肯定会错了
@Structure.FieldOrder(value= {"beginLoc","confidence","x","y","z","theta"})
public class PalletLocStruct extends Structure {
		//2:这个地方必须 public 不能private
        public int beginLoc;
        public float confidence = 1;
        public int x = 0;
        public int y = 0;
        public int z = 0;
        public float theta = 0;
        //3:这个地方具体原理不知道,只知道声明之后,当前结构体可以按照引用或者值取值
        public static class ByReference extends PalletLocStruct implements Structure.ByReference {}
        public static class ByValue extends PalletLocStruct implements Structure.ByValue{}
        //4:这个地方表示当前结构体是内存对齐的,因为c/c++ 的结构体成员多数情况是内存对齐,
        //可以想象如果c/c++内存对齐,而java没有内存对齐,那取出来的值肯定也对不上
        public PalletLocStruct() {
                super(ALIGN_NONE);
        }
}

上面代码中1-4点要非常注意,还有一点需要注意:如果调用动态链接库直接return 结构体,那么取值会出现一个非常诡异的现象:“取结构体的第一个成员变量没问题,但是第二个成团变量就对不上了”,我的猜想原因应该是Java中结构体每个成员在内存中内存顺序不是连续的,导致第一个成员可以正常取值,但是到了第二个就不能正常取值。目前我还没找到解决方法,所以如果想要返回一个结构体,那么最好用指针的方式,把结构体传给动态链接库,然后再取出结构体里面的值。比如我在项目中这样处理:
c++

 int palletDetection(palletLoc* palletLoc)
 {
 		palletLocs->confidence = 1;
        palletLocs->theta = 2;
        palletLocs->x = 3;
        palletLocs->y = 4;
        palletLocs->z = 5;
 }

java:

int palletDetection(PalletLocStruct palletLoc);

取值:
palletLoc.beginLoc ...

JNA数组
Java中数组不是一块连续的内存地址,但是在c/c++中是连续的,所以我们传递Java数组的时候,一定要声明一块连续的内存,且要保证这块内存不会够用,比如传递一个结构体数组:

//为调.so文件规划一片连续的内存空间  长度位10
PalletLocStruct[] array = (PalletLocStruct[])new PalletLocStruct().toArray(10);

写在最后

本来感觉万事俱备只待算法,最后部署联调的时候,出现了这个问题:

java.lang.UnsatisfiedLinkError: Unable to load library '/javaServer/videoSchedule/libpalletDetector.so':

一般这个异常多数是路径设置不对导致,排查一下java加载.so文件路径是否正确或者看一下dll或so的位数是否和jdk一致(32/64)。但是我遇到的是另外一种情况,算法同时编译的.so引用了两外两个.so库,但是他编译的时候没有把这两个.so加进来,所以导致无法加载。这里顺便贴个链接,有需要可以看一下嵌套.so生成https://www.freesion.com/article/46841131235/

你可能感兴趣的:(java,c++,c语言)