Java本地接口(JNI)提供了一个强大的平台用以整合Java代码和其他语言编写的代码 – 主要是C和C++。尽管从理论上讲,JNI提供了一个相当广义的接口,实际上JNI提供的支持结构主要是基于帮助Java链接C/C++代码。相关的文献资料似乎也主要是介绍链接Java和C/C++的专门方法。
本文演示了让Java代码调用汇编语言编写的代码的技术。用来写演示代码的汇编语言版本是MASM32。我假设本文读者熟悉汇编语言编程特别是MASM32。熟悉Java语言也是需要的。参考资源部分列出了我参考和学习的来源,我希望感兴趣进一步学习的读者也能从这些资源中获得教益。
在正式开始教程之前,有一点我想提醒大家:接下来展示的方法和实例都是基于Windows平台,不过你不用考虑Windows的特定版本,它可以顺利运行于任何版本,我自己在Win 98/2000/XP上面都尝试过没有任何问题。
正如我前面提到的,JNI相关的参考资源都是为C/C++开发准备的。对于使用汇编语言的开发者,必须深入的理解JNI如何对外暴露接口的细节。因此,就让我们来看看JNI的一些内部细节。
JNI入门
当一个Java方法调用汇编语言代码时,一些信息必须从一个语言环境移动到另一个语言环境。调用者通常要传递一些参数给被调用函数,同时被调用函数需要返回一些信息给调用者。除此之外,每个语言环境都需要与另一个语言环境共同工作所需的信息。问题是在Java虚拟机(JVM)中表示的数据和汇编语言环境下的数据是不同的。还有一些资料,尤其是JVM内部的一些是特别底层的,JVM不会向本地语言(C/C++/汇编语言)提供对于这些信息的直接访问。JNI提供了一整套丰富的接口函数来简化数据的交换,它能够存取JVM内部的数据库,提供从一种语言环境的数据类型到需要转换的语言环境的数据类型映射。
JNI也提供一些其他的支持结构以便于C和C++程序调用这些接口函数。不幸的是这些支持机制不能被汇编语言程序直接使用。所以汇编语言开发人员需要理解如何直接访问这些接口函数,要达到这个水平必须对JNI的结构有个全面的认识。JNI 结构
当一个Java程序调用本地方法的时候,被调用的方法会被强制接收两个附加在调用方法上的参数。第一个参事是JNIEnv指针,第二个是指向调用者的对象或类的参考引用。 这些是进入JNI世界的钥匙。
JNIEnv是一个指针, 它的值指向另一个指针。这第二个指针指向了一个函数表,它实际上是一个指针数值。在函数表里面的每个指针指向一个JNI接口函数。为了调用一个接口函数,我们必须在函数表里面得到正确的存放位置。让我们看看如何通过两个步骤得到这个值:
首先我们要找到第二个指针的值,换句话说,我们要得到JNIEnv指向的位置里面的内容,我们可以用下面的代码做到:
第一个指令把JNIEnv的内容放入ebx寄存器,下面一句把ebx这个地址指向位置存放的值放入eax。因为ebx指向的内容和JNIEnv相同,eax现在有了JNIEnv指向的位置的值,这意味着现在eax有了函数表的起始地址。mov ebx, JNIEnv mov eax, [ebx]
下一步我们需要从函数表的项目中取出指向我们想调用的接口函数的值。为了做到这一点,我们必须把函数索引乘以4(参见Sheng Liang的书)— 因为每个指针是四字节长度 –- 然后把结果加上我们在前面存在eax里面的函数表起始地址。下面是实现代码:
eax寄存器里面的内容现在可以用来调用函数了。mov ebx, eax ; save pointer to function table mov eax, index ; move the value of index into eax mov ecx, 4 mul ecx ; multiply index by 4 add ebx, eax ; ebx points to the desired entry mov eax, [ebx] ; eax points to the desired function
图1显示了存取JNI接口的过程。

图1:存取JNI函数一个实例
为了演示如何运用上面的技术调用一个汇编语言程序,让我们做一个简单的程序。在我们的例子里面,一个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程序调用本地代码,而且也是开发其他类型的相互作用功能的基础。