您的位置:首页 > 其它

开源项目JNA

2008-10-31 18:12 337 查看

开源项目:JavaNativeAccess

用更容易的方法来调用本地代码
ByJeffFriesen,JavaWorld.com,02/05/08

如果在Java程序中你使用JavaNativeInterface(JNI)来调用某个特定平台下的本地库文件,你就会发现这个过程很单调、乏味。JeffFriesen一直在介绍一个知名度很低的Java开源项目:JavaNativeAccess---它能够避免因使用JNI导致的错误和乏味,同时它还能让你通过编程的方式调用C语言库。

在Java语言没有提供必要的APIs的情况下,Java程序使用JavaNativeInterface(JNI)来调用特定平台下的本地库是必要的。例如:在WindowsXP平台中,我使用过JNI来调用通用串行总线和基于TWAIN的扫描仪器的库;在更古老的WindowsNT平台中,调用过智能卡的库。

我按照一个基本的、乏味的流程来解决这些问题:首先,我创建一个Java类用来载入JNI-friendly库(这个库能够访问其他的库)并且声明这个类的本地方法。然后,在使用JDK中的javah工具为JNI-friendly库中的函数---函数和这个类中的本地方法一一对应---创建一个代理。最后,我使用C语言写了一个库并用C编译器编译了这些代码。

尽管完成这些流程并不是很困难,但是写C代码是一个很缓慢的过程---例如:C语言中的字符串处理是通过指针来实现的,这会很复杂的。而且,使用JNI很容易出现错误,导致内存泄漏、很难找到程序崩溃的原因。

Java开源系列的第二篇文章中,我要介绍一个更简单、更安全的解决方法:ToddFastandTimothyWall的JavaNativeAccess(JNA)项目。JNA能够让你在Java程序中调用本地方法时避免使用C和JavaNativeInterface。在这篇文章中,让我以简要的介绍JNA和运行示例必需的软件来开始下面的内容。然后,向你展示如何使用JNA将3个Windows本地库中的有用代码移植到Java程序中。

GetstartedwithJNA(JNA入门)

JavaNativeAccess项目在Java.net上,你可以到这个网站上现在这个项目的代码和在线帮助文档。虽然在下载有5个相关的jar文件,在本文中你仅仅需要下载其中的jna.jar和example.jar。

Jna.jar提供基本的、运行这些示例文件必需的jna运行环境。这个jna.jar文件除了有Unix、Linux、Windows和MacOSX平台相关的JNT-friendly本地库外,还包含其他几个类包。每一个本地库都是用来访问相对应平台下的本地方法的。

example.jar包含了不同的示例来表明JNA的用途。其中的一个例子是使用JNA来实现一个在不同平台下的透明视窗技术的API。在文章最后的示例中将要展示如何使用这个API修复上个月的文章关于VerifyAge2应用中辨认透明效果的问题。

获取本地时间(Getlocaltime)

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

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

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

Listing1.GetLocalTime在C语言中的申明

typedefstruct
{
WORDwYear;
WORDwMonth;
WORDwDayOfWeek;
WORDwDay;
WORDwHour;
WORDwMinute;
WORDwSecond;
WORDwMilliseconds;
}
SYSTEMTIME,*LPSYSTEMTIME;
VOIDGetLocalTime(LPSYSTEMTIMElpst);


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

Listing2.Kernel32.java

//Kernel32.java
importcom.sun.jna.*;
importcom.sun.jna.win32.*;
publicinterfaceKernel32extendsStdCallLibrary
{
publicstaticclassSYSTEMTIMEextendsStructure
{
publicshortwYear;
publicshortwMonth;
publicshortwDayOfWeek;
publicshortwDay;
publicshortwHour;
publicshortwMinute;
publicshortwSecond;
publicshortwMilliseconds;
}
voidGetLocalTime(SYSTEMTIMEresult);
}

Kernel32接口(TheKernel32interface)

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

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

在前面,你已经知道了GetLocalTime()需要一个指向SYSTEMTIME结构体的指针作为它唯一的参数。因为Java不支持指针,JNA是通过申明一个com.sun.jna.Structure的子类来代替的。根据java文档中抽象类的概念,在参数环境中,Structure相当于C语言的struct*。
一个类型映射的问题
通过比较一个API函数返回的整型值,你会发现Windows/C语言的无符号整型和Java语言的有符号整型的JNA类型映射是有问题的。在比较的过程中,如果你不细心,那么错误的执行过程可能导致决定性情况。导致这种后果是因为忘记任何数值的符号位的确定是根据:在无符号整型的情况下会被解释为正号,而在有符号整型的进制中被理解为负号的。
在SYSTEMTIME类中的字段和C结构体中的相对应的属性字段的顺序是一一对应的。保
证字段顺序的一致性是非常重要的。例如,我发现交换wYear和wMonth会导致wYear和wMonth值互换。

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

