深入浅出JNA—快速调用原生函数
By 沈东良(网名:良少)
Blog: http://blog.youkuaiyun.com/shendl
2009/7/20
本文原名《使用JNA方便地调用原生函数》发表于2009年3月的“程序员”杂志上。
感谢程序员杂志的许可,使这篇文章能够成为免费的电子版,发布于网络上。
程序员杂志发表此文时,略有裁剪,因此本文比程序员上的文章内容更多。
JNA的API参考手册和最新版本的pdf文档,可以在如下地址下载:
http://code.google.com/p/shendl/downloads/list
目录
深入浅出JNA—快速调用原生函数................................................................................................1
为什么需要JNA...............................................................................................................................2
JNA介绍...........................................................................................................................................2
JNA实现原理...................................................................................................................................2
JNA调用原生函数...................................................................................................................3
例子1 使用JNA调用原生函数................................................................................3
使用JNA调用原生函数的模式.......................................................................................3
Java和原生代码的类型映射...........................................................................................................4
Java—C和操作系统数据类型的对应表.................................................................................4
JNA支持常见的数据类型的映射...........................................................................................4
跨平台、跨语言调用原则:...................................................................................................5
JNA模拟结构体...............................................................................................................................5
例2 使用JNA调用使用Struct的C函数..........................................................................5
Structure说明..................................................................................................................6
JNA模拟复杂结构体...............................................................................................................7
例3 结构体内部可以包含结构体对象的数组.........................................................7
例4 结构体内部可以包含结构体对象的指针的数组.............................................7
原生代码调用Java代码..................................................................................................................8
例5 通过回调函数实现原生代码调用Java代码............................................................8
JNA回调函数说明...................................................................................................................9
JNA模拟指针...................................................................................................................................9
例6 使用PointerByReference模拟指向指针的指针.....................................................11
例7 使用Pointer和PointerByReference模拟指针.....................................................11
Pointer类详解.......................................................................................................................12
结语................................................................................................................................................13
为什么需要JNA
和许多解释执行的语言一样,Java提供了调用原生函数的机制,以加强Java平台的能
力。Java™NativeInterface(JNI)就是Java调用原生函数的机制。
事实上,很多Java核心代码内部就是使用JNI实现的。这些Java功能实际上是通过原
生函数提供的。
但是,使用JNI对Java开发者来说简直是一场噩梦。
如果你已经有了原生函数,使用JNI,你必须使用C语言再编写一个动态链接库,这个
动态链接库的唯一功能就是使用Java能够理解的C代码来调用目标原生函数。
这个没什么实际用途的动态链接库的编写过程令人沮丧。同时编写Java和C代码使开
发难度大大增加。
因此,在Java开发社区中,人们一直都视JNI为禁地,轻易不愿涉足。
缺少原生函数的协助使Java的使用范围大大缩小。
反观.NET阵营,其P/Invoke技术调用原生函数非常方便,不需要编写一行C代码,只
需要写Annotation就可以快速调用原生函数。因此,与硬件有关的很多开发领域都被.NET
所占据。
JNA介绍
JNA(JavaNativeAccess)框架是一个开源的Java框架,是SUN公司主导开发的,建立在
经典的JNI的基础之上的一个框架。
JNA项目地址:https://jna.dev.java.net/
JNA使Java调用原生函数就像.NET上的P/Invoke一样方便、快捷。
JNA的功能和P/Invoke类似,但编写方法与P/Invoke截然不同。JNA没有使用Annotation,
而是通过编写一般的Java代码来实现。
P/Invoke是.NET平台的机制。而JNA是Java平台上的一个开源类库,和其他类库没有
什么区别。只需要在classpath下加入jna.jar包,就可以使用JNA。
JNA使Java平台可以方便地调用原生函数,这大大扩展了Java平台的整合能力。
JNA实现原理
JNI 是Java调用原生函数唯一的机制。JNA也是建立在JNI技术之上的。它简化了Java
调用原生函数的过程。
JNA提供了一个动态的C语言编写的转发器,可以自动实现Java和C的数据类型映射。
你不再需要编写那个烦人的C动态链接库。
当然,这也意味着,使用JNA技术比使用JNI技术调用动态链接库会有些微的性能损失。
可能速度会降低几倍。但对于绝大部分项目来说,影响不大。
JNA调用原生函数
让我们先看一个JNA调用原生函数的例子。
例子1 使用JNA调用原生函数
假设我们有一个动态链接库,发布了这样一个C函数:
void say(wchar_t*pValue){
std::wcout.imbue(std::locale("chs"));
std::wcout<<L"原生函数说:"<<pValue<<std::endl;
}
它需要传入一个Unicode编码的字符数组。然后在控制台上打印出一段中文字符。
为了调用这个原生函数,使用JNA,我们需要编写这样的Java代码:
publicinterfaceTestDll1extendsLibrary{
TestDll1INSTANCE=(TestDll1)Native.loadLibrary("TestDll1",TestDll1.class);
publicvoidsay(WStringvalue);
}
这里,如果动态链接库是以stdcall方式输出函数,那么就继承StdCallLibrary。
然后就可以像普通的Java程序那样调用这个接口:
publicstaticvoidmain(String[]args){
TestDll1.INSTANCE.say(newWString("HelloWorld!"));
System.out.println("Java输出。");
}
执行,可以看到控制台下如下输出:
原生函数说:HelloWorld!
Java输出。
使用JNA调用原生函数的模式
JNA不使用native关键字。
JNI使用native关键字,使用一个个Java方法来代表外部的原生函数。
而JNA使用一个Java接口来代表一个动态链接库发布的所有函数。
对于不需要的原生函数,你可以不在Java接口中声明Java方法原型。
如果使用JNI,你需要使用System.loadLibrary方法,把我们专为JNI编写的动态链接库
载入进来。这个动态链接库实际上是我们真正需要的动态链接库的代理。
上例中使用JNA类库的Native类的loadLibrary方法,是直接把我们需要的动态链接库
载入进来。使用JNA,我们不需要编写作为代理的动态链接库,不需要编写一行原生代码。
上面的JNA代码使用了单例,接口的静态变量返回的是接口的唯一实例,这个Java对
象是JNA通过反射动态创建的。通过这个对象,我们可以调用动态链接库发布的函数。
Java和原生代码的类型映射
跨平台、跨语言调用的最大难点,就是不同语言之间数据类型不一致造成的问题。绝大
部分跨平台调用的失败,都是这个问题造成的。
JNA使用的数据类型是Java的数据类型。而原生函数中使用的数据类型是原生函数的编
程语言使用的数据类型。可能是C,Delphi,汇编等语言的数据类型。因此,不一致是在所难免
的。
JNA提供了Java和原生代码的类型映射。
Java—C和操作系统数据类型的对应表
JNA支持常见的数据类型的映射
Java 类型 C类型 原生表现
boolean int 32位整数(可定制)
byte char 8位整数
char wchar_t 平台依赖
short short 16位整数
int int 32位整数
long longlong,__int64 64位整数
float float 32位浮点数
double double 64位浮点数
Buffer
Pointer
pointer 平台依赖(32或64位指针)
<T>[](基本类型的数组)
pointer
array
32或64位指针(参数/返回值)
邻接内存(结构体成员)
Java 类型 C类型 原生表现
String char* \0结束的数组(nativeencodingorjna.encoding)
WString wchar_t* \0结束的数组(unicode)
String[] char** \0结束的数组的数组
跨平台、跨语言调用原则:
尽量使用基本、简单的数据类型;
尽量少跨平台、跨语言传递数据!
如果有复杂的数据类型需要在Java和原生函数中传递,那么我们就必须在Java中模拟
大量复杂的原生类型。这将大大增加实现的难度,甚至无法实现。
如果在Java和原生函数间存在大量的数据传递,那么一方面,性能会有很大的损失。
更为重要的是,Java调用原生函数时,会把数据固定在内存中,这样原生函数才可以访问这
些Java数据。这些数据,JVM的GC不能管理,会造成内存碎片。
如果在你需要调用的动态链接库中,有复杂的数据类型和庞大的跨平台数据传递。那么
你应该另外写一些原生函数,把需要传递的数据类型简化,把需要传递的数据量简化。
JNA模拟结构体
在原生代码中,结构体是经常使用的复杂数据类型。这里我们研究一下怎样使用JNA
模拟结构体。
例2 使用JNA调用使用Struct的C函数
假设我们现在有这样一个C语言结构体
structUserStruct{
longid;
wchar_t* name;
intage;
};
使用上述结构体的函数
#defineMYLIBAPI extern "C" __declspec(dllexport)
WString[] wchar_t**\0结束的宽字符数组的数组
Structure
struct*
struct
指向结构体的指针(参数或返回值)(或者明确指定是结构体指
针)
结构体(结构体的成员)(或者明确指定是结构体)
Union union 等同于结构体
Structure[] struct[] 结构体的数组,邻接内存
Callback <T>(*fp)() Java函数指针或原生函数指针
NativeMappedvaries 依赖于定义
NativeLong long 平台依赖(32或64位整数)
PointerType pointer 和Pointer相同
MYLIBAPIvoidsayUser(UserStruct*pUserStruct);
对应的Java程序中,在例1的接口中添加下列代码:
publicstaticclassUserStructextendsStructure{
publicNativeLongid;
publicWStringname;
publicintage;
public static class ByReference extends UserStruct
implementsStructure.ByReference{}
public static class ByValue extends UserStruct implements
Structure.ByValue
{}
}
publicvoidsayUser(UserStruct.ByReferencestruct);
Java中的调用代码:
UserStructuserStruct=newUserStruct();
userStruct.id=newNativeLong(100);
userStruct.age=30;
userStruct.name=newWString("奥巴马");
TestDll1.INSTANCE.sayUser(userStruct);
Structure说明
现在,我们就在Java中实现了对C语言的结构体的模拟。
这里,我们继承了Structure类,用这个类来模拟C语言的结构体。
必须注意,Structure子类中的公共字段的顺序,必须与C语言中的结构的顺序一致。
否则会报错!
因为,Java调用动态链接库中的C函数,实际上就是一段内存作为函数的参数传递给C
函数。
动态链接库以为这个参数就是C语言传过来的参数。
同时,C语言的结构体是一个严格的规范,它定义了内存的次序。因此,JNA中模拟的
结构体的变量顺序绝对不能错。
如果一个Struct有2个int变量。Inta,intb
如果JNA中的次序和C中的次序相反,那么不会报错,但是数据将会被传递到错误的
字段中去。
Structure类代表了一个原生结构体。当Structure对象作为一个函数的参数或者返回
值传递时,它代表结构体指针。当它被用在另一个结构体内部作为一个字段时,它代表结构
体本身。
另外,Structure类有两个内部接口Structure.ByReference和Structure.ByValue。这两个接
口仅仅是标记,如果一个类实现Structure.ByReference接口,就表示这个类代表结构体指针。
如果一个类实现Structure.ByValue接口,就表示这个类代表结构体本身。
使用这两个接口的实现类,可以明确定义我们的Structure实例表示的是结构体的指针
还是结构体本身。
上面的例子中,由于Structure实例作为函数的参数使用,因此是结构体指针。所以这
里直接使用了UserStructuserStruct=newUserStruct();
也可以使用UserStructuserStruct=newUserStruct.ByReference();
明确指出userStruct对象是结构体指针而不是结构体本身。
JNA模拟复杂结构体
C语言最主要的数据类型就是结构体。结构体可以内部可以嵌套结构体,这使它可以模
拟任何类型的对象。
JNA也可以模拟这类复杂的结构体。
例3 结构体内部可以包含结构体对象的数组
structCompanyStruct{
longid;
wchar_t* name;
UserStruct users[100];
intcount;
};
JNA中可以这样模拟:
publicstaticclassCompanyStructextendsStructure{
publicNativeLongid;
publicWString name;
public UserStruct.ByValue[] users=new
UserStruct.ByValue[100];
publicintcount;
}
这里,必须给users字段赋值,否则不会分配100个UserStruct结构体的内存,这样JNA
中的内存大小和原生代码中结构体的内存大小不一致,调用就会失败。
例4 结构体内部可以包含结构体对象的指针的数组
structCompanyStruct2{
longid;
wchar_t* name;
UserStruct* users[100];
intcount;
};
JNA中可以这样模拟:
publicstaticclassCompanyStruct2extendsStructure{
publicNativeLongid;
publicWString name;
public UserStruct.ByReference[] users=new
UserStruct.ByReference[100];
public publicintcount;
}
测试代码:
CompanyStruct2.ByReference companyStruct2=new
CompanyStruct2.ByReference();
companyStruct2.id=newNativeLong(2);
companyStruct2.name=newWString("Yahoo");
companyStruct2.count=10;
UserStruct.ByReference pUserStruct=new
UserStruct.ByReference();
pUserStruct.id=newNativeLong(90);
pUserStruct.age=99;
pUserStruct.name=newWString("杨致远");
//pUserStruct.write();
for(inti=0;i<companyStruct2.count;i++){
companyStruct2.users[i]=pUserStruct;
}
TestDll1.INSTANCE.sayCompany2(companyStruct2);
执行测试代码,报错了。这是怎么回事?
考察JNI技术,我们发现Java调用原生函数时,会把传递给原生函数的Java数据固定
在内存中,这样原生函数才可以访问这些Java数据。对于没有固定住的Java对象,GC可以
删除它,也可以移动它在内存中的位置,以使堆上的内存连续。如果原生函数访问没有被固
定住的Java对象,就会导致调用失败。
固定住哪些java对象,是JVM根据原生函数调用自动判断的。而上面的CompanyStruct2
结构体中的一个字段是UserStruct对象指针的数组,因此,JVM在执行时只是固定住了
CompanyStruct2对象的内存,而没有固定住users字段引用的UserStruct数组。因此,造成
了错误。
我们需要把users字段引用的UserStruct数组的所有成员也全部固定住,禁止GC移动
或者删除。
如果我们执行了pUserStruct.write();这段代码,那么就可以成功执行上述代码。
Structure类的write()方法会把结构体的所有字段固定住,使原生函数可以访问。
原生代码调用Java代码
JNI技术是双向的,既可以从Java代码中调用原生函数,也可以从原生函数中直接创建
Java虚拟机,并调用Java代码。
但是,这样做要写大量C代码,对于广大Java程序员来说是很头疼的。
使用JNA,我们就可以不写一行C代码,照样实现原生代码调用Java代码!
JNA可以模拟函数指针,通过函数指针,就可以实现在原生代码中调用Java函数。
让我们先看一个模拟函数指针的JNA例子:
例5 通过回调函数实现原生代码调用Java代码
intgetValue(int(*fp)(intleft,intright),intleft,intright){
returnfp(left,right);
}
C函数中通过函数指针调用外部传入的函数,执行任务。
JNA中这样模拟函数指针:
publicstaticinterfaceFpextendsCallback{
intinvoke(intleft,intright);
}
C函数用如下Java方法声明代表:
publicintgetValue(Fpfp,intleft,intright);
现在,我们有了代表函数指针int(*fp)(intleft,intright)的接口Fp,但是还没有
Fp的实现类。
publicstaticclassFpAddimplementsFp{
@Override
publicintinvoke(intleft,intright){
return left+right;
}
}
JNA回调函数说明
原生函数可以通过函数指针实现函数回调,调用外部函数来执行任务。这就是策略模式。
JNA可以方便地模拟函数指针,把Java函数作为函数指针传递给原生函数,实现在原
生代码中调用Java代码。
JNA模拟指针
JNA可以模拟原生代码中的指针。Java和原生代码的类型映射表中的指针映射是这样
的:
原生代码中的数组,可以使用JNA中对应类型的数组来表示。
Java 类型 C类型 原生表现
Buffer
Pointer
pointer 平台依赖(32或64位指针)
<T>[](基本类型的数组)
pointer
array
32或64位指针(参数/返回值)
邻接内存(结构体成员)
PointerType pointer 和Pointer相同
原生代码中的指针,可以使用Pointer类型,或者PointerType类型及它们的子类型来
模拟。
Pointer代表原生代码中的指针。其属性peer就是原生代码中指针的地址。
我们不可以直接创建Pointer对象,但可以用它表示原生函数中的任何指针。
Pointer类有2个子类:Function,Memory。
Function类代表原生函数的指针,可以通过invoke(Class,Object[],Map)这一系列的
方法调用原生函数。
Memory类代表的是堆中的一段内存,它也是我们可以创建的Pointer子类。创建一个
Memory类的实例,就是在原生代码的内存区中分配一块指定大小的内存。这块内存会在GC
释放这个Java对象时被释放。Memory类在指针模拟中会被经常用到。
PointerType类代表的是一个类型安全的指针。ByReference类是PointerType类的子
类。ByReference类代表指向堆内存的指针。ByReference类非常简单。
publicabstractclassByReferenceextendsPointerType{
protectedByReference(intdataSize) {
setPointer(newMemory(dataSize));
}
}
ByReference类有很多子类,这些类都非常有用。
ByteByReference, DoubleByReference, FloatByReference, IntByReference,
LongByReference, NativeLongByReference, PointerByReference, ShortByReference,
W32API.HANDLEByReference,X11.AtomByReference,X11.WindowByReference
ByteByReference等类故名思议,就是指向原生代码中的字节数据的指针。
PointerByReference类表示指向指针的指针。
在JNA中模拟指针,最常用到的就是Pointer类和PointerByReference类。Pointer
类代表指向任何东西的指针,PointerByReference类表示指向指针的指针。Pointer类更加
通用,事实上PointerByReference类内部也持有Pointer类的实例。
PointerByReference类可以嵌套使用,它所指向的指针,本身可能也是指向指针的指
针。PointerByReference类的源代码:
publicclassPointerByReferenceextendsByReference{
publicPointerByReference(){
this(null);
}
publicPointerByReference(Pointervalue){
super(Pointer.SIZE);
setValue(value);
}
publicvoidsetValue(Pointervalue){
getPointer().setPointer(0,value);
}
publicPointergetValue(){
returngetPointer().getPointer(0);
}
}
可以看到,PointerByReference类的构造器做了如下工作:
1,首先 在堆 中 分配 一 个指 针 大小 的 内存 , 并用 一 个Pointer对象 代表 。
PointerByReference类的实例持有这个Pointer对象。
2,然后,这个堆上新创建的指针的值被设置为传入的参数的地址,也就是指向传入的Pointer
对象。这样,新创建的Pointer对象就是指针的指针。
例6 使用PointerByReference模拟指向指针的指针
假设我们有一个结构体UserStruct的实例userStruct,现在又有了一个指向
userStruct对象的指针pUser。
为了得到UserStruct**指针在Java中的对等体,我们可以执行如下代码:
PointerByReferenceppUser=newPointerByReference(pUser);
这会在堆中创建一个指针pointer,然后把pUser指针的地址复制到pointer对象中,这
样pointer也就是指向pUser的指针。Pointer对象就是代表UserStruct**类型的指针。可以
使用ppUser.getPointer()方法返回pointer对象。
我们在Java和原生代码的类型映射表中曾经指出,PointerType和Pointer类型相同,
都可以表示指针。PointerByReference类是PointerType类的子类,因此,ppUser对象也可
以代表UserStruct**类型的指针。
例7 使用Pointer和PointerByReference模拟指针
下面,给大家展示一个完整的例子,展示如何使用Pointer和PointerByReference类型
模拟各类原生指针。
C代码:
voidsayUser(UserStruct*pUserStruct){
std::wcout.imbue(std::locale("chs"));
std::wcout<<L"ID:"<<pUserStruct->id<<std::endl;
std::wcout<<L"姓名:"<<pUserStruct->name<<std::endl;
std::wcout<<L"年龄:"<<pUserStruct->age<<std::endl;
}
voidsayUser2(UserStruct**ppUserStruct){
//UserStruct**ppUserStruct=*pppUserStruct;
UserStruct*pUserStruct=*ppUserStruct;
ppUs
er
pointer
pUser
userStruct
sayUser(pUserStruct);
}
voidsayUser3(UserStruct***pppUserStruct){
//UserStruct**ppUserStruct=*pppUserStruct;
UserStruct**ppUserStruct=*pppUserStruct;
sayUser2(ppUserStruct);
}
然后发布这3个函数。
JNA中模拟:
在接口中添加方法:
publicvoidsayUser(UserStruct.ByReferencestruct);
publicvoidsayUser2(PointerByReferenceppUserStruct);
publicvoidsayUser3(PointerpppUserStruct);
JNA中调用:
UserStructpUserStruct2=newUserStruct();
pUserStruct2.id=newNativeLong(90);
pUserStruct2.age=99;
pUserStruct2.name=newWString("乔布斯");
pUserStruct2.write();
PointerpPointer=pUserStruct2.getPointer();
PointerByReferenceppUserStruct=new
PointerByReference(pPointer);
System.out.println("使用ppUserStruct!!!!");
TestDll1.INSTANCE.sayUser2(ppUserStruct);
System.out.println("使用pppUserStruct!!!!");
PointerByReferencepppUserStruct=new
PointerByReference(ppUserStruct.getPointer());
TestDll1.INSTANCE.sayUser3(pppUserStruct.getPointer());
可以看到,我们能够使用Pointer或者PointerByReference来表示指向指针的指针。
sayUser3中,我们使用了PointerByReference类的getPointer()方法返回了代表UserStruct***
类型的指针。
事实上,如果publicvoidsayUser3(PointerpppUserStruct);定义成
publicvoidsayUser3(PointerByReferencepppUserStruct);也是可以的,只是调
用时提供的参数变为pppUserStruct对象本身即可。
通过使用Pointer和PointerByReference类,我们可以模拟任何原生代码的指针。
Pointer类详解
setPointer()方法相当于pTr2=&ptr1;
setLong()方法相当于ptr2=&long;
getPointer(0)相当于(void*)*ptr2;
取指针指向的值,返回的还是指针。
getLong(0)相当于(long)*ptr2;
取指针指向的值,返回的是long类型的数据。
结语
JNA打破了Java和原生代码原本泾渭分明的界限,实现了Java和原生代码的强强联合,
在各自擅长的领域分工合作,快速解决问题。
Java可以方便地利用原生代码的优势:执行速度快,可以直接操作硬件,机器码不容
易被破解等。
原生代码可以通过回调Java函数,利用Java的优势:开发效率高,自动内存管理,跨
平台,类库丰富,网络功能强大,支持多种脚本语言等。
JNA为Java开发者打开了一扇通向广袤的原生代码世界的大门。
By 沈东良(网名:良少)
Blog: http://blog.youkuaiyun.com/shendl
2009/7/20
本文原名《使用JNA方便地调用原生函数》发表于2009年3月的“程序员”杂志上。
感谢程序员杂志的许可,使这篇文章能够成为免费的电子版,发布于网络上。
程序员杂志发表此文时,略有裁剪,因此本文比程序员上的文章内容更多。
JNA的API参考手册和最新版本的pdf文档,可以在如下地址下载:
http://code.google.com/p/shendl/downloads/list
目录
深入浅出JNA—快速调用原生函数................................................................................................1
为什么需要JNA...............................................................................................................................2
JNA介绍...........................................................................................................................................2
JNA实现原理...................................................................................................................................2
JNA调用原生函数...................................................................................................................3
例子1 使用JNA调用原生函数................................................................................3
使用JNA调用原生函数的模式.......................................................................................3
Java和原生代码的类型映射...........................................................................................................4
Java—C和操作系统数据类型的对应表.................................................................................4
JNA支持常见的数据类型的映射...........................................................................................4
跨平台、跨语言调用原则:...................................................................................................5
JNA模拟结构体...............................................................................................................................5
例2 使用JNA调用使用Struct的C函数..........................................................................5
Structure说明..................................................................................................................6
JNA模拟复杂结构体...............................................................................................................7
例3 结构体内部可以包含结构体对象的数组.........................................................7
例4 结构体内部可以包含结构体对象的指针的数组.............................................7
原生代码调用Java代码..................................................................................................................8
例5 通过回调函数实现原生代码调用Java代码............................................................8
JNA回调函数说明...................................................................................................................9
JNA模拟指针...................................................................................................................................9
例6 使用PointerByReference模拟指向指针的指针.....................................................11
例7 使用Pointer和PointerByReference模拟指针.....................................................11
Pointer类详解.......................................................................................................................12
结语................................................................................................................................................13
为什么需要JNA
和许多解释执行的语言一样,Java提供了调用原生函数的机制,以加强Java平台的能
力。Java™NativeInterface(JNI)就是Java调用原生函数的机制。
事实上,很多Java核心代码内部就是使用JNI实现的。这些Java功能实际上是通过原
生函数提供的。
但是,使用JNI对Java开发者来说简直是一场噩梦。
如果你已经有了原生函数,使用JNI,你必须使用C语言再编写一个动态链接库,这个
动态链接库的唯一功能就是使用Java能够理解的C代码来调用目标原生函数。
这个没什么实际用途的动态链接库的编写过程令人沮丧。同时编写Java和C代码使开
发难度大大增加。
因此,在Java开发社区中,人们一直都视JNI为禁地,轻易不愿涉足。
缺少原生函数的协助使Java的使用范围大大缩小。
反观.NET阵营,其P/Invoke技术调用原生函数非常方便,不需要编写一行C代码,只
需要写Annotation就可以快速调用原生函数。因此,与硬件有关的很多开发领域都被.NET
所占据。
JNA介绍
JNA(JavaNativeAccess)框架是一个开源的Java框架,是SUN公司主导开发的,建立在
经典的JNI的基础之上的一个框架。
JNA项目地址:https://jna.dev.java.net/
JNA使Java调用原生函数就像.NET上的P/Invoke一样方便、快捷。
JNA的功能和P/Invoke类似,但编写方法与P/Invoke截然不同。JNA没有使用Annotation,
而是通过编写一般的Java代码来实现。
P/Invoke是.NET平台的机制。而JNA是Java平台上的一个开源类库,和其他类库没有
什么区别。只需要在classpath下加入jna.jar包,就可以使用JNA。
JNA使Java平台可以方便地调用原生函数,这大大扩展了Java平台的整合能力。
JNA实现原理
JNI 是Java调用原生函数唯一的机制。JNA也是建立在JNI技术之上的。它简化了Java
调用原生函数的过程。
JNA提供了一个动态的C语言编写的转发器,可以自动实现Java和C的数据类型映射。
你不再需要编写那个烦人的C动态链接库。
当然,这也意味着,使用JNA技术比使用JNI技术调用动态链接库会有些微的性能损失。
可能速度会降低几倍。但对于绝大部分项目来说,影响不大。
JNA调用原生函数
让我们先看一个JNA调用原生函数的例子。
例子1 使用JNA调用原生函数
假设我们有一个动态链接库,发布了这样一个C函数:
void say(wchar_t*pValue){
std::wcout.imbue(std::locale("chs"));
std::wcout<<L"原生函数说:"<<pValue<<std::endl;
}
它需要传入一个Unicode编码的字符数组。然后在控制台上打印出一段中文字符。
为了调用这个原生函数,使用JNA,我们需要编写这样的Java代码:
publicinterfaceTestDll1extendsLibrary{
TestDll1INSTANCE=(TestDll1)Native.loadLibrary("TestDll1",TestDll1.class);
publicvoidsay(WStringvalue);
}
这里,如果动态链接库是以stdcall方式输出函数,那么就继承StdCallLibrary。
然后就可以像普通的Java程序那样调用这个接口:
publicstaticvoidmain(String[]args){
TestDll1.INSTANCE.say(newWString("HelloWorld!"));
System.out.println("Java输出。");
}
执行,可以看到控制台下如下输出:
原生函数说:HelloWorld!
Java输出。
使用JNA调用原生函数的模式
JNA不使用native关键字。
JNI使用native关键字,使用一个个Java方法来代表外部的原生函数。
而JNA使用一个Java接口来代表一个动态链接库发布的所有函数。
对于不需要的原生函数,你可以不在Java接口中声明Java方法原型。
如果使用JNI,你需要使用System.loadLibrary方法,把我们专为JNI编写的动态链接库
载入进来。这个动态链接库实际上是我们真正需要的动态链接库的代理。
上例中使用JNA类库的Native类的loadLibrary方法,是直接把我们需要的动态链接库
载入进来。使用JNA,我们不需要编写作为代理的动态链接库,不需要编写一行原生代码。
上面的JNA代码使用了单例,接口的静态变量返回的是接口的唯一实例,这个Java对
象是JNA通过反射动态创建的。通过这个对象,我们可以调用动态链接库发布的函数。
Java和原生代码的类型映射
跨平台、跨语言调用的最大难点,就是不同语言之间数据类型不一致造成的问题。绝大
部分跨平台调用的失败,都是这个问题造成的。
JNA使用的数据类型是Java的数据类型。而原生函数中使用的数据类型是原生函数的编
程语言使用的数据类型。可能是C,Delphi,汇编等语言的数据类型。因此,不一致是在所难免
的。
JNA提供了Java和原生代码的类型映射。
Java—C和操作系统数据类型的对应表
JNA支持常见的数据类型的映射
Java 类型 C类型 原生表现
boolean int 32位整数(可定制)
byte char 8位整数
char wchar_t 平台依赖
short short 16位整数
int int 32位整数
long longlong,__int64 64位整数
float float 32位浮点数
double double 64位浮点数
Buffer
Pointer
pointer 平台依赖(32或64位指针)
<T>[](基本类型的数组)
pointer
array
32或64位指针(参数/返回值)
邻接内存(结构体成员)
Java 类型 C类型 原生表现
String char* \0结束的数组(nativeencodingorjna.encoding)
WString wchar_t* \0结束的数组(unicode)
String[] char** \0结束的数组的数组
跨平台、跨语言调用原则:
尽量使用基本、简单的数据类型;
尽量少跨平台、跨语言传递数据!
如果有复杂的数据类型需要在Java和原生函数中传递,那么我们就必须在Java中模拟
大量复杂的原生类型。这将大大增加实现的难度,甚至无法实现。
如果在Java和原生函数间存在大量的数据传递,那么一方面,性能会有很大的损失。
更为重要的是,Java调用原生函数时,会把数据固定在内存中,这样原生函数才可以访问这
些Java数据。这些数据,JVM的GC不能管理,会造成内存碎片。
如果在你需要调用的动态链接库中,有复杂的数据类型和庞大的跨平台数据传递。那么
你应该另外写一些原生函数,把需要传递的数据类型简化,把需要传递的数据量简化。
JNA模拟结构体
在原生代码中,结构体是经常使用的复杂数据类型。这里我们研究一下怎样使用JNA
模拟结构体。
例2 使用JNA调用使用Struct的C函数
假设我们现在有这样一个C语言结构体
structUserStruct{
longid;
wchar_t* name;
intage;
};
使用上述结构体的函数
#defineMYLIBAPI extern "C" __declspec(dllexport)
WString[] wchar_t**\0结束的宽字符数组的数组
Structure
struct*
struct
指向结构体的指针(参数或返回值)(或者明确指定是结构体指
针)
结构体(结构体的成员)(或者明确指定是结构体)
Union union 等同于结构体
Structure[] struct[] 结构体的数组,邻接内存
Callback <T>(*fp)() Java函数指针或原生函数指针
NativeMappedvaries 依赖于定义
NativeLong long 平台依赖(32或64位整数)
PointerType pointer 和Pointer相同
MYLIBAPIvoidsayUser(UserStruct*pUserStruct);
对应的Java程序中,在例1的接口中添加下列代码:
publicstaticclassUserStructextendsStructure{
publicNativeLongid;
publicWStringname;
publicintage;
public static class ByReference extends UserStruct
implementsStructure.ByReference{}
public static class ByValue extends UserStruct implements
Structure.ByValue
{}
}
publicvoidsayUser(UserStruct.ByReferencestruct);
Java中的调用代码:
UserStructuserStruct=newUserStruct();
userStruct.id=newNativeLong(100);
userStruct.age=30;
userStruct.name=newWString("奥巴马");
TestDll1.INSTANCE.sayUser(userStruct);
Structure说明
现在,我们就在Java中实现了对C语言的结构体的模拟。
这里,我们继承了Structure类,用这个类来模拟C语言的结构体。
必须注意,Structure子类中的公共字段的顺序,必须与C语言中的结构的顺序一致。
否则会报错!
因为,Java调用动态链接库中的C函数,实际上就是一段内存作为函数的参数传递给C
函数。
动态链接库以为这个参数就是C语言传过来的参数。
同时,C语言的结构体是一个严格的规范,它定义了内存的次序。因此,JNA中模拟的
结构体的变量顺序绝对不能错。
如果一个Struct有2个int变量。Inta,intb
如果JNA中的次序和C中的次序相反,那么不会报错,但是数据将会被传递到错误的
字段中去。
Structure类代表了一个原生结构体。当Structure对象作为一个函数的参数或者返回
值传递时,它代表结构体指针。当它被用在另一个结构体内部作为一个字段时,它代表结构
体本身。
另外,Structure类有两个内部接口Structure.ByReference和Structure.ByValue。这两个接
口仅仅是标记,如果一个类实现Structure.ByReference接口,就表示这个类代表结构体指针。
如果一个类实现Structure.ByValue接口,就表示这个类代表结构体本身。
使用这两个接口的实现类,可以明确定义我们的Structure实例表示的是结构体的指针
还是结构体本身。
上面的例子中,由于Structure实例作为函数的参数使用,因此是结构体指针。所以这
里直接使用了UserStructuserStruct=newUserStruct();
也可以使用UserStructuserStruct=newUserStruct.ByReference();
明确指出userStruct对象是结构体指针而不是结构体本身。
JNA模拟复杂结构体
C语言最主要的数据类型就是结构体。结构体可以内部可以嵌套结构体,这使它可以模
拟任何类型的对象。
JNA也可以模拟这类复杂的结构体。
例3 结构体内部可以包含结构体对象的数组
structCompanyStruct{
longid;
wchar_t* name;
UserStruct users[100];
intcount;
};
JNA中可以这样模拟:
publicstaticclassCompanyStructextendsStructure{
publicNativeLongid;
publicWString name;
public UserStruct.ByValue[] users=new
UserStruct.ByValue[100];
publicintcount;
}
这里,必须给users字段赋值,否则不会分配100个UserStruct结构体的内存,这样JNA
中的内存大小和原生代码中结构体的内存大小不一致,调用就会失败。
例4 结构体内部可以包含结构体对象的指针的数组
structCompanyStruct2{
longid;
wchar_t* name;
UserStruct* users[100];
intcount;
};
JNA中可以这样模拟:
publicstaticclassCompanyStruct2extendsStructure{
publicNativeLongid;
publicWString name;
public UserStruct.ByReference[] users=new
UserStruct.ByReference[100];
public publicintcount;
}
测试代码:
CompanyStruct2.ByReference companyStruct2=new
CompanyStruct2.ByReference();
companyStruct2.id=newNativeLong(2);
companyStruct2.name=newWString("Yahoo");
companyStruct2.count=10;
UserStruct.ByReference pUserStruct=new
UserStruct.ByReference();
pUserStruct.id=newNativeLong(90);
pUserStruct.age=99;
pUserStruct.name=newWString("杨致远");
//pUserStruct.write();
for(inti=0;i<companyStruct2.count;i++){
companyStruct2.users[i]=pUserStruct;
}
TestDll1.INSTANCE.sayCompany2(companyStruct2);
执行测试代码,报错了。这是怎么回事?
考察JNI技术,我们发现Java调用原生函数时,会把传递给原生函数的Java数据固定
在内存中,这样原生函数才可以访问这些Java数据。对于没有固定住的Java对象,GC可以
删除它,也可以移动它在内存中的位置,以使堆上的内存连续。如果原生函数访问没有被固
定住的Java对象,就会导致调用失败。
固定住哪些java对象,是JVM根据原生函数调用自动判断的。而上面的CompanyStruct2
结构体中的一个字段是UserStruct对象指针的数组,因此,JVM在执行时只是固定住了
CompanyStruct2对象的内存,而没有固定住users字段引用的UserStruct数组。因此,造成
了错误。
我们需要把users字段引用的UserStruct数组的所有成员也全部固定住,禁止GC移动
或者删除。
如果我们执行了pUserStruct.write();这段代码,那么就可以成功执行上述代码。
Structure类的write()方法会把结构体的所有字段固定住,使原生函数可以访问。
原生代码调用Java代码
JNI技术是双向的,既可以从Java代码中调用原生函数,也可以从原生函数中直接创建
Java虚拟机,并调用Java代码。
但是,这样做要写大量C代码,对于广大Java程序员来说是很头疼的。
使用JNA,我们就可以不写一行C代码,照样实现原生代码调用Java代码!
JNA可以模拟函数指针,通过函数指针,就可以实现在原生代码中调用Java函数。
让我们先看一个模拟函数指针的JNA例子:
例5 通过回调函数实现原生代码调用Java代码
intgetValue(int(*fp)(intleft,intright),intleft,intright){
returnfp(left,right);
}
C函数中通过函数指针调用外部传入的函数,执行任务。
JNA中这样模拟函数指针:
publicstaticinterfaceFpextendsCallback{
intinvoke(intleft,intright);
}
C函数用如下Java方法声明代表:
publicintgetValue(Fpfp,intleft,intright);
现在,我们有了代表函数指针int(*fp)(intleft,intright)的接口Fp,但是还没有
Fp的实现类。
publicstaticclassFpAddimplementsFp{
@Override
publicintinvoke(intleft,intright){
return left+right;
}
}
JNA回调函数说明
原生函数可以通过函数指针实现函数回调,调用外部函数来执行任务。这就是策略模式。
JNA可以方便地模拟函数指针,把Java函数作为函数指针传递给原生函数,实现在原
生代码中调用Java代码。
JNA模拟指针
JNA可以模拟原生代码中的指针。Java和原生代码的类型映射表中的指针映射是这样
的:
原生代码中的数组,可以使用JNA中对应类型的数组来表示。
Java 类型 C类型 原生表现
Buffer
Pointer
pointer 平台依赖(32或64位指针)
<T>[](基本类型的数组)
pointer
array
32或64位指针(参数/返回值)
邻接内存(结构体成员)
PointerType pointer 和Pointer相同
原生代码中的指针,可以使用Pointer类型,或者PointerType类型及它们的子类型来
模拟。
Pointer代表原生代码中的指针。其属性peer就是原生代码中指针的地址。
我们不可以直接创建Pointer对象,但可以用它表示原生函数中的任何指针。
Pointer类有2个子类:Function,Memory。
Function类代表原生函数的指针,可以通过invoke(Class,Object[],Map)这一系列的
方法调用原生函数。
Memory类代表的是堆中的一段内存,它也是我们可以创建的Pointer子类。创建一个
Memory类的实例,就是在原生代码的内存区中分配一块指定大小的内存。这块内存会在GC
释放这个Java对象时被释放。Memory类在指针模拟中会被经常用到。
PointerType类代表的是一个类型安全的指针。ByReference类是PointerType类的子
类。ByReference类代表指向堆内存的指针。ByReference类非常简单。
publicabstractclassByReferenceextendsPointerType{
protectedByReference(intdataSize) {
setPointer(newMemory(dataSize));
}
}
ByReference类有很多子类,这些类都非常有用。
ByteByReference, DoubleByReference, FloatByReference, IntByReference,
LongByReference, NativeLongByReference, PointerByReference, ShortByReference,
W32API.HANDLEByReference,X11.AtomByReference,X11.WindowByReference
ByteByReference等类故名思议,就是指向原生代码中的字节数据的指针。
PointerByReference类表示指向指针的指针。
在JNA中模拟指针,最常用到的就是Pointer类和PointerByReference类。Pointer
类代表指向任何东西的指针,PointerByReference类表示指向指针的指针。Pointer类更加
通用,事实上PointerByReference类内部也持有Pointer类的实例。
PointerByReference类可以嵌套使用,它所指向的指针,本身可能也是指向指针的指
针。PointerByReference类的源代码:
publicclassPointerByReferenceextendsByReference{
publicPointerByReference(){
this(null);
}
publicPointerByReference(Pointervalue){
super(Pointer.SIZE);
setValue(value);
}
publicvoidsetValue(Pointervalue){
getPointer().setPointer(0,value);
}
publicPointergetValue(){
returngetPointer().getPointer(0);
}
}
可以看到,PointerByReference类的构造器做了如下工作:
1,首先 在堆 中 分配 一 个指 针 大小 的 内存 , 并用 一 个Pointer对象 代表 。
PointerByReference类的实例持有这个Pointer对象。
2,然后,这个堆上新创建的指针的值被设置为传入的参数的地址,也就是指向传入的Pointer
对象。这样,新创建的Pointer对象就是指针的指针。
例6 使用PointerByReference模拟指向指针的指针
假设我们有一个结构体UserStruct的实例userStruct,现在又有了一个指向
userStruct对象的指针pUser。
为了得到UserStruct**指针在Java中的对等体,我们可以执行如下代码:
PointerByReferenceppUser=newPointerByReference(pUser);
这会在堆中创建一个指针pointer,然后把pUser指针的地址复制到pointer对象中,这
样pointer也就是指向pUser的指针。Pointer对象就是代表UserStruct**类型的指针。可以
使用ppUser.getPointer()方法返回pointer对象。
我们在Java和原生代码的类型映射表中曾经指出,PointerType和Pointer类型相同,
都可以表示指针。PointerByReference类是PointerType类的子类,因此,ppUser对象也可
以代表UserStruct**类型的指针。
例7 使用Pointer和PointerByReference模拟指针
下面,给大家展示一个完整的例子,展示如何使用Pointer和PointerByReference类型
模拟各类原生指针。
C代码:
voidsayUser(UserStruct*pUserStruct){
std::wcout.imbue(std::locale("chs"));
std::wcout<<L"ID:"<<pUserStruct->id<<std::endl;
std::wcout<<L"姓名:"<<pUserStruct->name<<std::endl;
std::wcout<<L"年龄:"<<pUserStruct->age<<std::endl;
}
voidsayUser2(UserStruct**ppUserStruct){
//UserStruct**ppUserStruct=*pppUserStruct;
UserStruct*pUserStruct=*ppUserStruct;
ppUs
er
pointer
pUser
userStruct
sayUser(pUserStruct);
}
voidsayUser3(UserStruct***pppUserStruct){
//UserStruct**ppUserStruct=*pppUserStruct;
UserStruct**ppUserStruct=*pppUserStruct;
sayUser2(ppUserStruct);
}
然后发布这3个函数。
JNA中模拟:
在接口中添加方法:
publicvoidsayUser(UserStruct.ByReferencestruct);
publicvoidsayUser2(PointerByReferenceppUserStruct);
publicvoidsayUser3(PointerpppUserStruct);
JNA中调用:
UserStructpUserStruct2=newUserStruct();
pUserStruct2.id=newNativeLong(90);
pUserStruct2.age=99;
pUserStruct2.name=newWString("乔布斯");
pUserStruct2.write();
PointerpPointer=pUserStruct2.getPointer();
PointerByReferenceppUserStruct=new
PointerByReference(pPointer);
System.out.println("使用ppUserStruct!!!!");
TestDll1.INSTANCE.sayUser2(ppUserStruct);
System.out.println("使用pppUserStruct!!!!");
PointerByReferencepppUserStruct=new
PointerByReference(ppUserStruct.getPointer());
TestDll1.INSTANCE.sayUser3(pppUserStruct.getPointer());
可以看到,我们能够使用Pointer或者PointerByReference来表示指向指针的指针。
sayUser3中,我们使用了PointerByReference类的getPointer()方法返回了代表UserStruct***
类型的指针。
事实上,如果publicvoidsayUser3(PointerpppUserStruct);定义成
publicvoidsayUser3(PointerByReferencepppUserStruct);也是可以的,只是调
用时提供的参数变为pppUserStruct对象本身即可。
通过使用Pointer和PointerByReference类,我们可以模拟任何原生代码的指针。
Pointer类详解
setPointer()方法相当于pTr2=&ptr1;
setLong()方法相当于ptr2=&long;
getPointer(0)相当于(void*)*ptr2;
取指针指向的值,返回的还是指针。
getLong(0)相当于(long)*ptr2;
取指针指向的值,返回的是long类型的数据。
结语
JNA打破了Java和原生代码原本泾渭分明的界限,实现了Java和原生代码的强强联合,
在各自擅长的领域分工合作,快速解决问题。
Java可以方便地利用原生代码的优势:执行速度快,可以直接操作硬件,机器码不容
易被破解等。
原生代码可以通过回调Java函数,利用Java的优势:开发效率高,自动内存管理,跨
平台,类库丰富,网络功能强大,支持多种脚本语言等。
JNA为Java开发者打开了一扇通向广袤的原生代码世界的大门。