Java 调用本地动态库的组件(javah, JNA, JNR-FFI)

还是很 久很久以前,当初有 Java 调用本地动态库需求的时候,尝试过用 javah/native 原生的方式在 Java 中使用动态库,再就是小试了 JNative,它调用动态库只需 Java 端的动作, 它最后的更新日期是 9 年前 2013-04-26,基本是应该选择放弃了。

关于 JNative 的使用写过两篇

  1. Java调用动态库最简便方法和最好用的组件
  2. 使用JNative,在Java中传递一个C/C++结构参数到动态库中

如今想继续发掘下是否有别的更好的调用本地库的 JNI 组件,找到有

  1. JNIWrapper:居然是一个收费的,而且价格不菲,不作绍
  2. BridJ:也是 7 年前才有过代码的更新
  3. JNA(Java Native Access): 也就它稍为活跃一点点
  4. JNR-FFI:最近几个月也有更新,不知道使用体验如何

对比而言,JNA 和 JNR-FFI 值得一试。在先体验它们之前回忆一下 javah 如何调用本地动态库,以最简单快速的方式感触一翻,在进入 JNA 和 JNR-FFI 有所对比

原生方式使用 JNI

以 Mac OS X 下用 gcc 编译器为例,JDK 为 11

HelloJNI.java

编译并生成 JNI C/C++ 头文件

$ javac -d ./ -h ./ HelloJNI.java

会生成 example/HelloJNI.class 和 example_HelloJNI.h 文件

注:javah 在 Java 9 中不推荐使用,并从 Java 10 中移除了,如果分 javac 和 javah 两步操作,总是看到以下错误的话

Error: Could not find class file for 'example.HelloJNI'.

大概是编译 example.HelloJNI 用的 javac 和 javah 来自不同的 JDK,比如 javac 是 JDK 11 的,javah 是 JDK 8 中的。

example_HelloJNI.h 中声明的方法是

实现本地方法并生成动态库

创建 example_HelloJNI.c, 内容为

生成动态库

$ gcc -fPIC -I $JAVA_HOME/include -I $JAVA_HOME/include/darwin -shared -o libhello.dylib example_HelloJNI.c

这会在当前目录下生成 hello.so 文件。可用 nm 命令查看其中本地函数

$ nm hello.so|grep say
0000000000003f60 T _Java_example_HelloJNI_sayHello

执行 example.HelloJNI

$ java -Djava.library.path=./ example.HelloJNI
Hello World!

实际在 Mac OS X 下顺利走完上面的流程经历了一些磕磕绊绊。在 Mac OS X 下

System.loadLibrary("hello");

对应的动态库文件是 libhello.dylib, 所以在用 gcc 编译生成的动态库文件名必须是 libhello.dylib,否则无论如何设置 java.library.path 系统属性还是环境变量 LD_LIBRARY_PATH 都会摆脱不掉 java.lang.UnsatisfiedLinkError 的错误。

更直接的加载动态库文件的方法是指定动态库文件名调用 System.load() 方法 

System.load("/Users/yanbin/workspace/test/hello.so");

都不用管 java.library.path 和 LD_LIBRARY_PATH 的值。

注意:用 System.loadLibrary("hello"); 在 Linux 和 Windows 下会分别查找 hello.sohello.dll 文件。

欲知晓 System.loadLibrary("hello") 实际会加载哪个文件,可用 System.mapLibraryName("hello"); 查看, 比如它在 Mac OS X 下输出为 libhello.dylib

小结:原生的使用 JNI 方式需要 Java 和 C/C++ 两方互动,即使是已经有现成的动态库,也需要从 Java 的 native 方法出发,javah(或 javac -h) 生成头文件,编写实现来使用现成的动态库,或单独为 Java 应用构建单独的 JNI 用的动态库文件。

使用 JNA 调用本地动态库

有了前面原生的使用 JNI 方式作铺垫后,我们过度到更为便捷的使用动态库的方式,不需要写任何 C/C++ 代码,现成的动态库文件拿来即可用。JNative 也是允许我们直接使用别人生成好的动态库,这里来了解 JNA 和接下来的 JNR-FFI。

重新制做一个动态库文件作为演示用,如果有现成的动态库,知道其中导出的方法可以跳过这一步,直接使用第三方的动态库

演示用的 C 代码 hello.c

编译生成  libhello.dylib 文件

$ gcc -fPIC -shared -o libhello.dylib hello.c

会在当前目录下产生 libhello.dylib 文件。

注:在 Linux 和 Windows 文件名应为 hello.dll 和 libhello.so。

真正需要自己编写的 Java 代码 JNADemo.java

上面代码用到 com.sun.jna.{Library,Native}, 看来 JNA 还是出生名门 sun.com,不过已是没落贵族。引入该 JNA 库的 Maven 依赖是

jna:5.11.0 是 2022-3-27 发布到 Maven 中央仓库当前最新版,比从 github 上看到的更活跃。用 maven dependency:tree 显示来看它没有带其他依赖,也就是说使用 JNA 只需用到一个 jna-5.11.0.jar 文件, 大小 1.8M, 很干净。它囊括了多数平台下使用动态库的代码,实现原理是由它为平台提供的中间动态库去加载调用实际的用户动态库。

不使用 Maven 的话只需把下载 jna-5.11.0.jar 并放到项目的 lib 目录中(也可以其他目录名)。

然后编译

$ javac -cp lib/jna-5.11.0.jar -d ./ JNADemo.java