通过Kernel32获取本地时间(AccessthelocaltimewithKernel32)

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

Listing3.LocalTime.java
//LocalTime.java

importcom.sun.jna.*;

publicclassLocalTime
{
publicstaticvoidmain(String[]args)
{
Kernel32lib=(Kernel32)Native.loadLibrary("kernel32",
Kernel32.class);
Kernel32.SYSTEMTIMEtime=newKernel32.SYSTEMTIME();
lib.GetLocalTime(time);
System.out.println("Yearis"+time.wYear);
System.out.println("Monthis"+time.wMonth);
System.out.println("DayofWeekis"+time.wDayOfWeek);
System.out.println("Dayis"+time.wDay);
System.out.println("Houris"+time.wHour);
System.out.println("Minuteis"+time.wMinute);
System.out.println("Secondis"+time.wSecond);
System.out.println("Millisecondsare"+time.wMilliseconds);
}
}

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

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

编译和运行这个应用(Compileandruntheapplication)

这部分很容易。假设jna.jar、Kernel32.java和LocalTime.java是放在当前文件夹中,调用java–cpjna.jar;.LocalTime.java来编译这个应用的源代码。如果在Windows平台下,调用invokejava–cpjna.jar;.LocalTime来运行这个应用。你可以得到类似与Listing4的输出结果:
Listing4.LocalTime.java生成的输出
Yearis2007
Monthis12
DayofWeekis3
Dayis19
Houris12
Minuteis35
Secondis13
Millisecondsare156

获取操纵杆信息(Accessingjoystickdeviceinfo)

上面的例子已经介绍了JNA,但是这个获取本地时间和日期的例子并没有很好的利用这个技术,甚至也没有体现JNI的价值。Java语言中的System.currentTimeMillis()函数已经以毫秒的格式返回了这些信息。因为Java语言没有为游戏控制器提供API,所以获取操纵杆的信息更适合JNA的使用。

例如,你要构建一个平台无关的Java库,而且这些库使用JNA调用Linux,MacOSX,Windwos和Unix平台中本地的操纵杆API。为了简洁和方便起见,这个例子仅仅是调用Windows平台下的操纵杆API。而且我将重点介绍这个API很小的一部分。

类似GetLocalTime(),第一步是辨别出操作杆API的DLL,这个DLL是winmm.dll,和kernel32.dll在同一个文件夹中,它包含了操作杆的API和其他的多媒体APIs。还需知道要被使用的操作杆函数基于C语言的声明。这些函数声明已经在Listing5中列出来了。
Listing5.C-baseddeclarationsforsomeJoystickAPIfunctions
#defineMAXPNAMELEN32

typedefstruct
{
WORDwMid;//manufactureridentifier
WORDwPid;//productidentifier
TCHARszPname[MAXPNAMELEN];//productname
UINTwXmin;//minimumxposition
UINTwXmax;//maximumxposition
UINTwYmin;//minimumyposition
UINTwYmax;//maximumyposition
UINTwZmin;//minimumzposition
UINTwZmax;//maximumzposition
UINTwNumButtons;//numberofbuttons
UINTwPeriodMin;//smallestsupportedpollingintervalwhencaptured
UINTwPeriodMax;//largestsupportedpollingintervalwhencaptured
}
JOYCAPS,*LPJOYCAPS;

MMRESULTjoyGetDevCaps(UINTIDDevice,LPJOYCAPSlpjc,UINTcbjc);

UINTjoyGetNumDevs(VOID);
操作杆API的函数(FunctionsoftheJoystickAPI)
在Windows平台下是通过以joy作为函数名开始的函数以及被各种函数调用的结构体来实现操作杆API的。例如,joyGetNumDevs()返回的是这个平台下支持的操作杆设备最多的数目;joyGetDevCaps()返回的是每个连接上的操纵杆的质量。

joyGetDevCaps()函数需要3个参数:

处在0到joyGetNumDevs()-1之间的操作杆ID

保存返回的质量信息的JOYCAPS结构体的地址

JOYCAPS结构体的字节大小

虽然它的结果不同,这个函数返回的是一个32位的无符号整型结果,而且0表示一个已经连接的操纵杆。

