7.4.2 Java程序中集成C/C++代码
使用JNI的另外一个常见的情景是与已有的C/C++程序进行集成。在编写Java程序之前,就已经有了可以使用的原生代码库。这个原生代码库可能是程序的一部分,也可能是底层操作系统自带的。这些原生代码库的特点是在实现的时候并没有考虑与Java虚拟机的集成,因此也没有使用与JNI相关的内容。在使用这样的原生代码库时,可能会需要一个中间的原生代码库作为桥梁。这个原生代码库作为Java程序中原生方法的实现,负责实际调用时的参数类型转换和返回值传递等工作。考虑实现一个Java程序,希望使用底层操作系统上原生的消息提示对话框作为给用户提示信息的方式。使用第5章介绍的AWT或Swing用户界面库做到这一点并不难。不过下面介绍的是如何使用JNI来实现这个功能。以Windows平台为例,系统动态链接库user32.dll中包含的MessageBox方法可以提供所需的功能。
要使用user32.dll中的方法,一种做法是使用另外一个原生代码库作为桥梁。这种使用方式类似于上面介绍的第一种使用JNI的方式,即先对原生方法进行声明,再通过javah工具生成头文件,最后利用C++代码编写相关的实现。包含原生方法的Java类的基本声明如代码清单7-18所示。
代码清单7-18 消息提示Java类的基本声明
public class MessageBox{
static{
System.loadLibrary("MessageBox");
}
public native int show(String text, String caption);
}
相关的C++代码实现如代码清单7-19所示,其中也显示了JNI的原生方法实现中字符串的使用方式。Java中原生方法声明中的String类型会被转换成JNI中的jstring类型。在C++代码中,需要先通过JNIEnv中的GetStringUTFChars方法把jstring类型转换成C++中可以使用的char类型。转换之后可以直接调用user32.dll中的MessageBox方法。需要格外注意的是,在使用完从jstring类型转换后的char类型的字符串之后,需要通过ReleaseStringUTFChars方法来释放相关的内存,否则会出现内存泄露。这是因为C++中并没有Java语言中的自动内存管理机制。
代码清单7-19 消息提示的C++实现
JNIEXPORT jint JNICALL Java_com_java7book_chapter7_jni_MessageBox_show(JNIEnv*env, jobject obj, jstring text, jstring caption){
const char*text_str=env->GetStringUTFChars(text, NULL);
const char*caption_str=env->GetStringUTFChars(caption, NULL);
int result=MessageBox(0,text_str, caption_str, MB_OK|MB_ICONINFORMATION);
env->ReleaseStringUTFChars(text, text_str);
env->ReleaseStringUTFChars(caption, caption_str);
return result;
}
这种方式的不足之处在于开发人员不但需要了解C/C++语言,还需要了解JNI实现中的类型转换等细节。对于纯Java背景的开发人员来说,要调用一个已有代码库中的方法,可以使用JNA(Java Native Access)库[1]。JNA库简化了对原生代码库的调用方式,使用纯Java就可以实现对代码库的调用。JNA在实现方式上采用了代理设计模式。对于一个原生代码库,JNA可以创建出相应的代理对象。该代理对象中包含了原生代码库中已有方法所对应的Java方法,在程序中使用此代理对象即可。对代理对象中方法的调用会在进行自动类型转换之后传递给原生代码库中的相应方法,调用的返回结果也在类型转换后被正确返回。在对原生代码库中的方法进行调用时,方法查找和类型转换等操作都是由JNA负责完成的,对程序代码是透明的。
在JNA中,可以用继承自com.sun.jna.Library的接口来表示原生代码库。接口中的每个方法都对应原生代码库中的方法。接口中的方法声明需要与原生方法库中的方法相匹配。代码清单7-20中的User32Library接口表示Windows平台上的user32.dll。在接口中只声明了程序中所需的MessageBoxA方法,此方法对应于user32.dll中的MessageBoxA方法。在user32.dll中,MessageBoxA方法的声明是int MessageBoxA(HWND hWnd, LPCTSTR lpText, LPCTSTR lpCaption, UINT uType),在映射到Java接口中的方法之后,HWND类型映射到com.sun.jna.Pointer类,其他的参数类型直接映射到Java中的基本类型。在得到表示原生代码库的Java接口之后,可以使用JNA中的com.sun.jna.Native类的loadLibrary方法来得到一个实现此接口的代理对象。在调用原生方法代码库的时候直接使用代理对象即可。
代码清单7-20 表示user32.dll的User32Library接口
public interface User32Library extends Library{
User32Library INSTANCE=(User32Library)Native.loadLibrary("user32",User32Library.class);
int MessageBoxA(Pointer handle, String text, String caption, int type);
}
代码清单7-21给出了JNA代理对象的使用方式。对于与代码清单7-18中类型相同的show方法,使用JNA提供的代理对象的实现如代码清单7-21所示。调用的方式很简单,直接使用User32Library接口中的MessageBoxA方法即可。最后一个参数0x40的含义是代码清单7-19中的“MB_OK|MB_ICONINFORMATION”表达式的实际值。这里的show方法不需要声明为原生方法,只是一个普通的Java方法。
代码清单7-21 使用JNA的消息提示
public class MessageBoxJna{
public int show(String text, String caption){
return User32Library.INSTANCE.MessageBoxA(null, text, caption,0x40);
}
}
在使用JNA时,开发人员不需要直接编写C/C++代码,也不需要生成额外的原生代码库来调用已有的原生代码库中的方法。JNA所提供的代理对象可以负责完成相关的工作。JNA的不足之处在于调用原生代码库中的方法时的性能会受到一定的影响。另外在传递参数时,也不能使用在C/C++头文件中定义的常量。
注意 原生代码库中的某些方法可能是宏定义,如user32.dll中的MessageBox方法实际上是一个宏定义。实际存在的方法是MessageBoxA和MessageBoxW,分别对应使用ASCII和Unicode编码的情况。JNA无法直接翻译宏定义,需要手动选择正确的方法。如果在调用的时候出现找不到方法的错误,可以查询原生代码库的头文件或文档说明,以检查该方法名称是否为宏定义。
如果可以找到原生代码库对应的头文件,那么可以使用JNAerator工具[2]从头文件中生成JNA中Library的子接口的代码,其中包含了头文件中的全部方法。使用JNAerator工具的好处是可以避免手动映射过程中的错误,更加高效。
[1]JNA项目的网址是https://github.com/twall/jna。
[2]JNAerator工具的网址是http://code.google.com/p/jnaerator/。