栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 软件开发 > 后端开发 > C/C++/C#

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

C/C++/C# 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

(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程序加载该类库


注意:如果用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类型描述
booleanjbooleanunsigned char
bytejbytesigned 8 bits
charjcharunsigned 16 bits
shortjshortsigned 16 bits
intjintsigned 32 bits
longjlongsigned 64 bits
floatjfloat32 bits
doublejdouble64 bits
voidvoidN/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):如果我们需要调用一个动态链接库,其实现这样的一个函数:

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 {

    
    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中,值都不会被变更。

正确的调用方法是:

public class HelloJNA_Pointer {

    
    public interface LibraryAdd extends Library {

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

        
        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/

转载请注明:文章转载自 www.mshxw.com
本文地址:https://www.mshxw.com/it/1011511.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号