在当前目录下会产生 example/JNADemo.class 文件,我们再把前面生成的 libhello.dylib 也拷入到 lib 目录中

执行

$ java -cp .:./lib:lib/jna-5.11.0.jar example.JNADemo
hello world

说明:从代码 NativeLibrary 可知 JNA Native.load("hello", ...) 加载动态库的方式有

    1. 依据系统属性 jna.library.path, 或 jna.platform.library.path, 或 java.library.path 配置的路径中查找
    2. 或环境变量 LD_LIBRARY_PATH 查找动态库
    3. 动态库文件路径,如写成 CLibrary clib = Native.load("/Users/yanbin/JNADemo/lib/libhello.dylib", CLibrary.class);
    4. 从 classpath 下去找 libhello.dylib 文件的,所以这里用了 -cp 中的 lib 指示 JNA 去定位到 lib 目录中的 libhello.dylib 文件
    5. 以及 Mac OSX 平台下如从 ~/Library/Frameworks /System/Library/Frameworks 等处查找

配置系统属性 jna.debug_load=true 可打印出查找动态库的步骤。

另外, 加载动态库的操作是同步的,并且会缓存已加载的动态库,因此再执行一次 CLibrary.clib.sayHello("world") 无需加载动态库了

JNA 大大简化了 JNI 应用,其他的主要内容就是要清楚在 C/Java 之间的类型映射了,参见 JNA Default Type Mappings。大致列出就是

  1. char               <->  byte
  2. short             <->  short
  3. wchar_t        <->  char
  4. int(integer)  <->  int
  5. int(boolean) <->  boolean
  6. long                <->  NativeLong
  7. long long       <->  long
  8. flot                  <->  float
  9. double            <->  double
  10. char*               <->  String
  11. void*               <->  Pointer

再有就是更复杂的类型映射,如结构,联合体,类类型等,这里不细说,使用到的话再研究。

还可用 JNA 来调用 C 的标准库,如 printf, scanf 等函数

使用 JNR-FFI 调用本地动态库

使用 JNR-FFI 的 Maven 依赖配置

mvn dependency:tree 显示的就有许多传递依赖了

看得出来它在字节码生成优化上进行发力,所以性能上会比 JNA 要好。参见 java-native-benchmark, 基本上就是直接用 JNI 比较快,JNR 差得不是很远,但 JNA 总是最慢,它们在相对时间上的几个参考值,越小越好

JNI         JNR       JNA 
2.239      3.560    173.064
0.255      3.558     8.909

使用方式与 JNA 类似,官方文档 JNR-FFI User Documentation

简单应用,利用前面 JNA 的 libhello.dylib 动态库文件,Java 代码 (JNRDemo.java) 如下

编译,用 mvn compile 编译,由 Maven 管理依赖,或把上面 mvn dependency:tree 显示的所有依赖拷入到 lib 目录中,然后用 java 命令编译

$ javac -d ./ -cp .:lib/jffi-1.3.9.jar:lib/jffi-1.3.9-native.jar:<其他的 jar 包> JNRDemo.java

然后执行用

$ java -cp .:lib/jffi-1.3.9.jar:lib/jffi-1.3.9-native.jar:<其他的 jar 包> -Djava.library.path=lib example.JNRDemo
hello World!

与 JNA 不同的是,JNR 是从 -Djava.library.path 指定的路径上加载动态库,与直接使用 JNI 是一样的。所以设置 LD_LIBRARY_PATH 环境变量指向 libhello.dylib 所在目录也没问题。

另外,同样的 LibraryLoader.create(CLibrary.class).load(...) 还能通过文件路径去加载动态库,如上面的 load() 调用行改成如下也行

从 jnr-ffi 的源代码 LibraryLoader, 可发现它加载动态库的方式有

  1. 可通过 jnr.ffi.library.path, jaffl.library.path, jna.library.path, java.library.path 等系统属性配置搜索目录
  2. 环境变量 LD_LIBRARY_PATH 中搜索
  3. Unix 族系统还会依序从 /usr/local/lib, /usr/lib, /lib 中搜索动态库
  4. 动态库文件路径

但不支持 classpath 下查找动态库,JNR-FFI 也实现了同步加载并缓存动态库。

JNA 与 JNR-FFI 的简单对比

  1. 它们都实现了从系统属性及环境变量 LD_LIBRARY_PATH 配置的路径中查找动态库
  2. 都能以动态库文件路径加载
  3. 加载动态库都是同步的操作,并缓存加载的动态库
  4. JNA 还能从 classpath 中加载动态库,JNR 不能
  5. JNR 能从 /usr/local/lib/usr/lib, /lib 中加载动态库,JNA 不能
  6. JNR 所需的依赖比 JNA 多,JNA 仅有它自身一个 jar 包
  7. JNR 通过字节码相关的优化性能上比 JNA 要高,自己斟酌性能需求
  8. 它们的使用方式基本一样,详细的内容就是如何进行参数,返回值在 C/C++ 与 Java 之间的映射

因为一致的编程方式,所以在这两者中切换也不难。

链接:

  1. Java Native Interface(JNI)从零开始详细教程
  2. Java Native Access: A Cleaner Alternative to JNI?

本文链接 https://yanbin.blog/java-jni-libraries/, 来自 隔叶黄莺 Yanbin Blog

[版权声明] Creative Commons License 本文采用 署名-非商业性使用-相同方式共享 4.0 国际 (CC BY-NC-SA 4.0) 进行许可。

Subscribe
Notify of
guest

0 Comments
Inline Feedbacks
View all comments