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

This blog introduces JNA, a Java Native Access tool that allows for easier interaction with native libraries compared to the complex JNI method. It explains JNA's functioning, its library loading process, and demonstrates how to use JNA with a C library example. The post also delves into handling struct parameters in native methods." 133117390,5694251,使用Python和Flask实现Web实时日志更新,"['Python', 'Flask', '前端开发', 'Web框架', '实时更新']

 

简介

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.jnilib
12月 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.jnilib
12月 24, 2021 9:16:05 下午 com.sun.jna.Native extractFromResourcePath
信息: Extracting library to /Users/flydean/Library/Caches/JNA/temp/jna17752159487359796115.tmp
12月 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: null
12月 24, 2021 9:16:05 下午 com.sun.jna.NativeLibrary loadLibrary
信息: Trying libc.dylib
12月 24, 2021 9:16:05 下午 com.sun.jna.NativeLibrary loadLibrary
信息: Found library 'c' at libc.dylib
Hello, 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 根据深入的使用,敬请期待后续的文章。最后,有兴趣想要学习相关资料的朋友点赞三连+关注后私信我回复《555》即可免费领取!已下内容已打包好

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值