为了演示如何运用上面的技术调用一个汇编语言程序,让我们做一个简单的程序。在我们的例子里面,一个Java类(ShowMessage)调用汇编语言代码来显示一个Windows消息框。如果消息框被正常显示,汇编语言代码返回一个字符串通知调用类成功,否则将返回一个错误消息。任何一种情况下,调用类都在控制台打印返回字符串。
Java类的代码如下:
熟悉JNI的人应该注意到Java类的写法和调用C或C++写的本地方法的写法完全一样。为什么这样?肯定是这样,因为调用类不需要知道写被调用方法的语言到底是什么。Java代码里面所做的申明就是第三行里面说明是调用一个本地方法::class ShowMessage { public native String HelloDLL(String s); static { System.loadLibrary("hjwdll"); } public static void main(String[] args) { ShowMessage sm = new ShowMessage(); String returnMessage = sm.HelloDll("Hello, World of JNI"); System.out.println(returnMessage); } }
在这里我不会深入讲解Java类的内部结构 -- Sheng Liang和Bruce Eckel都讲解过。下面是我们感兴趣的汇编语言代码,我们会详细解释一些部分:public native String HelloDll(String s);
欢迎回到讲解部分,我不想详细解释程序的总体结构(需要详细信息,可以参考Iczelion的主页)。 注意<pathname> 需要根据你的系统目录结构来替换 – 在我的电脑上是C:\masm32。同时还要注意在.code那段开始部分的hwEntry过程,它表示这个程序已经被写成.dll。.dll (动态链接库)可以被Java程序在运行时刻链接。不用多说,一个.dll里面的函数在调用Window API方面没有任何限制,它可以用来做任何你想做的事情。这段 MASM代码可以被存为 hjwdll.asm,在成功编译后,会生成hjwdll.dll和hjwdll.lib文件。.386 .model flat,stdcall option casemap:none include <pathname>\include\windows.inc include <pathname>\include\user32.inc include <pathname>\include\kernel32.inc includelib <pathname>\lib\user32.lib includelib <pathname>\lib\kernel32.lib Java_ShowMessage_HelloDll PROTO :DWORD, :DWORD, :DWORD ;This macro returns pointer to the function table in fnTblPtr GetFnTblPtr MACRO envPtr, fnTblPtr mov ebx, envPtr mov eax, [ebx] mov fnTblPtr, eax ENDM ;This macro returns pointer to desired function in fnPtr. GetFnPtr MACRO fnTblPtr, index, fnPtr mov eax, index mov ebx, 4 mul ebx mov ebx, fnTblPtr add ebx, eax mov eax, [ebx] mov fnPtr, eax ENDM .data Caption db "JAV_ASM",0 ErrorMsg db "String conversion error",0 SccsMsg db "MessageBox displayed",0 .code hwEntry proc hInstance:HINSTANCE, reason:DWORD, reserved1:DWORD mov eax, TRUE ret hwEntry endp Java_ShowMessage_HelloDll proc JNIEnv:DWORD, jobject:DWORD, Msgptr:DWORD LOCAL fntblptr :DWORD LOCAL Message :DWORD LOCAL fnptr :DWORD GetFnTblPtr JNIEnv, fntblptr ; pointer to function table GetFnPtr fntblptr, 169, fnptr ; pointer to GETstringUTFChars push NULL ; push push Msgptr ; parameters for push JNIEnv ; GetStringUTFChars call [fnptr] ; call GetStringUTFChars mov Message, eax ; if eax is NULL then error .if eax == NULL invoke MessageBox, NULL, addr ErrorMsg, addr Caption, 16 GetFnPtr fntblptr, 167, fnptr ; pointer to NewStringUTF push offset ErrorMsg ; push parameters for push JNIEnv ; NewStringUTF call [fnptr] ; call NewStringUTF .else invoke MessageBox, NULL, Message, addr Caption, 64 push Message push Msgptr push JNIEnv call [fnptr] ; release string GetFnPtr fntblptr, 167, fnptr ; pointer to NewStringUTF push offset SccsMsg ; push parameters for push JNIEnv ; NewStringUTF call [fnptr] ; call NewStringUTF .endif ret ;return to Java program Java_ShowMessage_HelloDll endp End hwEntry
我们需要做的第一件事情是查找出现在汇编代码里面的本地方法的名字。在调用方法中的名字是HelloDll,我们发现在被调用方法中这个名字有了很大的不同。 从HelloDll到Java_ShowMessage_HelloDll的转换过程被称为mangling(名称粉碎),在The Java™ Native Interface Programmer's Guide and Specification 和Thinking in Java里面都有详细说明。那么这些函数被粉碎后的名字我们如何得到?答案是可以通过JNI使用的算法手工算出或者运行javah命令得到。具体到现在这里例子里面的ShowMessage类,你可以在命令行输入:javah -jni ShowMessage(注意要在ShowMessage.class的目录下运行),命令运行后你会得到一个叫ShowMessage.h的文件,里面会显示粉碎以后的函数名。要注意的一点是,如果你运行了javah,这个产生的.h文件不需要包含在汇编代码中,记住这个.h只是用来显示粉碎后的名称。
当然,我们真正感兴趣的是这两个宏:GetFnTblPtr和GetFnPtr。它们是上一段介绍的代码的改进版本。改进之处在于这些宏可以直接操作相关的内存位置,不需要通过寄存器来操作输入和输出变量。通过使用这些宏,获得指向希望调用的接口函数的指针变得相当容易。
HelloDll过程首先获取接口函数表的指针。然后它会获取指向GetStringUTFChars 这个接口函数的指针,这个函数的作用是把Java方法传递过来的String对象转换成汇编语言能够处理的UTF8字符串。接下去调用GetStringUTFChars函数需要的参数被压入堆栈。注意为了和JNI采用的stdcall约定保持一致,最右边的参数被最先压入堆栈。接口函数把它的返回值放在eax寄存器。如果发现这个值为,就表示转换过程出错了,否则一个指向UTF8字符串的有效指针会被存在eax寄存器,它可以被用来显示Java方法传递过来的消息。在这个消息被显示后,这个UTF8字符串需要象代码中显示的那样被释放。
本地方法根据它是否成功显示Java方法传递给它的消息来决定返回两个字符串中的一个给调用它的方法。然而,本地方法生成的字符串在返回之前必须被转换成一个Java的String对象。我们依靠调用NewStringUTF来实现这个操作。这里我要提醒大家注意一个事实:就是在同一线程中获取接口函数表指针只需要做一次。这就是为什么最好把指针转换过程分成两个步骤的原因,这样第一部分(也就是取得接口函数表指针)就不需要一遍遍的重复做。
当你编译完ShowMessage类,并且已经生成hjwdll.lib和hjwdll.dll文件以后,把这三个文件放在同一目录。这时你要是运行ShowMessage的话,你会看到一个象图2所示的消息框:

图2:从Java调用的Windows消息框
结论
JNI的功能远远超过这里所展示的这些。本文只是介绍了让Java代码和汇编语言代码可以相互作用的一种办法。这个例子里面演示的两点是非常重要的:如何访问接口函数以及类型转换,掌握这两点不仅仅能够让Java程序调用本地代码,而且也是开发其他类型的相互作用功能的基础。