写点什么

java 高级用法之: 在 JNA 中将本地方法映射到 JAVA 代码中

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

    阅读完需:约 11 分钟

java高级用法之:在JNA中将本地方法映射到JAVA代码中

简介

不管是 JNI 还是 JNA,最终调用的都是 native 的方法,但是对于 JAVA 程序来说,一定需要一个调用 native 方法的入口,也就是说我们需要在 JAVA 方法中定义需要调用的 native 方法。


对于 JNI 来说,我们可以使用 native 关键字来定义本地方法。那么在 JNA 中有那些在 JAVA 代码中定义本地方法的方式呢?

Library Mapping

要想调用本地的 native 方法,首选需要做的事情就是加载 native 的 lib 文件。我们把这个过程叫做 Library Mapping,也就是说把 native 的 library 映射到 java 代码中。


JNA 中有两种 Library 映射的方法,分别是 interface 和 direct mapping。


先看下 interface mapping,假如我们要加载 C library, 如果使用 interface mapping 的方式,我们需要创建一个 interface 继承 Library:


public interface CLibrary extends Library {    CLibrary INSTANCE = (CLibrary)Native.load("c", CLibrary.class);}
复制代码


上面代码中 Library 是一个 interface,所有的 interface mapping 都需要继承这个 Library。


然后在 interface 内部,通过使用 Native.load 方法来加载要使用的 c library。


上面的代码中,load 方法传入两个参数,第一个参数是 library 的 name,第二个参数是 interfaceClass.


下面的表格展示了 Library Name 和传入的 name 之间的映射关系:



事实上,load 还可以接受一个 options 的 Map 参数。默认情况下 JAVA interface 中要调用的方法名称就是 native library 中定义的方法名称,但是有些情况下我们可能需要在 JAVA 代码中使用不同的名字,在这种情况下,可以传入第三个参数 map,map 的 key 可以是 OPTION_FUNCTION_MAPPER,而它的 value 则是一个 FunctionMapper ,用来将 JAVA 中的方法名称映射到 native library 中。


传入的每一个 native library 都可以用一个 NativeLibrary 的实例来表示。这个 NativeLibrary 的实例也可以通过调用 NativeLibrary.getInstance(String)来获得。


另外一种加载 native libary 的方式就是 direct mapping,direct mapping 使用的是在 static block 中调用 Native.register 方式来加载本地库,如下所示:


public class CLibrary {    static {        Native.register("c");    }}
复制代码

Function Mapping

当我们加载完 native library 之后,接下来就是定义需要调用的函数了。实际上就是做一个从 JAVA 代码到 native lib 中函数的一个映射,我们将其称为 Function Mapping。


和 Library Mapping 一样,Function Mapping 也有两种方式。分别是 interface mapping 和 direct mapping。


在 interface mapping 中,我们只需要按照 native library 中的方法名称定义一个一样的方法即可,这个方法不用实现,也不需要像 JNI 一样使用 native 来修饰,如下所示:


public interface CLibrary extends Library {    int atol(String s);}
复制代码


注意,上面我们提到了 JAVA 中的方法名称不一定必须和 native library 中的方法名称一致,你可以通过给 Native.load 方法传入一个 FunctionMapper 来实现。


或者,你可以使用 direct mapping 的方式,通过给方法添加一个 native 修饰符:



public class HelloWorld { public static native double cos(double x); public static native double sin(double x); static { Native.register(Platform.C_LIBRARY_NAME); }
public static void main(String[] args) { System.out.println("cos(0)=" + cos(0)); System.out.println("sin(0)=" + sin(0)); }}
复制代码


对于 direct mapping 来说,JAVA 方法可以映射到 native library 中的任何 static 或者对象方法。


虽然 direct mapping 和我们常用的 java JNI 有些类似,但是 direct mapping 存在着一些限制。


大部分情况下,direct mapping 和 interface mapping 具有相同的映射类型,但是不支持 Pointer/Structure/String/WString/NativeMapped 数组作为函数参数值。


在使用 TypeMapper 或者 NativeMapped 的情况下,direct mapping 不支持 NIO Buffers 或者基本类型的数组作为返回值。


如果要使用基础类型的包装类,则必须使用自定义的 TypeMapper.