JOYCAPS结构体有3种类型。Windows平台下的WORD(16位无符号短整型)类型对应的是Java语言中16位有符号短整型。除此之外,Windows下的UINT(32位无符号整型)类型是和Java语言中32位有符号整型相对应的。而Windows平台上的textcharacter就是TCHAR类型。

微软通过TCHAR类型使开发人员能够从ASCII类型的函数参数平滑的转移到Unicode字符类型的函数参数上。而且,拥有text类型参数的函数的实现是通过宏转变为对应的ASCII或者wide-character的函数。例如,joyGetDevCaps()是一个对应joyGetDevCapsA()和joyGetDevCapsW()的宏。

使用TCHAR(WorkingwithTCHAR)
使用TCHAR和将TCHAR转变的宏会导致基于C语言的申明向基于JNA接口的转换
变得有点复杂—你在使用ASCII或者wide-character版本的操纵杆函数吗?两种版本都在如下的接口中展示了:

Listing6.WinMM.java
//WinMM.java

importcom.sun.jna.*;
importcom.sun.jna.win32.*;

publicinterfaceWinMMextendsStdCallLibrary
{
finalstaticintJOYCAPSA_SIZE=72;

publicstaticclassJOYCAPSAextendsStructure
{
publicshortwMid;
publicshortwPid;
publicbyteszPname[]=newbyte[32];
publicintwXmin;
publicintwXmax;
publicintwYmin;
publicintwYmax;
publicintwZmin;
publicintwZmax;
publicintwNumButtons;
publicintwPeriodMin;
publicintwPeriodMax;
}

intjoyGetDevCapsA(intid,JOYCAPSAcaps,intsize);

finalstaticintJOYCAPSW_SIZE=104;

publicstaticclassJOYCAPSWextendsStructure
{
publicshortwMid;
publicshortwPid;
publiccharszPname[]=newchar[32];
publicintwXmin;
publicintwXmax;
publicintwYmin;
publicintwYmax;
publicintwZmin;
publicintwZmax;
publicintwNumButtons;
publicintwPeriodMin;
publicintwPeriodMax;
}

intjoyGetDevCapsW(intid,JOYCAPSWcaps,intsize);

intjoyGetNumDevs();
}

Listing6没有介绍JNA的新特性。实际上,JNA强调了对本地库的接口命名规则。同时,还展示了如何将TCHAR映射到Java语言中的byte和char数组。最后,它揭示了以常量方式声明的结构体的大小。Listing7展示了当调用joyGetDevCapsA()和joyGetDevCapsW()时如何使用这些常量。
Listing7.JoystickInfo.java
//JoystickInfo.java

importcom.sun.jna.*;

publicclassJoystickInfo
{
publicstaticvoidmain(String[]args)
{
WinMMlib=(WinMM)Native.loadLibrary("winmm",WinMM.class);
intnumDev=lib.joyGetNumDevs();

System.out.println("joyGetDevCapsA()Demo");
System.out.println("---------------------/n");

WinMM.JOYCAPSAcaps1=newWinMM.JOYCAPSA();
for(inti=0;i<numDev;i++)
if(lib.joyGetDevCapsA(i,caps1,WinMM.JOYCAPSA_SIZE)==0)
{
Stringpname=newString(caps1.szPname);
pname=pname.substring(0,pname.indexOf('/0'));
System.out.println("Device#"+i);
System.out.println("wMid="+caps1.wMid);
System.out.println("wPid="+caps1.wPid);
System.out.println("szPname="+pname);
System.out.println("wXmin="+caps1.wXmin);
System.out.println("wXmax="+caps1.wXmax);
System.out.println("wYmin="+caps1.wYmin);
System.out.println("wYmax="+caps1.wYmax);
System.out.println("wZmin="+caps1.wZmin);
System.out.println("wZmax="+caps1.wZmax);
System.out.println("wNumButtons="+caps1.wNumButtons);
System.out.println("wPeriodMin="+caps1.wPeriodMin);
System.out.println("wPeriodMax="+caps1.wPeriodMax);
System.out.println();
}

System.out.println("joyGetDevCapsW()Demo");
System.out.println("---------------------/n");

WinMM.JOYCAPSWcaps2=newWinMM.JOYCAPSW();
for(inti=0;i<numDev;i++)
if(lib.joyGetDevCapsW(i,caps2,WinMM.JOYCAPSW_SIZE)==0)
{
Stringpname=newString(caps2.szPname);
pname=pname.substring(0,pname.indexOf('/0'));
System.out.println("Device#"+i);
System.out.println("wMid="+caps2.wMid);
System.out.println("wPid="+caps2.wPid);
System.out.println("szPname="+pname);
System.out.println("wXmin="+caps2.wXmin);
System.out.println("wXmax="+caps2.wXmax);
System.out.println("wYmin="+caps2.wYmin);
System.out.println("wYmax="+caps2.wYmax);
System.out.println("wZmin="+caps2.wZmin);
System.out.println("wZmax="+caps2.wZmax);
System.out.println("wNumButtons="+caps2.wNumButtons);
System.out.println("wPeriodMin="+caps2.wPeriodMin);
System.out.println("wPeriodMax="+caps2.wPeriodMax);
System.out.println();
}
}
}
尽管和LocalTime这个示例类似,JoystickInfo执行WinMMlib=(WinMM)Native.loadLibrary("winmm",WinMM.class);这句话来获取一个WinMM的实例,并且载入winmm.dll。它还执行WinMM.JOYCAPSAcaps1=newWinMM.JOYCAPSA();和WinMM.JOYCAPSWcaps2=newWinMM.JOYCAPSW();初始化必需的结构体实例。

