java 高级用法之: 调用本地方法的利器 JNA
简介
JAVA 是可以调用本地方法的,官方提供的调用方式叫做 JNI,全称叫做 java native interface。要想使用 JNI,我们需要在 JAVA 代码中定义 native 方法,然后通过 javah 命令创建 C 语言的头文件,接着使用 C 或者 C++语言来实现这个头文件中的方法,编译源代码,最后将编译后的文件引入到 JAVA 的 classpath 中,运行即可。
虽然 JAVA 官方提供了调用原生方法的方式,但是好像这种方法有点繁琐,使用起来没有那么的方便。
那么有没有更加简洁的调用本地方法的形式吗?答案是肯定的,这就是今天要讲的 JNA。
JNA 初探
JNA 的全称是 Java Native Access,它为我们提供了一种更加简单的方式来访问本地的共享库资源,如果你使用 JNA,那么你只需要编写相应的 java 代码即可,不需要编写 JNI 或者本地代码,非常的方便。
本质上 JNA 使用的是一个小的 JNI library stub,从而能够动态调用本地方法。
JNA 就是一个 jar 包,目前最新的版本是 5.10.0,我们可以像下面这样引用它:
JNA 是一个 jar 包,它里面除了包含有基本的 JAVA class 文件之外,还有很多和平台相关的文件,这些平台相关的文件夹下面都是 libjnidispatch*的库文件。
<img src="https://img-blog.csdnimg.cn/884d316db24a444fb9e8ea34d608e5a8.png" style="zoom:50%"/>
可以看到不同的平台对应着不同的动态库。
JNA 的本质就是将大多数 native 的方法封装到 jar 包中的动态库中,并且提供了一系列的机制来自动加载这个动态库。
接下来我们看一个具体使用 JNA 的例子:
这个例子中,我们想要加载系统的 c lib,从而使用 c lib 中的 printf 方法。
具体做法就是创建一个 CLibrary interface,这个 interface 继承自 Library,然后使用 Native.load 方法来加载 c lib,最后在这个 interface 中定义要使用的 lib 中的方法即可。
那么 JNA 到底是怎么加载 native lib 的呢?我们一起来看看。
JNA 加载 native lib 的流程
在讲解 JNA 加载 native lib 之前,我们先回顾一下 JNI 是怎么加载 native lib 的呢?
在 JNI 中,我们首先在 java 代码中定义要调用的 native 方法,然后使用 javah 命令,创建 C 的头文件,然后再使用 C 或者 C++来对这个头文件进行实现。
接下来最重要的一步就是将生成的动态链接库添加到 JAVA 的 classpath 中,从而在 JAVA 调用 native 方法的时候,能够加载到对应的库文件。
对于上面的 JNA 的例子来说,直接运行可以得到下面的结果:
我们可以向程序添加 JVM 参数:-Djna.debug_load=true,从而让程序能够输出一些调试信息,再次运行结果如下所示:
仔细观察上面的输出结果,我们可以大概了解 JNA 的工作流程。JNA 的工作流程可以分为两部分,第一部分是 Library Loading,第二部分是 Native Library Loading。
两个部分分别对应的类是 com.sun.jna.Native 和 com.sun.jna.NativeLibrary。
第一部分的 Library Loading 意思是将 jnidispatch 这个共享的 lib 文件加载到 System 中,加载的顺序是这样的:
jna.boot.library.path.
使用 System.loadLibrary(java.lang.String)从系统的 library path 中查找。如果不想从系统 libary path 中查找,则可以设置 jna.nosys=true。
如果从上述路径中没有找到,则会调用 loadNativeDispatchLibrary 将 jna.jar 中的 jnidispatch 解压到本地,然后进行加载。如果不想从 classpath 中查找,则可以设置 jna.noclasspath=true。 如果不想从 jna.jar 文件中解压,则可以设置 jna.nounpack=true。
如果你的系统对于从 jar 文件中解压文件有安全方面的限制,比如 SELinux,那么你需要手动将 jnidispatch 安装在一个可以访问的地址,然后使用 1 或者 2 的方式来设置加载方式和路径。
当 jnidispatch 被加载之后,会设置系统变量 jna.loaded=true,表示 jna 的 lib 已经加载完毕。
默认情况下我们加载的 lib 文件名字叫 jnidispatch,你也可以通过设置 jna.boot.library.name 来对他进行修改。
我们看一下 loadNativeDispatchLibrary 的核心代码:
首先是查找 stub lib 文件:/com/sun/jna/darwin-aarch64/libjnidispatch.jnilib, 默认情况下这个 lib 文件是在 jna.jar 包中的,所以需要调用 extractFromResourcePath 方法将 jar 包中的 lib 文件拷贝到临时文件中,然后调用 System.load 方法将其加载。
第二部分就是调用 com.sun.jna.NativeLibrary 中的 loadLibrary 方法来加载 JAVA 代码中要加载的 lib。
在 loadLibrary 的时候有一些搜索路径的规则如下:
jna.library.path,用户自定义的 jna lib 的路径,优先从用户自定义的路径中开始查找。
jna.platform.library.path, 和 platform 相关的 lib 路径。
如果是在 OSX 操作系统上,则会去搜索 ~/Library/Frameworks, /Library/Frameworks, 和 /System/Library/Frameworks ,去查询对应的 Frameworks。
最后会去查找 Context class loader classpath(classpath 或者 resource path),具体的格式是 ${os-prefix}/LIBRARY_FILENAME。如果内容是在 jar 包中,则会将文件解压缩至 jna.tmpdir,然后进行加载。
所有的搜索逻辑都放在 NativeLibrary 的方法 loadLibrary 中实现的,方法体太长了,这里就不一一列举了,感兴趣的朋友可以自行去探索。
本地方法中的结构体参数
如果本地方法传入的参数是基本类型的话,在 JNA 中定义该 native 方法就用基本类型即可。
但是有时候,本地方法本身的参数是一个结构体类型,这种情况下我们该如何进行处理呢?
以 Windows 中的 kernel32 library 为例,这个 lib 中有一个 GetSystemTime 方法,传入的是一个 time 结构体。
我们通过继承 Structure 来定义参数的结构体:
然后定义一个 Kernel32 的 interface:
最后这样调用:
总结
以上就是 JNA 的基本使用,有关 JNA 根据深入的使用,敬请期待后续的文章。
本文的代码:https://github.com/ddean2009/learn-java-base-9-to-20.git
本文已收录于 http://www.flydean.com/02-jna-overview/
最通俗的解读,最深刻的干货,最简洁的教程,众多你不知道的小技巧等你来发现!
欢迎关注我的公众号:「程序那些事」,懂技术,更懂你!
版权声明: 本文为 InfoQ 作者【程序那些事】的原创文章。
原文链接:【http://xie.infoq.cn/article/34bf8e58722bcf142392eac52】。文章转载请联系作者。
评论