开源框架JNA的使用

图灵学院

发布时间: 20-02-2704:24

Java开发过程中,有时候会需要和C,C++等交互,这时候我们就想起了经典技术JNI,但是JNI的使用过程十分繁琐,而且容易出现各种问题,还得封装而且问题不好定位。假如我们有一个.so文件,如果使用JNI去调用,我们需要另外用C语音写一个.so的共享文件,并且得使用SUN规定的数据结构去替代C语言的数据结构,至此才能调用so文件里面公布的函数。作为JAVA的程序员这个过程是令人头疼的。

相比之下,使用JNA就简单多了,只需要依赖一个jar包,就像调用一个java方法一样简单。JNA全称Java Native Access,是一个建立在JNI技术之上的Java开源框架。JNA提供一组Java工具类用于在运行期动态访问系统本地库(native library:如Window的dll,Linux的so)而不需要编写任何Native/JNI代码。开发人员只要在一个java接口中描述目标native library的函数与结构,JNA将自动实现Java接口到native function的映射。

dll和so是C函数的集合和容器,这与Java中的接口概念吻合,所以JNA把dll文件和so文件看成一个个接口。在JNA中定义一个接口就是相当于了定义一个DLL/SO文件的描述文件,该接口代表了动态链接库中发布的所有函数。而且,对于程序不需要的函数,可以不在接口中声明。JNA provides Java programs easy access to native shared libraries without writing anything but Java code - no JNI or native code is required.

JNA定义的接口一般继承com.sun.jna.Library接口,如果dll文件中的函数是以stdcall方式输出函数,那么,该接口就应该继承com.sun.jna.win32.StdCallLibrary接口。Jna难点:编程语言之间的数据类型不一致。

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

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

该常量通过Native.load()这个API函数获得,该函数有2个参数:

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

接口中只需要定义你要用到的函数或者公共变量,不需要的可以不定义,如上例只定义printf函数:

该方法对应C语言的库函数

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

Java和C的数据类型对照表

(3)调用链接库中的函数定义好接口后,就可以使用接口中的函数即相应dll/so中的函数了,调用方法就是通过接口中的实例进行调用,如上例中:

这就是JNA使用系统自带的动态链接库的简单例子,不像JNI使用用户自定义库时还得定义一大堆配置信息,对于JNA来说,使用用户自定义库与使用系统自带的库是完全一样的方法,不需要额外配置什么信息。比如在Windows下建立一个动态库程序:

然后编译成一个dll文件(比如CDLL.dll),放到当前目录下,然后编写JNA程序调用即可:

JNA模拟结构体

例:使用JNA调用使用了Struct的C函数假设我们现在有这样一个C语言结构体

使用上述结构体的函数

对应的Java 程序中,在例1 的接口中添加下列代码:

Java中的代码

Structure说明

现在,我们就在Java中实现了对C语言的结构体的模拟。这里,我们继承了Structure类,用这个类来模拟C语言的结构体。

必须注意,Structure 子类中的公共字段的顺序,必须与C语言中的结构的顺序一致。否则会报错!因为,Java调用动态链接库中的C函数,实际上就是一段内存作为函数的参数传递给C函数。动态链接库以为这个参数就是C语言传过来的参数。同时,C语言的结构体是一个严格的规范,它定义了内存的次序。因此,JNA中模拟的结构体的变量顺序绝对不能错。如果一个Struct有2个int变量。Int a, int b如果JNA中的次序和C中的次序相反,那么不会报错,但是数据将会被传递到错误的字段中去。

Structure类代表了一个原生结构体。当Structure对象作为一个函数的参数或者返回值传递时,它代表结构体指针。当它被用在另一个结构体内部作为一个字段时,它代表结构体本身。另外,Structure类有两个内部接口Structure.ByReference 和Structure.ByValue。这两个接口仅仅是标记,如果一个类实现Structure.ByReference 接口,就表示这个类代表结构体指针。

如果一个类实现Structure.ByValue 接口,就表示这个类代表结构体本身。使用这两个接口的实现类,可以明确定义我们的Structure 实例表示的是结构体的指针还是结构体本身。

上面的例子中,由于Structure 实例作为函数的参数使用,因此是结构体指针。所以这里直接使用了UserStruct userStruct=new UserStruct();

也可以使用UserStruct userStruct=new UserStruct.ByReference();明确指出userStruct 对象是结构体指针而不是结构体本身。

JNA模拟复杂结构体C语言最主要的数据类型就是结构体。结构体可以内部可以嵌套结构体,这使它可以模拟任何类型的对象。JNA也可以模拟这类复杂的结构体。

