第一篇文章终于写完…跨行三年,一直都是看别人的文章…今天咱终于自己写了一篇,自己总结的,希望能给你一点点帮助,如有错误,希望指出,立马改正。
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++动态链接库提供了一种方法。
若调用本地的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
总结一下调用分为五部:
注意:如果用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)》最后一章”本地方法“。
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/