原文:Simplify Native Code Access with JNA
作者:Sanjay Dasgupta
出处:http://today.java.net/article/2009/11/11/simplify-native-code-access-jna
用Java语言编写的C结构
C函数通常使用结构(struct)类型作为参数,不过由于Java没有结构类型,于是JNA使用类来代替。类与结构密切相关,因此相关的Java代码看起来很直观,而且效果很好。以下的代码是从kernel32.dll的代理接口Kernel32.java中提取出来的,说明了用以支持GetSystemTime()函数的结构SYSTEMTIME到Java类的转换。
import com.sun.jna.Library; import com.sun.jna.Structure;
public interface Kernel32 extends Library { // ... (被删除的其他成员) ... public static class SYSTEMTIME extends Structure { public short wYear; public short wMonth; public short wDayOfWeek; public short wDay; public short wHour; public short wMinute; public short wSecond; public short wMilliseconds; } void GetSystemTime(SYSTEMTIME st); // ... (被删除的其他成员) ... }
|
需要注意的是,代替C的结构类型的Java类必须扩展JNA的com.sun.jna.Structure基类,把这些类内嵌在代理接口里面有助于在单个文件中整齐地安排每一样东西,当结构只是被同一个代理接口中的函数使用的时候这种做法特别有效。不过这些类也可以定义成独立的公共类(在代理接口之外),如果优先选择或者要求这样做的话。JNA网站有更多关于这些方面的资料。
下面显示的代码,即示例代码中的GetSystemTime.java,说明了对结构类型的使用。在这一例子中,被调用的函数使用结构来把信息传递“出来”,不过结构也可以用来把信息传递“进去”(如同在Windows的SetSystemTime()函数中的情况那样)或者“进去和出来”亦可。
import libs.Kernel32; import libs.Kernel32.SYSTEMTIME; import com.sun.jna.Native;
public class GetSystemTime {
public static void main(String[] args) { Kernel32 kernel32 = (Kernel32) Native.loadLibrary("kernel32", Kernel32.class); SYSTEMTIME st = new SYSTEMTIME(); kernel32.GetSystemTime(st); System.out.printf("Year: %d%n", st.wYear); System.out.printf("Month: %d%n", st.wMonth); System.out.printf("Day: %d%n", st.wDay); System.out.printf("Hour: %d%n", st.wHour); System.out.printf("Minute: %d%n", st.wMinute); System.out.printf("Second: %d%n", st.wSecond); } }
|
按照后面“运行示例代码”一节中的指引来编译和运行这一程序。
正确地找出被转换结构的每个成员的类型是很重要的,在这里犯错误通常会导致灾难性的后果,你可以通过修改SYSTEMTIME中的类型来体验一下。JNA有一些其他的技巧可以用来指定某个结构应该是通过引用(缺省情况)还是通过值来传递,以及内嵌在另一结构中的结构应如何存储。JNA网站有许多关于这些方面的指导,后面题为“从JNI转换到JNA”的一节中有几个关于C结构到Java类转换的例子。
在没有还要考虑内存对齐要求的情况下,关于跨语言的结构可移植性的讨论是不会完整的,由于文章的这部分内容主要致力于JNA的基础,因此我们把关于对齐需求的讨论推迟到较后面的一节“从JNI转换到JNA”中。
指针和字符串
在C、C++和某些其他语言中使用指针是一件非常自然的事情,但是指针的使用也激增了Java的发明者想要防止的某些错误和编程弊端,因此,虽然Java程序与C++代码极为相似,但是Java没有指针。不过这样或者那样的指针通常都会作为参数在本地函数中使用,因此JNA程序在解决这一限制方面必须要有些创造性。
下面的例子(GetVolumeInformation.java)利用了Java从C继承的语言功能:对数组的引用是指向数组的第一个元素的指针。
import libs.Kernel32; import com.sun.jna.Native;
public class GetVolumeInformation {
private static String b2s(byte b[]) { // Converts C string to Java String int len = 0; while (b[len] != 0) ++len; return new String(b, 0, len); }
public static void main(String[] args) { Kernel32 kernel32 = (Kernel32) Native.loadLibrary( "kernel32", Kernel32.class); int drives = kernel32.GetLogicalDrives(); for (int i = 0; i < 32; ++i) { if ((drives & (1 << i)) == 0) continue; String path = String.format("%c:\\", (char) ((int) 'A' + i)); byte volName[] = new byte[256], fsName[] = new byte[256]; int volSerNbr[] = new int[1], maxCompLen[] = new int[1], fileSysFlags[] = new int[1]; boolean ok = kernel32.GetVolumeInformationA(path, volName, 256, volSerNbr, maxCompLen, fileSysFlags, fsName, 256); if (ok) System.out.printf("%s %08X '%s' %s %08X%n", path, volSerNbr[0], b2s(volName), b2s(fsName), fileSysFlags[0]); else System.out.printf("%s (Offline)%n", path); } } }
|
GetVolumeInformation()的规范说明了它的第四个到第六个参数(前面黑体强调的部分)的类型是LPDWORD,这一类型可理解成“指向整型的指针”,我们使用整型数组作为这些参数的替换,这样就可以绕过Java没有指针这个问题。因此,在代理的方法声明中,这些参数都被定义成int[]类型,然后我们在运行时(参加前面的代码)传入只有一个元素的整型数组,由GetVolumeInformation()返回的值则放在这个填充每个数组的单个整型元素中。
该程序的输出如下所示,在我的计算机上,D:是一个CD-ROM驱动器,在捕获这一输出的时候,该驱动器并没有处于加载的状态,设备G:则是一个USB闪存驱动器。
C:\ 609260D7 'My-C-Drive' NTFS 000700FF D:\ (Offline) E:\ C8BCF084 'My-E-Drive' NTFS 000700FF G:\ 634BE81B 'SDG-4GB-DRV' FAT32 00000006
|
按照后面“运行示例代码”一节中的指引来编译和运行这一程序。
在前面的代码中有另一件需要注意的事情,那就是字符串传递给本地代码以及从本地代码中传递出来的方式。Java的String不需要特别的处理(查看前面代码中的变量path)就可被传“入”本地代码中,不过传递“出来”给Java的带有null终止符的串则需要谨慎地处理,可以查看一下变量volName和fsName的使用方式,以及前面代码中的方法b2s(byte b[])。最后一点需要注意的是,GetVolumeInformation()是一个宏,其“真正的”名称是GetVolumeInformationA(),可参阅该函数的规范了解所有详情。
在Java中处理指针的另一种做法是基于包com.sun.jna.ptr中的类以及类com.sun.jna.Pointer,可以在下面“从JNI转换到JNA”一节中讨论的代码那里找到这些类的使用例子。
从JNI转换到JNA
讨论完基础的功能之后,现在是时候用一些更丰盛的东西来充实一下你的头脑了。本文接下来的部分讨论在转换现有应用(基于JNI)到JNA方面所面临的问题,审视这些转换代码(包含在示例代码中)就会给你提供更加深入的了解,明白JNA如何能够被用来处理“真实的”应用的复杂性。本文所使用的JNI代码来自文章“Tech: Acquire Images with TWAIN and SANE, Part 1”,该文描述了如何使用TWAIN库从扫描仪摄像头以及其他的影像设备中获取图像。
要运行TWAIN代码的话,你最好有一个TWAIN设备(扫描仪、摄像头之类的)连接到你的计算机上,不过如果你的计算机没有安装TWAIN设备的话,你应该下载并安装TWAIN Developer Toolkit,该工具包包含了一个模拟图像源的程序。为了理解该代码,你还需要有可用的TWAIN头文件。
如果要运行TWAIN演示程序的话就执行后面的“运行示例代码”中说明的JTwainDemo.bat文件,为了理解程序的整个流程,按照原来的JNI文章中的Let There Be TWAIN一节开始的指引来做。
下面的图2说明了对来自该JNI文章的示例代码所做的修改。
图2:对来自JNI文章的“code.zip”的修改
jtwain.cpp和twain.h已被删除,因为他们只包含JNI特定的代码,Philos.java被删除是因为它与TWAIN或者JNI无关,JTwain.java被修改成包含TWAIN功能的一个JNA实现而不是初始的JNA代码。包libs是新建的,它包含的三个文件(Kernel32.java、User32.java和Win32Twain.java)是本文中讨论的代理接口。余下的三个文件则保持不变。可以觉察到包含了前面描述的那些简单例子程序的包democode并未在图2中显示出来。
TWAIN代码到JNA的转换过程照例提供了任何非一般项目都具有的学习经验,不过它也引发了一种罕见的以及隐蔽的错误——结构内存对齐错误——这是使用本地代码的Java工程所特有的。由于内存对齐错误很难被检测到,因此对于许多Java用户来说,它们可能还比较新鲜,以下几节内容提供了处理这些错误的详细指导。