Java JNA

简介

JNA 全称‘Java Native Access’ 是一个建立在传统JNI技术上的Java开源框架。JNA提供了一组Java工具类用于运行期动态访问系统本地库(Native Library)而不需要编写任何Native、JNI代码。开发者只需要在Java接口中描述目标Native Library的函数与结构,JNA会自动实现Java接口到Native Function的映射。

1,dll和so是C函数的集合和容器,这与Java中的接口概念吻合,所以JNA把dll文件和so文件看成一个个接口。在JNA中定义一个接口就是相当于了定义一个DLL/SO文件的描述文件,该接口代表了动态链接库中发布的所有函数。而且,对于程序不需要的函数,可以不在接口中声明。

2,JNA定义的接口一般继承com.sun.jna.Library接口,如果dll文件中的函数是以stdcall方式输出函数,那么,该接口就应该继承com.sun.jna.win32.StdCallLibrary接口。

3,Jna难点:编程语言之间的数据类型不一致。

类型对照

Java类型 C类型 C类型原生表现
boolean int 32位整数(可定制)
byte char 8位整数
char wchar_t 平台依赖
short short 16位整数
int int 32位整数
long long,__int64 64位整数
float float 32位浮点数
double double 64位浮点数
Buffer/Pointer pointer 平台依赖(32或64位指针)
pointer/array 32或64位指针(参数/返回值)邻接内存(结构体成员)
String char* /0结束的数组(nativeencodingorjna.encoding)
WString wchar_t* /0结束的数组(unicode)
String[] char** /0结束的数组的数组
WString[] wchar_t** /0结束的宽字符数组的数组
Structure struct*/struct 指向结构体的指针(参数或返回值)(或者明确指定是结构体指针)结构体(结构体的成员)(或者明确指定是结构体)
Union union 等同于结构体
Structure[] struct[] 结构体的数组,邻接内存
Callback (*fp)() Java函数指针或原生函数指针
NativeMapped varies 依赖于定义
NativeLong long 平台依赖(32或64位整数)
PointerType pointer 和Pointer相同

案例

下面的示例映射了标准C库中的printf函数并调用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package com.sun.jna.examples;

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

/** JNA简单接口示例 */
public class HelloWorld {

// 这是标准的,稳定的映射方式,支持广泛
// Java类型到Native类型的定制和映射

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]);
}
}
}

运行程序,如果不带参数就会单独打印“Hello,World”,如果带参则会打印出所有参数。由此可以看出,不需要写一行C代码,就可以直接在Java中调用外部动态链接库里的函数。
注意:参照上面的类型对照表,可以发现,传参不支持Class。

关于C输出语句占位符

格式 含义
%d, %i 代表整数
%f 浮点
%s 字符串
%c char
%p 指针
%fL 长log
%e 科学计数
%g 小数或科学计数。
%a,%A 读入一个浮点值(仅C99有效)。
%c 读入一个字符。
%d 读入十进制整数。
%i 读入十进制,八进制,十六进制整数。
%o 读入八进制整数。
%x, %X 读入十六进制整数。
%s 读入一个字符串,遇空格、制表符或换行符结束。
%f,%F,%e,%E,%g,%G 用来输入实数,可以用小数形式或指数形式输入。
%p 读入一个指针。
%u 读入一个无符号十进制整数。
%n 至此已读入值的等价字符数。
%[] 扫描字符集合。
%% 读 % 符号

程序解读

1、首先定义一个接口用于Native Function映射,继承自Library或StdCallLibrary
默认继承Library,如果dll中的函数是以stdcall方式调用,那么就继承StdCallLibrary,WIN32 Api都采用stdcall调用方式 就例如Windows中最为重要的kernel32库

2、接口内部定义
接口内部需要一个公共的静态常量:INSTANCE,通过这个常量我们可以获得这个接口的实例,从而使用接口的方法,也就是最关键的,调用外部dll/so的函数。该常量通过Native.load()静态函数获得,该函数的弃用方法为 Native.loadLibrary() 该函数有两个参数:

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

  • 第二个参数是本接口的Class类型。JNA通过这个Class类型,根据指定的.dll/.so文件,动态创建接口的实例。该实例由JNA通过反射自动生成。

1
CLibrary getInstance = (CLibrary) Native.load(Platform.isWindows() ? "msvcrt" : "c", CLibrary.class);

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

1
void printf(String from, Object... args);

定义的时候要注意参数和返回值的类型,需要同链接库中的函数类型保持一致

3、调用dll函数
定义好接口之后,我们就可以使用接口方法调用dll/so中相对应的函数了。

如果想调用自己实现的库函数,可以将自己实现的库函数编译成dll/so,放到当前目录下,然后和上述例子一样的去编写调用即可。

关于Windows下dll的函数导出列表可以自行查询。这里有一个网站记载了了Windows的所有dll

JNA构造结构体

在C里面没有Class类型,但是我们需要传递对象时可以通过转化为Struct