JAR使用JNA并打包dll

搞Java的同学应该都清楚,Java与C++交互方式是通过DLL,使用JNI技术,也就是现在Android上最常用的方式,但其实现在有更高层次的封装了,那就是JNA方式。虽然JNA使用很简单,但是发布却比较麻烦,特别是参考网上的文章基本很少有正确的方法,本文将详述正确的方法。

JNA的使用

JNA(Java Native Access)框架是一个开源的Java框架,是SUN公司主导开发的,建立在经典的JNI的基础之上的一个框架。JNA框架解决了JNI需要额外编写动态库代码与Java交互的工作,可以直接使用其他语言提供的动态库,开发人员只要在一个Java接口中描述目标native library的函数与结构,JNA将自动实现Java接口到native function的映射,大大降低了Java调用本体共享库的开发难度。

使用JNA在引入JNA的JAR之后,只需要定义接口,类似如下

public interface Clibrary extends Library {
//加载libfoo.so或foo.dll链接库
Clibrary INSTANTCE = (Clibrary)Native.load("foo", Clibrary.class);

int foo();//此方法为链接库中的方法
}

上面是对接口的定义,其中加载的那行的库名称为 foo,在Linux平台上为 libfoo.so 在windows平台为 foo.dll,熟悉两个平台的开发的都应该比较清楚 。然后后面是具体的动态库导出函数的接口定义,注意一些数据类型的转换,比如C中的 const char[] 应该使用Java的 byte[] 类型,不能使用Java的 char[] 类型。

调用方的代码也很简单,类似

public void test() {
int ret = Clibrary.INSTANTCE.foo();
}

JNA的封装

一般我们希望在动态库的基础上封装Java接口,毕竟直接调用动态库接口不是太方便,包括一些错误处理什么的等等,都可以转换成Java的方式。然后最终将封装库额外加动态库一起封装成一个JAR对外提供。

这里面涉及到两方面的问题,第一是打包动态库到JAR中,这个倒不是很难,网络上有大量的文章可参考,此处就不再赘述了。

另外就是如何才能让JNA加载到动态库,我们知道JNA加载的方式有两种,第一种就是类似上面的直接给传递一个库名称,JNA自动在 classpath 查找,如果将动态库拷贝到运行环境的 classpath 上就能加载。另一种就是加载绝对路径的动态库。

但是由于打包之后动态库实际上存在JAR包之中,并不存在于本地的文件系统上,因此只需要想办法将动态库释放出来,然后使用第二种方式加载即可。释放其实可以通过 getResourceAsStream 读取到文件,而 Clibrary.class.getResourceAsStream 就是从当前JAR读取文件,路径 / 表示JAR包根目录,参照真实路径进行修改,具体可以通过压缩软件把JAR打开查看目录结构。一般可以将文件释放到 temp 路径去,然后返回释放后的文件的绝对路径给JNA加载,我采用了类似下面的代码实现功能

private synchronized static String extractLibrary(String libName) {
String systemType = System.getProperty("os.name");
String libPrefix = (systemType.toLowerCase().indexOf("win")!=-1) ? "" : "lib";
String libExtension = (systemType.toLowerCase().indexOf("win")!=-1) ? ".dll" : ".so";
String libFullName = libPrefix + libName + libExtension;
String nativeTempDir = System.getProperty("java.io.tmpdir");

File extractedLibFile = new File(nativeTempDir + File.separator + libFullName);
InputStream in = Clibrary.class.getResourceAsStream("/lib/" + libFullName);
if(!extractedLibFile.exists()){
try(
BufferedInputStream reader = new BufferedInputStream(in);
FileOutputStream writer = new FileOutputStream(extractedLibFile);
) {
byte[] buffer = new byte[1024];
while(true){
int len = reader.read(buffer);
if(len == 0 || len == -1)
break;

writer.write(buffer, 0, len);
}
} catch (IOException e){
e.printStackTrace();
}
}

return extractedLibFile.getAbsolutePath();
}

有上面这样的代码逻辑后,JNA加载动态库的代码相应变为

Clibrary INSTANTCE = (Clibrary)Native.load(extractLibrary("foo"), Clibrary.class);

其他一些问题

上面实现了基本逻辑后,基本满足了对外提供功能的需要。但是Java是跨平台的语言,上面的做法只能满足一个平台,这其中的主要原因是我们提供的动态库,如果我们提供了Win32平台的dll,这个JAR就能在Win32系统上的Java上运行,类似的Java运行时对32位和64位还有要求,因此如果要满足大部分的需求,比较好的办法是将支持的平台的所有动态库打包到一起,在上面的释放代码处进行判断具体的平台来释放需要的文件,而且文件最终要路径唯一,就可以适应各种平台。

最近的文章

AS无法连接真机的解决方案

调试Android程序的时候,如果需要摄像头了,虚拟机就用不上了,需要连接真机。虽然连接真机很容易,但是后续因为Win10系统原因造成又无法连接成功,因此针对连接问题总结其解决方案。 基本方法一般来说,当我们将手机插入电脑后,如果之前没安装过驱动,则基本是会出现设备管理器有不可识别的设备。这种情况下 …

技术 继续阅读
更早的文章

AndroidStudio编译JNI

以前使用其他IDE做JNI的时候还比较麻烦,特别是JNI和Java的操作是分开的,现在使用Android Studio做特别方便,还可以直接打包成 aar 格式对外提供。因为项目是对外提供开发包方式的,并不是自身App模式,因此打算直接创建一个简单的Android工程作为调用测试,再单独添加模块作为 …

技术 继续阅读