“汇集”(Marshaling)是指将一个函数自变量从它原始的二进制形式转换成与语言无关的某种形式,再将这种通用形式转换成适合调用函数采用的二进制格式。在前面的例子中,我们调用了MessageBox()函数,并向它传递了两个字串。MessageBox()是个C函数,而且Java字串的二进制布局与C字串并不相同。但尽管如此,自变量仍获得了正确的传递。这是由于在调用C代码前,J/Direct已帮我们考虑到了将Java字串转换成C字串的问题。这种情况适合所有标准的Java类型。下面这张表格总结了简单数据类型的默认对应关系:
Java C byte BYTE或CHAR short SHORT或WORD int INT,UINT,LONG,ULONG或DWORD char TCHAR long __int64 float Float double Double boolean BOOL String LPCTSTR(只允许在OLE模式中作为返回值) byte[] BYTE * short[] WORD * char[] TCHAR * int[] DWORD *
这个列表还可继续下去,但已很能说明问题了。大多数情况下,我们不必关心与简单数据类型之间的转换问题。但一旦必须传递用户自定义类型的自变量,情况就立即变得不同了。例如,可能需要传递一个结构化的、用户自定义的数据类型,或者需要把一个指针传给原始内存区域。在这些情况下,有一些特殊的编译引导命令标记一个Java类,使其能作为一个指针传给结构(@dll.struct引导命令)。欲知使用这些关键字的细节,请参考产品文档。
有些Win32 API函数要求将一个函数指针作为自己的参数使用。Windows API函数随后就可以调用自变量函数(通常是在以后发生特定的事件时)。这一技术就叫作“回调函数”。回调函数的例子包括窗口进程以及我们在打印过程中设置的回调(为后台打印程序提供回调函数的地址,使其能更新状态,并在必要的时候中止打印)。
另一个例子是API函数EnumWindows(),它能枚举目前系统内所有顶级窗口。EnumWindows()要求获取一个函数指针作为自己的参数,然后搜索由Windows内部维护的一个列表。对于列表内的每个窗口,它都会调用回调函数,将窗口句柄作为一个自变量传给回调。
为了在Java里达到同样的目的,必须使用com.ms.dll包里的Callback类。我们从Callback里继承,并取消callback()。这个方法只能接近int参数,并会返回int或void。方法签名和具体的实施取决于使用这个回调的Windows API函数。现在,我们要进行的全部工作就是创建这个Callback衍生类的一个实例,并将其作为函数指针传递给API函数。随后,J/Direct会帮助我们自动完成剩余的工作。
下面这个例子调用了Win32 API函数EnumWindows();EnumWindowsProc类里的callback()方法会获取每个顶级窗口的句柄,获取标题文字,并将其打印到控制台窗口。
import com.ms.dll.*; import com.ms.win32.*; class EnumWindowsProc extends Callback { public boolean callback(int hwnd, int lparam) { StringBuffer text = new StringBuffer(50); User32.GetWindowText( hwnd, text, text.capacity()+1); if(text.length() != 0) System.out.println(text); return true; // to continue enumeration. } } public class ShowCallback { public static void main(String args[]) throws InterruptedException { boolean ok = User32.EnumWindows( new EnumWindowsProc(), 0); if(!ok) System.err.println("EnumWindows failed."); Thread.currentThread().sleep(3000); } }
对sleep()的调用允许窗口进程在main()退出前完成。
通过@dll.import引导命令内的修改符(标记),还可用到J/Direct的另两项特性。第一项是对OLE函数的简化访问,第二项是选择API函数的ANSI及Unicode版本。
根据约定,所有OLE函数都会返回类型为HRESULT的一个值,它是由COM定义的一个结构化整数值。若在COM那一级编写程序,并希望从一个OLE函数里返回某些不同的东西,就必须将一个特殊的指针传递给它——该指针指向函数即将在其中填充数据的那个内存区域。但在Java中,我们没有指针可用;此外,这种方法并不简练。利用J/Direct,我们可在@dll.import引导命令里使用ole修改符,从而方便地调用OLE函数。标记为ole函数的一个固有方法会从Java形式的方法签名(通过它决定返回类型)自动转换成COM形式的函数。
第二项特性是选择ANSI或者Unicode字串控制方法。对字串进行控制的大多数Win32 API函数都提供了两个版本。例如,假设我们观察由USER32.DLL导出的符号,那么不会找到一个MessageBox()函数,相反会看到MessageBoxA()和MessageBoxW()函数——分别是该函数的ANSI和Unicode版本。如果在@dll.import引导命令里不规定想调用哪个版本,JVM就会试着自行判断。但这一操作会在程序执行时花费较长的时间。所以,我们一般可用ansi,unicode或auto修改符硬性规定。欲了解这些特性更详细的情况,请参考微软公司提供的技术文档。
同J/Direct相比,RNI是一种比非Java代码复杂得多的接口;但它的功能也十分强大。RNI比J/Direct更接近于JVM,这也使我们能写出更有效的代码,能处理固有方法中的Java对象,而且能实现与JVM内部运行机制更紧密的集成。
RNI在概念上类似Sun公司的JNI。考虑到这个原因,而且由于该产品尚未正式完工,所以我只在这里指出它们之间的主要差异。欲了解更详细的情况,请参考微软公司的文档。
JNI和RNI之间存在几方面引人注目的差异。下面列出的是由msjavah生成的C头文件(微软提供的msjavah在功能上相当于Sun的javah),应用于前面在JNI例子里使用的Java类文件ShowMsgBox。
/* DO NOT EDIT - automatically generated by msjavah */ #include <native.h> #pragma warning(disable:4510) #pragma warning(disable:4512) #pragma warning(disable:4610) struct Classjava_lang_String; #define Hjava_lang_String Classjava_lang_String /* Header for class ShowMsgBox */ #ifndef _Included_ShowMsgBox #define _Included_ShowMsgBox #define HShowMsgBox ClassShowMsgBox typedef struct ClassShowMsgBox { #include <pshpack4.h> long MSReserved; #include <poppack.h> } ClassShowMsgBox; #ifdef __cplusplus extern "C" { #endif __declspec(dllexport) void __cdecl ShowMsgBox_ShowMessage (struct HShowMsgBox *, struct Hjava_lang_String *); #ifdef __cplusplus } #endif #endif /* _Included_ShowMsgBox */ #pragma warning(default:4510) #pragma warning(default:4512) #pragma warning(default:4610)
除可读性较差外,代码里还隐藏着一些技术性问题,待我一一道来。
在RNI中,固有方法的程序员知道对象的二进制布局。这样便允许我们直接访问自己希望的信息;我们不必象在JNI里那样获得一个字段或方法标识符。但由于并非所有虚拟机都需要将相同的二进制布局应用于自己的对象,所以上面的固有方法只能在Microsoft JVM下运行。在JNI中,通过JNIEnv自变量可访问大量函数,以便同JVM打交道。在RNI中,用于控制JVM运作的函数变成了可直接调用。它们中的某一些(如控制异常的那一个)类似于它们的JNI“兄弟”。但大多数RNI函数都有与JNI中不同的名字和用途。
JNI和RNI最重大的一个区别是“垃圾收集”的模型。在JNI中,垃圾收集在固有方法执行期间遵守与Java代码执行时相同的规则。而在RNI中,要由程序员在固有方法活动期间自行负责“垃圾收集器”器的启动与中止。默认情况下,垃圾收集器在进入固有方法前处于不活动状态;这样一来,程序员就可假定准备使用的对象用不着在那个时间段内进行垃圾收集。然而一旦固有方法准备长时间执行,程序员就应考虑激活垃圾收集器——通过调用GCEnable()这个RNI函数(GC是“Garbage Collector”的缩写,即“垃圾收集”)。也存在与全局句柄特性类似的机制——程序员可利用可保证特定的对象在GC活动期间不至于被当作“垃圾”收掉。概念是类似的,但名称有所差异——在RNI中,人们把它叫作GCFrames。
RNI与Microsoft JVM紧密集成这一事实既是它的优点,也是它的缺点。RNI比JNI复杂得多,但它也为我们提供了对JVM内部活动的高度控制;其中包括垃圾收集。此外,它显然针对速度进行了优化,采纳了C程序员熟悉的一些折衷方案和技术。但除了微软的JVM之外,它并不适于其他JVM。