对象 JAVA 中的方法映射来说,该映射最终会创建一个 Function 对象。

Invocation Mapping

讲完 library mapping 和 function mapping 之后,我们接下来讲解一下 Invocation Mapping。


Invocation Mapping 代表的是 Library 中的 OPTION_INVOCATION_MAPPER,它对应的值是一个 InvocationMapper。


之前我们提到了 FunctionMapper,可以实现 JAVA 中定义的方法名和 native lib 中的方法名不同,但是不能修改方法调用的状态或者过程。


而 InvocationMapper 则更进一步, 允许您任意重新配置函数调用,包括更改方法名称以及重新排序、添加或删除参数。


下面举个例子:


   new InvocationMapper() {       public InvocationHandler getInvocationHandler(NativeLibrary lib, Method m) {           if (m.getName().equals("stat")) {               final Function f = lib.getFunction("_xstat");               return new InvocationHandler() {                   public Object invoke(Object proxy, Method method, Object[] args) {                       Object[] newArgs = new Object[args.length+1];                       System.arraycopy(args, 0, newArgs, 1, args.length);                       newArgs[0] = Integer.valueOf(3); // _xstat version                       return f.invoke(newArgs);                   }               };           }           return null;       }   }
复制代码


看上面的调用例子,感觉有点像是反射调用,我们在 InvocationMapper 中实现了 getInvocationHandler 方法,根据给定的 JAVA 代码中的 method 去查找具体的 native lib,然后获取到 lib 中的 function,最后调用 function 的 invoke 方法实现方法的最终调用。


在这个过程中,我们可以修改方传入的参数,或者做任何我们想做的事情。


还有一种情况是 c 语言中的内联函数或者预处理宏,如下所示:


// Original C code (macro and inline variations)   #define allocblock(x) malloc(x * 1024)   static inline void* allocblock(size_t x) { return malloc(x * 1024); }
复制代码


上面的代码中定义了一个 allocblock(x)宏,它实际上等于 malloc(x * 1024),这种情况就可以使用 InvocationMapper,将 allocblock 使用具体的 malloc 来替换:


   // Invocation mapping   new InvocationMapper() {       public InvocationHandler getInvocationHandler(NativeLibrary lib, Method m) {           if (m.getName().equals("allocblock")) {               final Function f = lib.getFunction("malloc");               return new InvocationHandler() {                   public Object invoke(Object proxy, Method method, Object[] args) {                       args[0] = ((Integer)args[0]).intValue() * 1024;                       return f.invoke(newArgs);                   }               };           }           return null;       }   }
复制代码

防止 VM 崩溃

JAVA 方法和 native 方法映射肯定会出现一些问题,如果映射方法不对或者参数不匹配的话,很有可能出现 memory access errors,并且可能会导致 VM 崩溃。


通过调用 Native.setProtected(true),可以将 VM 崩溃转换成为对应的 JAVA 异常,当然,并不是所有的平台都支持 protection,如果平台不支持 protection,那么 Native.isProtected()会返回 false。


如果要使用 protection,还要同时使用 jsig library,以防止信号和 JVM 的信号冲突。libjsig.so 一般存放在 JRE 的 lib 目录下,{os.arch}/libjsig.so, 可以通过将环境变量设置为 LD_PRELOAD (或者 LD_PRELOAD_64)来使用。

性能考虑

上面我们提到了 JNA 的两种 mapping 方式,分别是 interface mapping 和 direct mapping。相较而言,direct mapping 的效率更高,因为 direct mapping 调用 native 方法更加高效。


但是上面我们也提到了 direct mapping 在使用上有一些限制,所以我们在使用的时候需要进行权衡。


另外,我们需要避免使用基础类型的封装类,因为对于 native 方法来说,只有基础类型的匹配,如果要使用封装类,则必须使用 Type mapping,从而造成性能损失。

总结

JNA 是调用 native 方法的利器,如果数量掌握的话,肯定是如虎添翼。


本文已收录于 http://www.flydean.com/03-jna-library-mapping/

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

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

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

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

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

评论

发布
暂无评论
java高级用法之:在JNA中将本地方法映射到JAVA代码中_Java_程序那些事_InfoQ写作平台