编译和运行这个程序(Compileandruntheapplication)
将C语言中的string类型转换为Java语言的String类型
pname=pname.substring(0,pname.indexOf('/0'));这段代码将一个Cstring转换成了Javastring.如果不使用这个转换,C语言的string结束符’/0’和string后面的无用字符都会成为Java语言中String实例对象的内容。
假如jna.jar,WinMM.java和JoystickInfo.java在同一个文件夹中,调用javac-cpjna.jar;.JoystickInfo.java来编译这个应用的源代码。
在windows平台下,调用java-cpjna.jar;.JoystickInfo就可以运行这个应用程序了。如果没有操纵杆设备,你应该得到Listing8中的输出。



Listing8.输出操纵杆信息(OutputofJoystickInfo)
joyGetDevCapsA()Demo
---------------------

joyGetDevCapsW()Demo
---------------------
上面的输出是因为每次调用joyGetDevCap()返回的是一个非空值,这表示没有操纵杆/游戏控制器设备或者是出现错误。为了获取更多有意思的输出,将一个设备连接到你的平台上并且再次运行JoystickInfo。如下,将一个微软SideWinder即插即用游戏触摸板设备联上之后我获取了如下的输出:
Listing9.操纵杆连接上之后的运行结果(OutputafterrunningJoystickInfowithajoystickattached)
joyGetDevCapsA()Demo
---------------------

Device#0
wMid=1118
wPid=39
szPname=MicrosoftPC-joystickdriver
wXmin=0
wXmax=65535
wYmin=0
wYmax=65535
wZmin=0
wZmax=65535
wNumButtons=6
wPeriodMin=10
wPeriodMax=1000

joyGetDevCapsW()Demo
---------------------

Device#0
wMid=1118
wPid=39
szPname=MicrosoftPC-joystickdriver
wXmin=0
wXmax=65535
wYmin=0
wYmax=65535
wZmin=0
wZmax=65535
wNumButtons=6
wPeriodMin=10
wPeriodMax=1000

窗口透明度(Transparentwindows)

在这系列文章中上篇文章是关于BernhardPauler's气泡提示(balloontip)工程的。我构建了一个叫做VerifyAge的、包含有一个气泡提示的GUI应用。Figure1中显示了这个GUI应用的一个小问题:这个气泡提示的没有经过修饰的对话框部分遮住了应用窗口的一部分边框,导致了无法点击这个边框的最小化和最大化按钮,并且使整个GUI很难看.

Figure1.ItisnotpossibletominimizetheGUIwhentheballoontipisdisplayed
尽管未修饰部分的对话框不能显示气泡提示的透明度,java语言不支持窗口透明度。幸运的是,我们可以通过使用com.sun.jna.examples.WindowUtils类调用JNA的examples.jar文件来解决这个问题。
WindowUtils提供在Unix,Linux,MacOSX和Windows平台上使用JNA’s来实现窗口透明的工具方法。例如,publicstaticvoidsetWindowMask(finalWindoww,Iconmask)让你根据像素而不是通过预定的掩罩(mask)参数来选取某部分的窗口。这个功能将在Listing10中展示:

Listing10.UsingJNAtorenderawindowtransparent
//Createamaskforthisdialog.Thismaskhasthesameshapeasthe
//dialog'sroundedballoontipandensuresthatonlytheballoontip
//partofthedialogwillbevisible.Allotherdialogpixelswill
//disappearbecausetheycorrespondtotransparentmaskpixels.