JNA中可以这样模拟:

这里,必须给users字段赋值,否则不会分配100个UserStruct结构体的内存,这样JNA中的内存大小和原生代码中结构体的内存大小不一致,调用就会失败。

例:结构体内部可以包含结构体对象的指针的数组

JNA 中可以这样模拟:

测试代码:

执行测试代码,报错了。这是怎么回事?

考察JNI技术,我们发现Java调用原生函数时,会把传递给原生函数的Java 数据固定在内存中,这样原生函数才可以访问这些Java数据。对于没有固定住的Java对象,GC可以删除它,也可以移动它在内存中的位置,以使堆上的内存连续。如果原生函数访问没有被固定住的Java对象,就会导致调用失败。固定住哪些java对象,是JVM根据原生函数调用自动判断的。而上面的CompanyStruct2结构体中的一个字段是UserStruct 对象指针的数组,因此,JVM 在执行时只是固定住了CompanyStruct2 对象的内存,而没有固定住users 字段引用的UserStruct 数组。因此,造成了错误。我们需要把users 字段引用的UserStruct 数组的所有成员也全部固定住,禁止GC 移动或者删除。如果我们执行了pUserStruct.write();这段代码,那么就可以成功执行上述代码。Structure 类的write()方法会把结构体的所有字段固定住,使原生函数可以访问。

案例一:获取本地时间(Get local time)

如果你在Java Native Access 首页 看过“JNA如何入门”,你就会知道一个很简单的关于调用Windows 平台下的API函数:GetSystemTime() 的JNA示例。这个不完整的例子只是展示了JNA的基本特点。(在例子的基础上,我做了一个更完整的基于Windows的例子来介绍JNA)我在Windows平台下完善了这个例子来介绍JNA。

第一例子基于Windows GetLocalTime() API函数返回本地当前的时间和日期。和GetSystemTime()不同的是,返回的时间/日期是协调通用时间(UTC)格式的,GetLocalTime()返回的时间/日期信息的格式是根据当前时区来表示。

在一个Java程序中使用JNA调用GetLocalTime,你需要知道这个函数所在的Windows平台下的动态链接库(DLL)的名称(和可能所在的地理区域)。我们发现GetLocalTime()和GetSystemTime在同一个DLL文件中:kernel32.dll。你还需要知道GetLocalTime()在C语言环境中的申明。申明如下Listing 1:

Listing 1. GetLocalTime在C语言中的申明

这个基于C语言的申明表明传到这个函数的参数数目和类型。在这个例子中,只有一个参数—一个指向Windows SYSTEMTIME结构体的指针。而且,每个结构体成员的类型是16bit长度的无符号整型。根据这些信息,你能够创建一个完全描述GetLocalTime()函数的接口,如Listing 2中所示:

Listing 2. Kernel32.java

Kernel32 接口(The Kernel32 interface)

因为JNA使用通过一个接口来访问某个库中的函数,Listing 2表示了一个描述GetLocalTime()的接口。根据约定,我把接口命名为Kernel32是因为GetLocalTime()在Windows的kernel32.dll库。

这个接口必须继承com.sun..jna.Library接口。因为Windows API函数遵循stdcall调用协议(stdcall calling convention),为Windows API申明的接口也必须继承com.sun.jna.win32. StdCallLibrary接口。因此这个接口共继承了Library 和 com.sun.jna.win32.StdCall两个接口。

在前面,你已经知道了GetLocalTime() 需要一个指向SYSTEMTIME结构体的指针作为它唯一的参数。因为Java不支持指针,JNA是通过申明一个com.sun.jna.Structure的子类来代替的。根据java文档中抽象类的概念,在参数环境中,Structure相当于C语言的struct*。

在SYSTEMTIME类中的字段和C结构体中的相对应的属性字段的顺序是一一对应的。保证字段顺序的一致性是非常重要的。例如,我发现交换wYear和wMonth会导致wYear和wMonth值互换。

每个字段在java中是short integer类型的。按照JNA首页上 “默认类型映射”章节给出的提示,这个short integer分配类型是正确。然而,我们应该知道一个重要的区别:Windows平台下的WORD类型等同于C语言环境中的16-bit的无符号的short integer,而java中short integer是16-bit有符号的short integer。

一个类型映射的问题

通过比较一个API 函数返回的整型值,你会发现Windows/C 语言的无符号整型和Java语言的有符号整型的JNA类型映射是有问题的。在比较的过程中,如果你不细心,那么错误的执行过程可能导致决定性情况。导致这种后果是因为忘记任何数值的符号位的确定是根据:在无符号整型的情况下会被解释为正号,而在有符号整型的进制中被理解为负号的。

