前一阵子遇到的一个问题是,如何在Java程序中调用Windows DLL的函数。查了查资料,得知使用名为JNI(Java Native Interface)的方式可以让Java调用DLL中的函数。JNI的使用方法如下:

  1. 在Java中定义JNI调用,并撰写使用该JNI调用的相关代码。
  2. 根据定义好的JNI调用生成相应的C语言头文件。
  3. 利用上一步生成的头文件,使用C书写Windows平台上的代码。
  4. 编译C代码生成DLL库文件。
  5. 执行Java程序中JNI调用的代码。

我们可以看到,第三步使用C语言书写代码时,须遵从JNI的规范,这就为我们带来了一个问题:如果我们想使用的DLL文件是已经编译好的,它未必遵从JNI规范,怎么办?另一个问题是,在Java程序中需要定义JNI调用,但是我们显然不希望在业务逻辑代码中定义JNI调用。于是产生了下面的结构: #ref(2006/09/jni.png,nolink,JNI结构图)

最上方的Java Application是业务逻辑的实现部分,最下方的Windows DLL是我们拥有的DLL函数库,而中间虚线框的部分即为使用JNI进行开发的部分。

我们假设Java Application部分的代码如下。文件名为JavaApp.java。

package jnisample;

public class JavaApp {
    public static final void main(String args[]) {
        JNIWrapper.Hello();
    }
}

这里JNIWrapper类为我们即将进行开发的JNI Wrapper的类名。

我们先创建一个空的JNIWrapper.java以使得JavaApp.java能够正常编译。

package jnisample;

class JNIWrapper {
    static void Hello() {}
}

然后我们编译这两个文件:

javac -d . JNIWrapper.java
javac -d . JavaApp.java

编译完成之后我们将在jnisample目录下得到JNIWrapper.class和JavaApp.class两个文件。

我们再假设现有的DLL函数库的实现代码如下:(文件名HelloDll.c)

#include <windows.h>

__declspec(dllexport) BOOL __stdcall Hello();

BOOL __stdcall Hello() {
    MessageBox(NULL, "Helo, world!", "Hello", MB_OK | MB_ICONINFORMATION);
    return TRUE;
}

然后我们使用Microsoft Visual Studio 6.0自带的编译器 cl 进行编译:

cl HelloDll.c -MD -LD User32.lib

这样我们将得到HelloDll.dll文件。将该文件与jnisample目录放到同一层次上。

下面我们来看一下如何使用JNI在JavaApp.java中调用HelloDll.c中的Hello函数。首先我们将JNI Wrapper模块的JNIWrapper.java改成下面的样子:

package jnisample;

class JNIWrapper {
    static native void _dll_Hello();

    static {
        try {
            System.loadLibrary("DllWrapper");
        } catch (Exception e) {
            System.out.println("DllWrapper.dll load errer");
        }
    }

    static void Hello() {
        _dll_Hello();
    }
} 

然后再次进行编译:

javac -d . JNIWrapper.java

下一步利用J2SE提供的工具来生成必要的头文件。执行下列命令:

javah -jni jnisample.JNIWrapper

这样我们得到一个名为jnisample_JNIWrapper.h的文件。打开这个文件之后可以看到,里面定义了这样一个函数:

JNIEXPORT void JNICALL Java_jnisample_JNIWrapper__1dll_1Hello(JNIEnv *, jclass); 

这就是我们应当在DLL Wrapper模块中实现的函数。

创建DllWrapper.c文件,内容如下:

#include "jnisample_JNIWrapper.h"
#include <windows.h>

typedef BOOL (*FUNCHELLO)();

JNIEXPORT void JNICALL Java_jnisample_JNIWrapper__1dll_1Hello(JNIEnv * jenv, jclass jcls)
{
    HANDLE hDll;
    FUNCHELLO fnHello;

    hDll = LoadLibrary("HelloDll.dll");
    if (hDll) {
        fnHello = (FUNCHELLO)GetProcAddress(hDll, "_Hello@0");
        if (fnHello) fnHello();
        FreeLibrary(hDll);
    }
}

然后进行编译:

cl DllWrapper.c -I"C:\Program Files\java\jdk1.5.0_04\include" -I"C:\Program Files\Java\jdk1.5.0_04\include\win32" -MD -LD

生成DllWrapper.dll文件。到此为止我们的目录结构如下:

DllWrapper.dll
HelloDll.dll
jnisample/JNIWrapper.class
jnisample/JavaApp.class

执行以下命令即可看到执行结果了。

java jnisample.JavaApp

以上简单地阐述了如何利用JNI来调用平台相关的本地代码。在实际应用中还会出现很多问题,例如Java和C语言之间的数据类型转换等,这里就不一一详述了。

(示例程序下载) jnisample.zip