JNA总结


JNA,全称 Java Native Access,是一个开源的Java框架。
github:https://github.com/java-native-access/jna

先说JNI

JNI,全称 Java Native Interface。它允许Java代码和其他语言(尤其C/C++)写的代码进行交互,只要遵守调用约定即可。
但其调用过程是非常繁琐的,如果想使用JNI技术调用一个现有的.dll/.so的库文件:

1、首先需要另外使用C语言写一个.dll/.so共享库(类似于适配器Adapter),使用SUN规定的数据结构替代C语言的数据结构,调用已有的.dll/so中公布的函数。
2、再在Java中载入这个适配器.dll/.so,再编写Java Native函数作为.dll/.so中函数的代理。

经过2个繁琐的步骤才能在Java中调用本地代码。如此之坑,估计没几个程序猿会坚持下来了。

JNA 概述

JNA(Java Native Access)是一个开源的Java框架,是Sun公司推出的一种调用本地方法的技术,是建立在经典的JNI基础之上的一个框架。之所以说它是JNI的替代者,是因为JNA大大简化了调用本地方法的过程,使用很方便,基本上不需要脱离Java环境就可以完成。

JNA为Java程序提供了对本机共享库的轻松访问,而无需编写除Java代码之外的任何内容 – 不需要JNI或本机代码。此功能可与Windows的Platform/Invoke和Python的ctypes相媲美。

JNA允许你使用天然的Java方法调用来直接调用本机函数。Java调用看起来就像本机代码中的调用一样。大多数调用不需要特殊处理或配置,不需要样板或生成的代码。

JNA 技术原理

JNA使用一个小型的JNI库插桩程序来动态调用本地代码。开发者使用Java接口描述目标本地库的功能和结构,这使得它很容易利用本机平台的功能,而不会产生多平台配置和生成JNI代码的高开销。

虽然对性能给予了极大的关注,但正确性和易用性优先考虑。

此外,JNA包括一个已与许多本地函数映射的平台库,以及一组简化本地访问的公用接口。

Hello JNA

官网的github中已经标注了版本,注意 artifact 已经变了:原来是 com.sun.jna,现在是 net.java.dev.jna。maven的配置如下:

<dependency>
    <groupId>net.java.dev.jna</groupId>
    <artifactId>jna</artifactId>
    <version>5.2.0</version>
</dependency>
package net.singlex.jna;

import com.sun.jna.Library;
import com.sun.jna.Native;

public class HelloJNA {

    public static void main(String[] args) {
        CLibrary.instance.printf("Hello JNA\n");

        args = new String[] {"a", "b"};
        for (int i = 0; i < args.length; i++) {
            CLibrary.instance.printf("Argument %d: %s\n", i, args[i]);
        }
    }

    public interface CLibrary extends Library {
        CLibrary instance = Native.load("c", CLibrary.class);

        void printf(String format, Object... args);
    }
}


输出结果:
Hello JNA
Argument 0: a
Argument 1: b

Process finished with exit code 0

程序解释:

1、需要定义一个接口,继承自Library 或StdCallLibrary

默认的是继承Library ,如果动态链接库里的函数是以stdcall方式输出的,那么就继承StdCallLibrary,比如众所周知的kernel32库。比如上例中的接口定义:

2、接口内部定义

接口内部需要一个公共静态常量:instance。通过这个常量,就可以获得这个接口的实例,从而使用接口的方法,也就是调用外部dll/so的函数。

该常量通过Native.load()这个API函数获得(Native.loadLibrary()已经不推荐使用了),该函数有2个参数:

第1个参数是动态链接库dll/so的名称,但不带.dll或.so这样的后缀,这符合JNI的规范,因为带了后缀名就不可以跨操作系统平台了。搜索动态链接库路径的顺序是:先从当前类的当前文件夹找,如果没有找到,再在工程当前文件夹下面找win32/win64文件夹,找到后搜索对应的dll文件,如果找不到再到WINDOWS下面去搜索,再找不到就会抛异常了。比如上例中printf函数在Windows平台下所在的dll库名称是msvcrt,而在 其它平台如Linux下的so库名称是c。

第2个参数是本接口的Class类型。JNA通过这个Class类型,根据指定的.dll/.so文件,动态创建接口的实例。该实例由JNA通过反射自动生成。
接口中只需要定义你要用到的函数或者公共变量,不需要的可以不定义,如上例只定义printf函数:

void printf(String format, Object... args);

注意参数和返回值的类型,应该和链接库中的函数类型保持一致。

3、调用链接库中的函数

定义好接口后,就可以使用接口中的函数即相应dll/so中的函数了,前面说过调用方法就是通过接口中的实例进行调用,非常简单。
调用自己生成的库,同理。

JNA 难点

如果认真写了上面的demo,其实就会发现了关键的一点:数据类型转换。这么多语言,类型都是不一样的,转换的话必然带坑。

有过跨语言、跨平台开发的程序员都知道,跨平台、语言调用的难点,就是不同语言之间数据类型不一致造成的问题。绝大部分跨平台调用的失败,都是这个问题造成的。关于这一点,不论何种语言,何种技术方案,都无法解决这个问题。JNA也不例外。

上面说到接口中使用的函数必须与链接库中的函数原型保持一致,这是JNA甚至所有跨平台调用的难点,因为C/C++的类型与Java的类型是不一样的,你必须转换类型让它们保持一致,比如printf函数在C中的原型为:

void printf(const char *format, [argument]);

你不可能在Java中也这么写,Java中是没有char *指针类型的,因此const char *转到Java下就是String类型了。

这就是类型映射(Type Mappings),JNA官方给出的默认类型映射表如下:

还有很多其它的类型映射,需要的请到JNA官网查看。

另外,JNA还支持类型映射定制,比如有的Java中可能找不到对应的类型(在Windows API中可能会有很多类型,在Java中找不到其对应的类型),JNA中TypeMapper类和相关的接口就提供了这样的功能。

JNA 取代 JNI ?

开始就说了,JNA是基于JNI实现的Java框架。使用JNI技术,不仅可以实现Java访问C函数,也可以实现C语言调用Java代码。

而JNA只能实现Java访问C函数,作为一个Java框架,自然不能实现C语言调用Java代码。此时,你还是需要使用JNI技术。

JNI是JNA的基础,是Java和C互操作的技术基础。

参考文章

https://github.com/java-native-access/jna
http://www.cnblogs.com/lanxuezaipiao/p/3635556.html
https://blog.csdn.net/shendl/article/details/3589676

发表评论