原文:Simplify Native Code Access with JNA
作者:Sanjay Dasgupta
出处:http://today.java.net/article/2009/11/11/simplify-native-code-access-jna
本文介绍了把本地代码库(native library)和Java编程集成在一起的Java本地访问(Java Native Access,JNA)方法,展示了在不需要使用另一种语言编写的粘接代码的情况下,JNA是如何使Java代码调用本地函数成为可能的。文中的一些例子说明了使用模式、易犯错误以及疑难排解技术。本文还通过描述来自较早的一篇java.net文章中的JNI例子代码到JNA的转换来对JNA和JNI(Java Native Interface)做了比较。
了解JNA是很有用处的,因为看重架构中立的Java API是从不支持平台特定的功能的。因此,举例来说,如果你刚刚发明的某一killer应用程序需要播放Windows的“紧急停止”声响的话,那么你就会被卡在这个地方,因为不能借助标准的API来调用Windows的MessageBeep()函数。
虽然Java本身是架构中立的,但在本文中使用的例子代码却是、也必然是平台特定的。这些代码是在运行了32位Microsoft Windows XP和Sun JRE 1.6.0 update 16的笔记本电脑上进行开发和测试的,不过,这些代码是非常通用的,应该可以在一系列的Windows和JVM版本上运行,代码还未用到Java 1.6、Windows 2008和Windows Vista的新功能。
进行JNA开发的第一步
在开始一个JNA工程的时候,必须要进行以下几件事情:
从JNA项目网站下载jna.jar,并把它添加到工程的构建路径中,该文件是唯一需要的JNA资源,需要记住的是,jna.jar也必须被包含到运行时的类路径中。
找出Java代码将要访问的DLL的名称,DLL名称需要用来初始化JNA的联接机制。
创建用来表示应用将要访问的DLL的Java接口,本文附带的示例代码包含了三个DLL:kernel32.dll、user32.dll和Twain_32.dll的接口例子。
测试Java代码到本地方法的联接,后面的第一个例子“联接:名称的作用”描述了当JNA不能找到DLL或者DLL中的函数时,预期抛出的异常。
如果工程很大或者很复杂的话,那么在早期阶段就完成这些步骤会是一个好主意。如果需要进行概念验证(proof of concept,POC)的话,那么可以考虑在POC中包含JNA接口代码的重要部分,这有助于验证关于JNA是否适合此项工作的假设,并会降低整个工程的风险。
DLL的代理
JNA使用代理模式来隐藏本地代码集成的复杂性,它提供了一个工厂方法,Java程序使用该方法来获取DLL的代理对象,然后程序就可以通过调用代理对象相应的方法来调用DLL的函数,下面图1中的序列图描述了代理对象的创建和使用。
图1:创建DLL的Java代理对象
JNA包办了运行时的各个方面,但需要你帮助创建代理的Java类,因此你需要创建的第一段代码是带有方法定义的Java接口,这些方法定义需要和DLL的函数相一致。若要正确地与JNA运行时配合的话,接口必须扩展com.sun.jna.Library,以下的代码展示了Windows user32 DLL的代理接口的一个简略形式,需要注意的是,应该为每个DLL都提供一个这样的Java接口。
package libs; import com.sun.jna.win32.Library;
public interface User32 extends Library { ... (为了清晰删除的一些行) ... boolean LockWorkStation(); boolean MessageBeep(int uType); ... (为了清晰删除的一些行) ... }
|
许多DLL,例如Windows API中的那些,拥有众多的函数,但代理接口只需要包含应用真正用到的那些方法的声明就可以了。
联接:名称的作用
我们的第一个例子(LockWorkStation.java)非常的简单,就是在工作站运行时锁定它(与同时按下Windows的标志键和L键的效果是一样的)。它使用之前展示的User32接口来创建Windows user32 DLL的代理,然后调用代理的LockWorkStation()方法——该方法接着调用DLL的LockWorkStation()函数,JNA透明地处理了代理方法到DLL函数的运行时映射——用户只需要确保方法名称和函数名称完全相同。
import com.sun.jna.Native; // JNA基础构建 import libs.User32; // user32.dll的代理接口
public class LockWorkStation { public static void main(String[] args) {
// 创建user32.dll的代理…... User32 user32 = (User32) Native.loadLibrary("user32", User32.class);
// 借助代理调用“LockWorkStation()”…... user32.LockWorkStation();
} }
|
按照后面“运行示例代码”一节中的指引来编译和运行此程序。
LockWorkStation()调用无需参数和返回值,这消除了任何编程错误的可能性,但有两件事情仍然能够很简单地导致代码出错,情况如下:
loadLibrary()抛出信息为“Unable to load library ... The specified module could not be found.”的java.lang.UnsatisfiedLinkError异常,如果这一错误出现的话,检查一下DLL名称的拼写,以及验证一下DLL是否存在于搜索目录中(查看JNA文档)。
代理方法(例如LockWorkStation())抛出信息为“Error looking up function ... The specified procedure could not be found.”的java.lang.UnsatisfiedLinkError异常,如果这一错误出现的话,检查一下函数名称的拼写,以及验证一下该“函数”实际上并非一个宏(macro),Windows API DLL包含了不少的这样的宏(例如Winuser.h中定义的GetMessage()),因此,仔细阅读一下DLL的文档(以及相应的头文件),宏的名称需要人工进行转换。
在运行LockWorkStation.java的时候应该不会收到任何一个这样的异常,不过只要修改DLL的名称或者函数的名称然后重新编译代码,你就可以模拟这些错误。事实上,JNA确实有允许使用与函数名称(在DLL中)不同的方法名称(在代理接口中)的机制,关于这一功能的更多信息可以在JNA文档中找到。
参数和返回类型
下面展示了我们的下一个例子,使用Windows的Beep()函数来逐字地发出代表“Hello world”的摩尔斯电码的声音。
import com.sun.jna.Native; // JNA基础构建 import libs.Kernel32; // kernel32.dll的代理接口
public class BeepMorse { private static Kernel32 kernel32 = (Kernel32) Native.loadLibrary("kernel32", Kernel32.class);
private static void toMorseCode(String letter) throws Exception { for (byte b : letter.getBytes()) { kernel32.Beep(1200, ((b == '.') ? 50 : 150)); Thread.sleep(50); } }
public static void main(String[] args) throws Exception { String helloWorld[][] = { {"....", ".", ".-..", ".-..", "---"}, // HELLO {".--", "---", ".-.", ".-..", "-.."} // WORLD }; for (String word[] : helloWorld) { for (String letter : word) { toMorseCode(letter); Thread.sleep(150); } Thread.sleep(350); } } }
|
Beep()函数有两个参数:频率和持续时间,这两个类型为DWORD的值被定义为无符号的长整型,由于当前所有的Windows系统中的无符号长整型都是32位长的,因此我们在代理接口中使用Java的int类型来定义这两个参数,如下所示:
package libs; import com.sun.jna.Library;
public interface Kernel32 extends Library { // ... (为了清晰删除的一些行) ... boolean Beep(int frequency, int duration); int GetLogicalDrives(); // ... (为了清晰删除的一些行) ... }
|
给出正确的参数类型非常重要,你可以通过修改Beep()的参数类型来验证这一点,把定义修改成Beep(long, long)或者Beep(float, float)不会引起任何的运行时错误,但是你会完全听不到任何声音。JNA的网站上有一些关于把Windows类型转换成Java类型的资料,更多详细的信息则可以在维基百科的Windows Programming/Handles and Data Types页面和Microsoft的Windows Data Types网页中找到。
按照后面“运行示例代码”一节中的指引来编译和运行这一程序,不过记得要先把音量调低!
Beep()返回一个布尔值,虽然在这个例子中它被忽略了,不过如果函数返回的值是有用的话,那么必须使用与参数类型相同的准则把返回的类型映射到一个合适的Java类型上。下面的代码(GetLogicalDrives.java)说明了由kernel32 DLL中的GetLogicalDrives()函数返回的int值的使用情况。
import com.sun.jna.Native; import libs.Kernel32;
public class GetLogicalDrives {
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) { int bit = (1 << i); if ((drives & bit) == 0) continue; System.out.printf("%c:\\%n", (char) ((int) 'A' + i)); } } }
|
不过需要注意的是,在现实情况中,Java程序应该从不需要使用JNA来调用GetLogicalDrives(),因为java.io.File.listRoots()提供了同样的信息。
按照后面“运行示例代码”一节中的指引来编译和运行该程序。
本文的引言部分提到了使用Windows的标准声响来指示具体的事件,这些声响可以通过调用MessageBeep(int type)函数来产生,可以在文件MessageBeep.java中找到展示使用MessageBeep()的示例代码。