1.JNA
JNA全称Java Native Access,是一个建立在经典的JNI技术之上的Java开源框架(https://github.com/twall/jna)。JNA提供一组Java工具类用于在运行期动态访问系统本地库(native library:如Window的dll)而不需要编写任何Native/JNI代码。开发人员只要在一个java接口中描述目标native library的函数与结构,JNA将自动实现Java接口到native function的映射。
JNA在线帮助文档:https://github.com/java-native-access/jna/blob/master/www/WindowsDevelopmentEnvironment.md
下载地址:https://github.com/java-native-access/jna/tags
2.数据类型
Java和C++的数据类型对照表
C++ | Java |
---|---|
char * | String |
word | short |
byte | byte |
byte[] | byte[] |
dword | int |
long | NativeLong |
Void * | Pointer |
lpvoid | Pointer |
lpDword | IntByReference |
HWND | HWND |
char[] | byte[] |
byte * | Pointer |
Java和C的数据类型对照表
Java | 类型 | C类型原生表现 |
---|---|---|
boolean | int | 32位整数(可定制) |
byte | char | 8位整数 |
char | wchar_t | 平台依赖 |
short | short | 16位整数 |
int | int | 32位整数 |
long | long,__int64 | 64位整数 |
float | float | 32位浮点数 |
double | double | 64位浮点数 |
Buffer/Pointer | pointer | 平台依赖(32或64位指针) |
没有 | pointer/array | 32或64位指针(参数/返回值)邻接内存(结构体成员) |
String | char* | /0结束的数组(nativeencodingorjna.encoding) |
WString | wchar_t* | /0结束的数组(unicode) |
String[] | char** | /0结束的数组的数组 |
WString[] | wchar_t** | /0结束的宽字符数组的数组 |
Structure | struct*/struct | 指向结构体的指针(参数或返回值)(或者明确指定是结构体指针)结构体(结构体的成员)(或者明确指定是结构体) |
Union | union | 等同于结构体 |
Structure[] | struct[] | 结构体的数组,邻接内存 |
Callback | (*fp)() | Java函数指针或原生函数指针 |
NativeMapped | varies | 依赖于定义 |
NativeLong | long | 平台依赖(32或64位整数) |
PointerType | pointer | 和Pointer相同 |
int (usually) | enum | 枚举类型 |
3.使用
3.1 Maven依赖
<dependency><groupId>net.java.dev.jna</groupId><artifactId>jna</artifactId><version>5.9.0</version></dependency>
3.2 加载动态库
1.创建动态库对应的接口类
public interface RobotApi extends Library {/**@brief 创建机器人连接实例,返回机器人连接实例指针*@param index(in):所创建的机器人连接索引,从0递增,最大为max_connection_count - 1*@return CRobotAPI* p:空类型的机器人连接实例指针*/CRobotAPI.ByReference CreateCRobotAPI(int index);/**@brief 初始化并连接机器人@param pRobot(in):指向被操作的机器人连接实例的指针*@param robot_addr(in):机器人IP地址*@return 0:连接成功,非0:错误码*/int ConnectRobot(CRobotAPI.ByReference pRobot,Memory robot_addr);/*@param pRobot(in):指向被操作的机器人连接实例的指针*@brief 断开机器人连接*/int DisconnectRobot(CRobotAPI.ByReference pRobot);/** 机器人上电*@param pRobot(in):指向被操作的机器人连接实例的指针*@return 0:上电成功,非0:错误码*/int PowerOn(CRobotAPI.ByReference pRobot);/** 机器人上电*@param pRobot(in):指向被操作的机器人连接实例的指针*@return 0:上电成功,非0:错误码*/int PowerOff(CRobotAPI.ByReference pRobot);}
注意
- 接口名、参数、返回值要和动态文件相匹配,对应类型参考步骤二
- 接口类要继承Library或者StdCallLibrary
3.3 接口实例化
因为想要将动态库文件放入自定义文件夹下,所以加载目录设置成自定义目录,下文有介绍
getIndex()方法为获取接口所需参数,无需关注,主要看整体使用流程
@Slf4j
@Service
public class RobotApiService {private static RobotApi instance;@Value("${manage.file.path}")private String path;@PostConstructpublic void init(){String name = path;String os = System.getProperty("os.name").toLowerCase();String arch = System.getProperty("os.arch").toLowerCase();if (os.contains("win")) {// 根据实际的 Windows 动态库名称进行替换name = path + File.separator +"RobotAPI";} else if (os.contains("mac")) {// 根据实际的 macOS 动态库名称进行替换} else if (os.contains("nix") || os.contains("nux") || os.contains("aix")) {// 根据实际的 Linux 动态库名称进行替换name = path + File.separator +"libRobotAPI.so";} else {throw new UnsupportedOperationException("不支持的操作系统: " + os + " " + arch);}instance = Native.load(name, RobotApi.class);}public CRobotAPI.ByReference createCRobotAPI(int index){return instance.CreateCRobotAPI(index);}public int powerOn(int id){return instance.PowerOn(getIndex(id));}public int powerOff(int id){return instance.PowerOff(getIndex(id));}public int connectRobot(String robotAddr,int index){CRobotAPI.ByReference reference = createCRobotAPI(index);StaticLog.info("指针地址{}",reference);return instance.ConnectRobot(reference,mem);}/*** 断开机器人连接*/public int disconnectRobot(int index){CRobotAPI.ByReference pRobot = getIndex(index);return instance.DisconnectRobot(pRobot);}private static void freeMemory(Memory mem) {long peer = Pointer.nativeValue(mem);//手动释放内存Native.free(peer);//避免Memory对象被GC时重复执行Nativ.free()方法Pointer.nativeValue(mem, 0);}
}
配置文件
#服务配置
manage:file:path: D:\workspace\jni-jna-web-master\dll
3.4 控制类
@RestController
@RequestMapping("/robot")
public class RobotController {@Resourceprivate RobotApiService apiService;/*** 连接机械臂** @return 控制指针*/@GetMapping("/connectRobot")public int connectRobot(int index) {String addr = "127.0.0.1";return apiService.connectRobot(addr,index);}/*** 断开机器人连接*/@GetMapping("/disconnectRobot")public int disconnectRobot(int index) {return apiService.disconnectRobot(index);}
}
3.5验证
启动项目
访问接口
成功调用动态库接口
4.注意事项
- 动态库版本选择要和当前操作系统相匹配,windows和linux不能共用,还要注意操作系统位数,32和64也不能混用。
- JAVA调用C++代码时需要将动态库编译成C语言格式的,否则C++会修改默认的接口名称导致JNA调用失败。
- 在使用指针的情况下要手动释放,java是值传递的,没有指针(地址)的概念,但是c/c++是有指针的,所以Pointer是JNA中引入的类,用来表示native方法中的指针,不被JVM所管理需要手动释放。
- Windows环境下可以通过Native.load(path,class)方法直接加载dll文件,linux环境下需要将自定义的文件目录加入到系统配置中,否则无法读取so文件。
- 如果动态库返回结构体实例指针则需要创建对应的类去接收,下文有介绍。
5.指南
5.1 Linux下读取SO文件
原理:
JVM在载入动态库时候,会从java.library.path所指定的目录下开始查找,找不到就会报动态库缺少的错误。此外,如果动态库a.so依赖于b.so,则jvm在加载a.so之前,会先加载b.so。也就是说,如果a.so和b.so不在一个目录下,即使在加载a.so时,指定了目录,也会报动态库缺少错误。
增加动态库目录常用方法有两种
-
修改ld.so.conf文件
用文本编辑器打开/etc/ld.so.conf或/etc/ld.so.conf.d/下的配置文件(可能需要sudo权限)。sudo vim /etc/ld.so.conf
在文件末尾添加新的动态库目录路径(每个目录一行)。
/your/custom/library/path
保存并关闭文件。
运行ldconfig来更新动态链接器的缓存。
sudo ldconfig
-
使用LD_LIBRARY_PATH环境变量
你可以临时地通过设置LD_LIBRARY_PATH环境变量来添加动态库目录。export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/your/custom/library/path
#查看当前环境变量
echo $LD_LIBRARY_PATH
查看动态库依赖
使用ldd命令可以查看应用程序或动态库的依赖。
可以查看当前动态库是否缺少依赖,上图缺少GLIBCXX_3.4.21,需要安装,否则无法启动
5.2 接收动态库返回结构体指针并在其他接口中使用
1.JNA模拟结构体
C语言中的定义
class CRobotAPI{
private:unsigned int m_robot_index;char* m_server_addr;bool m_connected;unsigned int m_wobj_num; //自定义工件坐标系个数,可取值32-200,默认200
};
在java中的模拟
@Data
public class CRobotAPI extends Structure {public int m_robot_index;public String m_server_addr;public boolean m_connected;//自定义工件坐标系个数,可取值32-200,默认200public int m_wobj_num;// 定义值传递和指针传递类public static class ByReference extends CRobotAPI implements Structure.ByReference {//指针和引用的传递使用ByReference}public static class ByValue extends CRobotAPI implements Structure.ByValue {//拷贝参数传递使用ByValue}/*** 重写getFieldOrder获取字段列表, 很重要,没有会报错*/@Overrideprotected List<String> getFieldOrder() {return Arrays.asList("m_robot_index", "m_server_addr", "m_connected", "m_wobj_num");}
}
代码说明与使用总结
-
C、C++中的结构体成员默认是public的,无需使用public关键字修饰,而在Java的实现中必须使用public关键字修饰,否则会报成员不存在的错误。
-
在Java中模拟C、C++的结构体时数据类型的映射必须一一对应,成员属性的名字可以不同(但一般为了方便追踪问题,建议定义成和.h头文件中一致)。
-
在Java中定义的成员属性的顺序必须和C、C++中头文件结构体中定义的顺序一致,不能调整顺序,否则会引起异常。
-
必须使用@Structure.FieldOrder或者重载FieldOrder方法,并返回一个字符串数组,数组中的成员是结构体的成员属性列表。
-
关于内存对齐,如果结构体中使用了内存对齐,那么在java中也必须声明一个无参构
造函数,并调用父类方法指定内存对齐模式,否则有可能引发内存访问异常。 -
如果需要用到结构体指针,则需要在结构体类的实现中实现静态内部类ByReference,如代码中所示,CRobotAPI .ByReference 就等同于 CRobotAPI *
-
如果需要用到结构体对象数组,则需要在结构体类的实现中实现静态内部类ByValue,如上述代码所示, CRobotAPI .ByValue 就等同于 CRobotAPI []
动态库返回结构体实例指针
需要上一步返回的结构体实例指针作为参数传递回去
实际过程中可能需要多次使用该指针,可以保存到当前线程内共多次使用。
5.3 调用C++编译的动态库
如果用c++实现本地方法,需要用extern ”C“来声明,这样是为了不让使用c++编译器来编译本地方法,因为c++编译器编译可能会给方法加上后缀,导致Java无法找到本地方法的实现。
extern "C" {void externC(int a ,int b){}
}