通过Kernel32获取本地时间(Access the local time with Kernel32)

JNA首页上的GetSystemTime()示例已经表明必须使用预先申明的接口为本地库分配一个实例对象。你可以通过com.sun.jna.Native类中静态公用方法loadLibrary(String name, Class interfaceClass)来完成上述的目标。Listing 3 所示:

Listing 3. LocalTime.java

Listing 3 执行Kernel32 lib = (Kernel32) Native.loadLibrary (“kernel32”, Kernel32.class);来分配一个Kernel32实例对象并且装载kernel32.dll。因为kernel32.dll是Windows平台下标准的dll文件,所以不要指定访问这个库的路径。然而,如果找不到这个dll文件,loadLibrary()会抛出一个UnsatisfiedLinkError异常。

Kernel32.SYSTEMTIME time = new Kernel32.SYSTEMTIME ();创建了一个SYSTEMTIME结构体的示例。初始化后下面是lib.GetLocalTime (time);,这句话使用本地的时间/日期来给这个实例赋值。几个System.out.println()语句是输出这些值。

编译和运行这个应用(Compile and run the application)

这部分很容易。假设jna.jar、Kernel32.java和LocalTime.java是放在当前文件夹中,调用java –cp jna.jar;. LocalTime.java来编译这个应用的源代码。如果在Windows平台下,调用invoke java –cp jna.jar;. LocalTime 来运行这个应用。你可以得到类似与Listing 4的输出结果:

Listing 4. 从LocalTime.java生成的输出

Year is 2007Month is 12Day of Week is 3Day is 19Hour is 12Minute is 35Second is 13Milliseconds are 156

案例二:调用本地的使用JNA的调用本地方法的时候需要自定义数据结构,下面我们通过调用Windows提供的的锁定工作站方法来了解一下JNA。

1、首先查询Windows API知道锁定工作站的方法在user32.dll中定义,接下来定义一个接口来继承JNA的Library.java接口,用作声明DLL库文件,这里我们就把它命名为User32:

2、查询user32.dll提供的API得知锁定工作方法是LockWorkStation,返回类型是boolean型,在User32.java中新增相应的方法:

这样我们的User32.java这个类就定义好了。接下来我们写测试程序进行调用。

3、编写测试类比如LockWorkStation.java,首先通过JNA的Native类加载对应的dll:

然后就可以调用LockWorkStation方法了,完整代码如下:

复制代码这里说明一下loadLibrary方法中第一个参数是需要加载的dll文件名称,第二个参数的作用是让JNA使用这个类的加载器去加载DLL文件,加载顺序是,先从Users.class类的当前文件夹找,如果没有找到,再在工程当前文件夹下面找win32/win64文件夹,找到后搜索对应的dll文件,如果找不到再到WINDOWS下面去搜索,再找不到就会抛异常了。以TWAINDSM.dll将文件放到工程的根文件夹可以按照下面这个格式放:

附件: jnaexplorer.JPG

上面的User32定义的是dll库文件,有时会碰到比如HANDLE、POINT、WORD和MSG等数据类型,有些数据类型JNA中没有提供,需要自己定义,根据作用的不同,定义的时候继承的父类也不一样,比如HANDLE定义方法是:

HANDLE被定义为类型安全的指针。而POINT用作表示坐标,不需要这么复杂,定义方式为:

使用JNA的过程中也不一定会一帆风顺,比如会抛出”非法内存访问”,这时候检查一下变量是否==null。还有内存对齐的问题,当从内存中获取图片信息进行保存的时候,如果内存对齐处理不好,就会抛出很严重的异常,导致JVM异常退出,JNA提供了四种内存对齐的方式,分别是:ALIGN_DEFAULT、ALIGN_NONE、ALIGN_GNUC和ALIGN_MSVC。ALIGN_DEFAULT采用平台默认的对齐方式(推荐);ALIGN_NONE是不采用对齐方式;ALIGN_GNUC为针对linux/gcc操作系统的对齐方式。ALIGN_MSVC为针对win32/msvc架构的内存对齐方式。

JNA也提供了一种保护机制,比如防止JNA出现异常不会导致JVM异常退出,默认是开启这个功能的,开启方式为 System.setProperty(“jna.protected”,”true”); 记得要在JNA加载dll文件之前调用,然后try {...} catch(Throwable e)异常,不过你也不要期望过高,不要以为加上这个就万事大吉,出现”非法内存访问”的时候还是会束手无策。

举报/反馈