写点什么

java 高级用法之: 调用本地方法的利器 JNA

作者:程序那些事
  • 2022 年 3 月 09 日
  • 本文字数:3857 字

    阅读完需:约 13 分钟

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,我们可以像下面这样引用它:


<dependency>            <groupId>net.java.dev.jna</groupId>            <artifactId>jna</artifactId>            <version>5.10.0</version>        </dependency>
复制代码


JNA 是一个 jar 包,它里面除了包含有基本的 JAVA class 文件之外,还有很多和平台相关的文件,这些平台相关的文件夹下面都是 libjnidispatch*的库文件。


<img src="https://img-blog.csdnimg.cn/884d316db24a444fb9e8ea34d608e5a8.png" style="zoom:50%"/>


可以看到不同的平台对应着不同的动态库。


JNA 的本质就是将大多数 native 的方法封装到 jar 包中的动态库中,并且提供了一系列的机制来自动加载这个动态库。


接下来我们看一个具体使用 JNA 的例子:


public class JNAUsage {
public interface CLibrary extends Library { CLibrary INSTANCE = (CLibrary) Native.load((Platform.isWindows() ? "msvcrt" : "c"), CLibrary.class);
void printf(String format, Object... args); }
public static void main(String[] args) { CLibrary.INSTANCE.printf("Hello, World\n"); for (int i=0;i < args.length;i++) { CLibrary.INSTANCE.printf("Argument %d: %s\n", i, args[i]); } }}
复制代码


这个例子中,我们想要加载系统的 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 的例子来说,直接运行可以得到下面的结果:


Hello, World
复制代码


我们可以向程序添加 JVM 参数:-Djna.debug_load=true,从而让程序能够输出一些调试信息,再次运行结果如下所示:


12月 24, 2021 9:16:05 下午 com.sun.jna.Native extractFromResourcePath信息: Looking in classpath from jdk.internal.loader.ClassLoaders$AppClassLoader@251a69d7 for /com/sun/jna/darwin-aarch64/libjnidispatch.jnilib12月 24, 2021 9:16:05 下午 com.sun.jna.Native extractFromResourcePath信息: Found library resource at jar:file:/Users/flydean/.m2/repository/net/java/dev/jna/jna/5.10.0/jna-5.10.0.jar!/com/sun/jna/darwin-aarch64/libjnidispatch.jnilib12月 24, 2021 9:16:05 下午 com.sun.jna.Native extractFromResourcePath信息: Extracting library to /Users/flydean/Library/Caches/JNA/temp/jna17752159487359796115.tmp12月 24, 2021 9:16:05 下午 com.sun.jna.NativeLibrary loadLibrary信息: Looking for library 'c'12月 24, 2021 9:16:05 下午 com.sun.jna.NativeLibrary loadLibrary信息: Adding paths from jna.library.path: null12月 24, 2021 9:16:05 下午 com.sun.jna.NativeLibrary loadLibrary信息: Trying libc.dylib12月 24, 2021 9:16:05 下午 com.sun.jna.NativeLibrary loadLibrary信息: Found library 'c' at libc.dylibHello, World
复制代码


仔细观察上面的输出结果,我们可以大概了解 JNA 的工作流程。JNA 的工作流程可以分为两部分,第一部分是 Library Loading,第二部分是 Native Library Loading。


两个部分分别对应的类是 com.sun.jna.Native 和 com.sun.jna.NativeLibrary。


第一部分的 Library Loading 意思是将 jnidispatch 这个共享的 lib 文件加载到 System 中,加载的顺序是这样的:


  1. jna.boot.library.path.

  2. 使用 System.loadLibrary(java.lang.String)从系统的 library path 中查找。如果不想从系统 libary path 中查找,则可以设置 jna.nosys=true。

  3. 如果从上述路径中没有找到,则会调用 loadNativeDispatchLibrary 将 jna.jar 中的 jnidispatch 解压到本地,然后进行加载。如果不想从 classpath 中查找,则可以设置 jna.noclasspath=true。 如果不想从 jna.jar 文件中解压,则可以设置 jna.nounpack=true。

  4. 如果你的系统对于从 jar 文件中解压文件有安全方面的限制,比如 SELinux,那么你需要手动将 jnidispatch 安装在一个可以访问的地址,然后使用 1 或者 2 的方式来设置加载方式和路径。


当 jnidispatch 被加载之后,会设置系统变量 jna.loaded=true,表示 jna 的 lib 已经加载完毕。


默认情况下我们加载的 lib 文件名字叫 jnidispatch,你也可以通过设置 jna.boot.library.name 来对他进行修改。


我们看一下 loadNativeDispatchLibrary 的核心代码:


String libName = "/com/sun/jna/" + Platform.RESOURCE_PREFIX + "/" + mappedName;            File lib = extractFromResourcePath(libName, Native.class.getClassLoader());            if (lib == null) {                if (lib == null) {                    throw new UnsatisfiedLinkError("Could not find JNA native support");                }            }
LOG.log(DEBUG_JNA_LOAD_LEVEL, "Trying {0}", lib.getAbsolutePath()); System.setProperty("jnidispatch.path", lib.getAbsolutePath()); System.load(lib.getAbsolutePath()); jnidispatchPath = lib.getAbsolutePath();
复制代码


首先是查找 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 的时候有一些搜索路径的规则如下:


  1. jna.library.path,用户自定义的 jna lib 的路径,优先从用户自定义的路径中开始查找。

  2. jna.platform.library.path, 和 platform 相关的 lib 路径。

  3. 如果是在 OSX 操作系统上,则会去搜索 ~/Library/Frameworks, /Library/Frameworks, 和 /System/Library/Frameworks ,去查询对应的 Frameworks。

  4. 最后会去查找 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 来定义参数的结构体:


@FieldOrder({ "wYear", "wMonth", "wDayOfWeek", "wDay", "wHour", "wMinute", "wSecond", "wMilliseconds" })public static class SYSTEMTIME extends Structure {    public short wYear;    public short wMonth;    public short wDayOfWeek;    public short wDay;    public short wHour;    public short wMinute;    public short wSecond;    public short wMilliseconds;}
复制代码


然后定义一个 Kernel32 的 interface:


public interface Kernel32 extends StdCallLibrary { Kernel32 INSTANCE = (Kernel32)    Native.load("kernel32", Kernel32.class);Kernel32 SYNC_INSTANCE = (Kernel32)    Native.synchronizedLibrary(INSTANCE);
void GetSystemTime(SYSTEMTIME result);}
复制代码


最后这样调用:


Kernel32 lib = Kernel32.INSTANCE;SYSTEMTIME time = new SYSTEMTIME();lib.GetSystemTime(time);
System.out.println("Today's integer value is " + time.wDay);
复制代码

总结

以上就是 JNA 的基本使用,有关 JNA 根据深入的使用,敬请期待后续的文章。


本文的代码:https://github.com/ddean2009/learn-java-base-9-to-20.git


本文已收录于 http://www.flydean.com/02-jna-overview/

最通俗的解读,最深刻的干货,最简洁的教程,众多你不知道的小技巧等你来发现!

欢迎关注我的公众号:「程序那些事」,懂技术,更懂你!

发布于: 刚刚阅读数: 2
用户头像

关注公众号:程序那些事,更多精彩等着你! 2020.06.07 加入

最通俗的解读,最深刻的干货,最简洁的教程,众多你不知道的小技巧,尽在公众号:程序那些事!

评论

发布
暂无评论
java高级用法之:调用本地方法的利器JNA_Java_程序那些事_InfoQ写作平台