//Note:Thedrawingcodeisbasedonthedrawingcodein
//RoundedBalloonBorder.

Rectanglebounds=getBounds();
BufferedImagebi=newBufferedImage(bounds.width,bounds.height,
BufferedImage.TYPE_INT_ARGB);
Graphicsg=bi.createGraphics();
g.fillRoundRect(0,0,bounds.width,bounds.height-VERT_OFFSET,
ARC_WIDTH*2,ARC_HEIGHT*2);
g.drawRoundRect(0,0,bounds.width-1,bounds.height-VERT_OFFSET-1,
ARC_WIDTH*2,ARC_HEIGHT*2);
int[]xPoints={HORZ_OFFSET,HORZ_OFFSET+VERT_OFFSET,HORZ_OFFSET};
int[]yPoints={bounds.height-VERT_OFFSET-1,bounds.height-VERT_OFFSET
-1,bounds.height-1};
g.fillPolygon(xPoints,yPoints,3);
g.drawLine(xPoints[0],yPoints[0],xPoints[2],yPoints[2]);
g.drawLine(xPoints[1],yPoints[1],xPoints[2],yPoints[2]);
g.dispose();
WindowUtils.setWindowMask(this,newImageIcon(bi));
在Listing10中的代码段是从本文代码文档(codearchive)里的加强版的VerifyAge2应用中的TipFrame的构造函数结尾部分摘录的。这个构造函数定义了围绕提示气泡的掩罩(mask)的形状,在这个形状范围里描绘不透明的像素。
假如你当前文件夹中有examples.jar,jna.jar,和VerifyAge2.java,调用javac-cpexamples.jar;balloontip.jarVerifyAge2.java来编译源文件.然后调用java-Dsun.java2d.noddraw=true-cpexamples.jar;balloontip.jar;.VerifyAge2运行这个应用.Figure2展示了透明示例.

Figure2.YoucannowminimizetheGUIwhenaballoontipappears

总结(Inconclusion)

JNA项目有很长的历史了(追溯到1999年),但是它第一次发布是在2006年11月。从此以后它慢慢的被需要将本地C代码整合到Java工程中的开发者注意到了。因为JNA能够用来解决JuRuby中常见一个问题:缺乏对POSIX调用的支持(lackofsupportforPOSIXcalls),它也在JRuby程序员中掀起些波浪。JNA也同样被作为实现用低级C代码继承Ruby的一种解决方案(extendingRubywithlow-levelCcode)。
我喜欢使用JNA来工作,相信你也会发现它比使用JNI来访问本地代码更简单、更安全。无需多言,JNA还有更多的特性在本文中没有体现出来。查阅它的资源部分:获取这个开源java项目更多的信息(learnmoreaboutthisopensourceJavaproject)。用它做demo,而且在论坛(discussionforum)上共享你的经验。下一个月我会带着另一个开源项目回来的,这个开源项目会给你每天的java开发带来益处。

附录:WindowUtils.setWindowMask()的替代品

在刚刚写完这篇文章后,我发现java语言支持在6u10版本中支持窗口的透明和形状定制。读完KirillGrouchnikov的博客后,我用WindowUtils.setWindowMask()的替代品修改了VerifyAge2,如下:
//Createandinstallaballoontipshapetoensurethatonlythispart
//ofthedialogwillbevisible.

Rectanglebounds=getBounds();
GeneralPathgp;
gp=newGeneralPath(newRoundRectangle2D.Double(bounds.x,bounds.y,
bounds.width,
bounds.height-
VERT_OFFSET,
ARC_WIDTH*2-1,
ARC_HEIGHT*2-1));
gp.moveTo(HORZ_OFFSET,bounds.height-VERT_OFFSET);
gp.lineTo(HORZ_OFFSET,bounds.height);
gp.lineTo(HORZ_OFFSET+VERT_OFFSET+1,bounds.height-VERT_OFFSET);
AWTUtilities.setWindowShape(this,gp);

这段代码使用新类AWTUtilities(在com.sun.awt包中),而且publicvoidsetWindowShape(Windoww,Shapes)函数将TipFrame和JDialog窗口设置气泡形状。

作者资料

JeffFriesen是一名自由的软件开发人员,同时也是Java技术领域的的教育者。在他的javajeff.mb.ca网站上可以获取他发布的所有的Java文章和其他资料。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: