java调用dll方法详解

前言:

初学java调用dll库时,经常出现无法加载库、找不到方法等错误(UnsatisfiedLinkError等)。本文对常见的问题进行详细的分析,给出较为完整的解决方案。

正文:

在java中写一个native方法,实现对dll的调用,一般过程如下:

public class Native {
	static native void say(String src);
	static{
		System.loadLibrary("libname");
	}
}

然后,用javah 命令生成头文件,得到:

/* DO NOT EDIT THIS FILE - it is machine generated */
#include 
/* Header for class Native */

#ifndef _Included_Native
#define _Included_Native
#ifdef __cplusplus
extern "C" {
#endif
/*
 * Class:     Native
 * Method:    say
 * Signature: (Ljava/lang/String;)V
 */
JNIEXPORT void JNICALL Java_Native_say
  (JNIEnv *, jclass, jstring);

#ifdef __cplusplus
}
#endif
#endif

创建C项目(Eclipse平台),将上述的头文件拷贝过来,再写一个.c或者.cpp的文件来实现原生方法

#include "native.h"
JNIEXPORT void JNICALL Java_Native_say(JNIEnv* env, jclass clz, jstring src){
	//....
}

build,得到dll文件,改成name.dll,放到java项目下即可。

经过以上方法,运气好的话,可能一次就成了。但也常见以下错误,我将其分为3类:

A类错误,找不到/无法加载dll文件:

0.java.lang.UnsatisfiedLinkError: no XXX  in java.library.path

1.java.lang.UnsatisfiedLinkError: Can't load library

2.UnsatisfiedLinkError: XXXXXXX 不是有效的 Win32 应用程序。

B类错误,在所有成功加载的dll文件中,找不到要调用的方法:

0.java.lang.UnsatisfiedLinkError:  Native.say(Ljava/lang/String;)V

C类错误,C.C/C++编译器找不到/无法解析jni.h头文件:

0.#include 这里提示找不到头文件,以及方法声明那里提示语法错误。

D类错误,jvm版本与dll冲突:

0.ava.lang.UnsatisfiedLinkError:Can't load IA 32-bit .dll on a AMD 64-bit platform。这类错误好理解,就是32位jvm用了64位dll,或者反过来。也比较容易解决,通常换一个对应版本的dll就可以;或者换jdk版本,但比较麻烦。

以上4类问题,D类由于解决起来较为简单,不再讲解。

C类:C.C/C++编译器找不到/无法解析jni.h头文件

这类错误不属于我们代码编写问题,是编译平台的问题。只要能让编译平台知道去哪里找这个头文件就可以。eclipse的话,如果以mingw为C编译器,比较好的解决办法就是,把jdk目录下,include/jni.h和include/win32/jni_md.h放到mingw文件夹下,mingw版本号那个文件夹里的include中,比如我的目录是:D:\Cbianyi\mingw64\x86_64-w64-mingw32\include。如果是用VS6,可以在下图的位置添加jdk的上述目录:

java调用dll方法详解_第1张图片

如果是VS2017,设置include目录即可。个人推荐写C代码还是用VS的好,VS6也比eclipse要方便。

A类:dll寻找/加载错误

我们load一个dll,实际包括寻找和加载两个过程,其中寻找dll这一过程是由java层完成的;加载是jvm完成的。

A.0和A.1 寻找dll:一般都是出在"找"这个环节。

寻找-加载dll有两种方法:常见的方法是System.loadLibrary("libname"),参数为dll文件名,不带后缀(下文简称sys.loadlib)。错误的根源一般在此方法。另外一种是用System.load("filename"),参数为dll的绝对路径,带后缀(下文简称sys.load)。如果用这种方法,基本不会错,除非路径写错了或者dll文件自身有问题。这两个方法实际上最终都要通过绝对路径来寻找dll,只不过对于sys.loadlib,java会将这个libname补全为filename,再去找。我们看一下源码。

这两个方法交汇于ClassLoader. loadLibrary(Class fromClass, String name,boolean isAbsolute)(下文简称loader.load)。当使用sys.load方法时,传到此方法的isAbsolute参数为true,有如下源码:

static void loadLibrary(Class fromClass, String name,
                            boolean isAbsolute) {
      //...
        if (isAbsolute) {
            if (loadLibrary0(fromClass, new File(name))) {
                return;
            }
            throw new UnsatisfiedLinkError("Can't load library: " + name);
        }
        //....
    }

这说明,对于给出绝对路径的sys.load,java将直接去找这个路径,如果没有,就给出UnsatisfiedLinkError("Can't load library: " + name)。

而如果用的是sys.loadlib,那么isAbsolute为false,要先补全绝对路径。补全的过程比较繁琐,与我们最直接相关的为:

 static void loadLibrary(Class fromClass, String name,
                            boolean isAbsolute) {
       
       //....
         for (int i = 0 ; i < sys_paths.length ; i++) {
            File libfile = new File(sys_paths[i], System.mapLibraryName(name));
            if (loadLibrary0(fromClass, libfile)) {
                return;
            }
            libfile = ClassLoaderHelper.mapAlternativeName(libfile);
            if (libfile != null && loadLibrary0(fromClass, libfile)) {
                return;
            }
        }
        if (loader != null) {
            for (int i = 0 ; i < usr_paths.length ; i++) {
                File libfile = new File(usr_paths[i],
                                        System.mapLibraryName(name));
                if (loadLibrary0(fromClass, libfile)) {
                    return;
                }
                libfile = ClassLoaderHelper.mapAlternativeName(libfile);
                if (libfile != null && loadLibrary0(fromClass, libfile)) {
                    return;
                }
            }
        }
        throw new UnsatisfiedLinkError("no " + name + " in java.library.path");
       //....
    }

也就是说,补全的路径为sys_path和use_paths这两个数组里的内容。如果找不到的话,就会提示 UnsatisfiedLinkError("no " + name + " in java.library.path")这个错误。如果我们不做其他设置,这两个路径分别代表了jre和系统环境变量。显而易见,如果调用这些系统外的dll,可以把写好的dll放到这些环境变量中。或者,可以指定一个java.library.path目录。当有指定这一目录时,上述代码中usr_path将仅有一个该目录,而不是系统环境变量。指定的方式有很多种。但是把自己写的dll放到环境变量(或者设置成环境变量),以及设置 libpath的方法,随着项目的移动、变更和发布,仍然会发生找不到dll的情况。

我给出的解决方案为,不使用sys.loadlib的方法,而是直接给出“绝对路径”。同样我们不能写死一个路径。实际上,load一个dll和inputstream一个file,十分类似,都是先去找文件,再读到内存中。因此,把dll文件看成一个普通的资源文件即可,相信一个程序员可以有多种方式定位一个项目下的文件,无论项目怎么移动和发布。我用的方法是,先得到类所在的路径,然后根据类路径和dll路径的相对关系拼接的。这样一来,无论怎么移动项目,发布,都不会有问题。如下,我将要加载的B32.dll,放在与Native同级的lib目录下:

		String dir=Native.class.getResource("").getPath().substring(1);
		String s1=dir+"lib/B32.dll";
		System.load(s1);

如此,可以保证找到dll。

A.2加载dll

无论用相对还是绝对路径去寻找dll,当找到这个dll时,jvm将通过Windows系统的LoadLibrary(下文简称win.load)试图将dll加载到内存中。这一部分脱离java层。为证实这一观点,需要一些C的基础知识。我们用ollydbg做一下调试:

java调用dll方法详解_第2张图片

bp LoadLibraryA加断点后,放开几次后看到:

这个FileName正是要加载的dll。

然后在ret处断点,得到B32在内存中的映射位置:

转到0x8B00000看内存,

可以确定已经成功将B32.dll映射到内存。

进一步的,我们可以证明,只要一个文件的名称为B32.dll,jvm就会调用win.load方法,试图加载。新建一个txt空文件,改名B32.dll,仍然按上述方法调试,还是可以得到:

但是,ret将返回0,因为这不是一个有效/合法的dll文件,程序也将报错:

可以看到,报错提示的是,不是有效的Win32程序。所以我讲“不是有效/合法的dll文件”是不严谨的。并非一定要用dll作为load的对象;.exe也可以。如果把B32.dll改名为B32.exe,源码System.load给出的路径的后缀名也改为.exe,一样是可以成功调用的。

额外的,我们再验证一下System.loadlibrary("B32")这种通过文件名加载的方式,也会补全绝对路径这一论断。在源码中就这一句,没有给出绝对路径。也不做其他设置。

调试参数如下:

java调用dll方法详解_第3张图片

图中参数具体为:-Djava.library.path="F:\java32\jdk1.8.0_181\bin\lib" Main,也就是指定java.library.path。

同样可以看到最终是以绝对路径的方式加载。

B32映射地址:

但是使用sys.loadlib,系统自动补全的后缀为dll,此时在用.exe,会在寻找的时候就出错,不会再有后边加载这一过程了。

A类问题总结:java先根据给出的绝对路径,或补足的路径,去寻找是否有这个文件;如果有,再由jvm试图去加载这个文件,此时如果该文件为合法的dll(win32)文件,加载过程才算成功。当文件不存在,或者不是合法的dll(win32)文件,都会出现错误。

B类错误,在所有成功加载的dll文件中,找不到要调用的方法:

java在调用一个native方法的时候,会去所有已成功加载的dll的导出名称表里寻找与之匹配的方法名,如果照的到,就可以调用。找不到,就会报此类问题。当然,jvm运行的时候不可能每次调用都要这样找一次 ,而是有一套处理办法,看资料是通过一个地址映射表完成。这部分太底层,还没研究懂。我们来看与B类错误关系最为紧密的:只要在dll的导出名称表中找的到java调用的那个方法的名称,就可以成功调用。这里有两个需要注意和验证的地方:

第一,在导出名称表中找得到,不是单纯的指这个方法存在于导出名称表中,还包括,可以被系统找到。这一点,我在第一篇文章里有解释:dll导出名称表的查找策略。这个问题也是在搞这个java发现的,之前也不知道。

第二,调用时,仅凭方法名来比对,与方法的参数、返回类型等等都无关。比如说,我在java中要调用的是一个无参无返回值类型的方法void say(),而dll中导出的是一个3个参数,int返回类型的int say(arg1,arg2,arg3),同样可以实现调用。

第三,成功调用,但不保证方法成功执行。

以下我们来验证一下:

java层不变,我们加载B32.dll,试图调用native void say(String s)方法。这个方法经过头文件编译后,得到:

extern "C" JNIEXPORT void JNICALL Java_Native_say (JNIEnv *, jclass, jstring)。也就是说,在B32.dll里边要找Java_Native_say方法。extern "C"、JNIEXPORT以及JNICALL为C语言基础知识,不了解的可以自己查一下。

我们先做“常规”的流程。在C中方法:

extern "C" JNIEXPORT void JNICALL Java_Native_say(JNIEnv *env, jclass cla, jstring s){

       puts("VS2017");

       printf("%d\n", s);

       printf("%X\n", &s);

       const char* temp = env->GetStringUTFChars(s, 0);

       puts(temp);

}

编译好Dll后,查看PE结构,得到整个方法的内存地址:

dbg调试,按上文调试的方法,找到方法的入口,下断点,单步运行。可见,程序的确跑到这里,5个call对应C源码中的puts、printf、printf、env->GetStringUTFChars以及puts,除第四个cal是字符串转换外,其他四个每次执行完,都会得到一条对应的输出。

这说明,java调用dll的方法,在内存中,是要跑到对应的方法内存地址的。

基于此,我们来验证“调用时,仅凭方法名来比对,与方法的参数、返回类型等等都无关”。

java层不变,我们把原来的B32.dll移走。在系统目录windows下,随便找一个dll。

java调用dll方法详解_第4张图片

粘贴到我们的lib目录中,改名B32.dll。

然后把这个dll导出名称表的一个函数名称改为Java_Native_say。鉴于dll名称表的查找策略,这里最好直接改名称表的第一项,用十六进制编辑器来改。

java调用dll方法详解_第5张图片

原来第一项的名称为“AccConvertAccessMaskToActrlAccess”。然后,再查一下函数地址:

java调用dll方法详解_第6张图片

好了,开始dbg调试。得到这个伪"B32.dll"的映像地址,加上RVA,下断点:

java调用dll方法详解_第7张图片

说明的确是进入到这方法了。然后放开继续运行的话,是有可能出错的。我们这里改的这个就报错了。当然有时候也不会出错,即便方法的修饰符不一致。

好了,现在可以确定,java调用dll中的方法,和C程序调用dll方法,基本一致的,都是在dll导出表中寻找,并且是以C的方式完成的。

那正常开发,根据自动生成的.h文件,直接copy方法声明,然后再实现生成dll,为什么会找不到呢?

其实,严格按照我上边给出的方法,如果编译32位的dll,的确是找不到的。对于extern "C" JNIEXPORT void JNICALL Java_Native_say (JNIEnv *, jclass, jstring)这种方法定义,无论用eclipse,还是VS6,VS2017,导出表的名称将会是_Java_Native_say@12,而不会得到Java_Native_say。我是编译好后,改的16进制数据,如同上边做验证那样,把方法名改了。这种方式明显不行,如果导出方法很多,一个一个改岂不是很费劲。eclipse编辑C使用mingw,如果是32位的,是有参数可以修改,直接生成无附加符号的方法;VS的话,我还没找到方法。如果是64位平台,无论eclipse还是VS,就会直接生成“Java_Native_say”。

另外,再多说点。在编写native源码的时候,如果不在头文件声明,也不在.cpp/c中声明,而是仅有定义,那么extern "C" JNIEXPORT void JNICALL Java_Native_say (JNIEnv *, jclass, jstring)这行必须保证完整,一个词不能少;如果是先声明,再定义,生命时必须保证完整,定义时,extern "C" JNIEXPORT都可以省略,只写 void JNICALL Java_Native_say (JNIEnv *, jclass, jstring)即可。

最后,理解上述AB问题的内容后,我们可以推测,也可以验证:

1.System.load/loadLibrary不一定要写在声明native方法的那个类中,也非必须写在static{}里。在代码的任何地方,只要是调用native方法前调用sys.load即可。只不过放在native类的static{}中,可以保证不会漏掉加载过程。

2.可以load多个dll;如果这些dll中有同名方法,调用时,将调用第一次load成功的那个dll里的该方法。

总结一下要点:

第一:准确的给出路径,保证java能够找到dll文件;合法的dll,让jvm能够成功的加载dll。

第二:准确的dll导出名称表的方法名称,让jvm可以找到native方法所在的内存地址,完成调用。

你可能感兴趣的:(java调用dll方法详解)