Java通过JNI调用VC的DLL总结

Java下有时一些需要效率的操作要用C++来完成,调用C/C++的库一般有两种方式,JNI和JNA。自己学习JNI时也遇到不少坑,这里总结一下JNI的使用过程。
建立Java Project项目:
Java通过JNI调用VC的DLL总结
建立Java类文件,填入包名和类名:
Java通过JNI调用VC的DLL总结
写入如下代码:
Java通过JNI调用VC的DLL总结
如果Eclipse设置自动编译的话,现在在项目bin目录下应该生成了CdesDll.class文件,接下来使用javah命令生成C++需要的.h文件,也就是给C++生成接口。
原来自己写头文件,但JNI要求的函数命名规则是比较严格的,写错一点就调用不成功,期间也走了不少弯路。现在Java有javah命令为我们自动生成头文件了,既然有现成工具,最好就别自己写了。
在使用javah命令前需要注意的是javah查找的是CLASSPATH设定的路径,如果这个路径下没有要处理的java类文件就会报错。如果CLASSPATH环境变量已经设置为.;%JAVA_HOME%\lib\dt.jar;%JAVA_HOME%\lib\tools.jar;:
Java通过JNI调用VC的DLL总结
这里的环境变量不区分大小写,注意到第一个路径为“.”即当前路径,那么使用javah命令时要进入java类文件的当前路径,由于这里的CdesDll类是带有包名的(com.des.jni),所以要进入com文件夹所在路径(E:\workspace\CDESDLL\bin):
Java通过JNI调用VC的DLL总结
其实.java和.class文件都可以识别的,因此换为com\des\jni\CdesDll.java所在的src路径一样:
Java通过JNI调用VC的DLL总结
使用命令行进入项目的bin目录,输入命令:

javah -jni com.des.jni.CdesDll

如果没设置CLASSPATH环境变量,需要指定-classpath参数,同样使用命令行进入项目的bin目录,输入命令:

javah -classpath . -jni com.des.jni.CdesDll  (注意“.”两边各有一个空格)

说明
-classpath <路径>用于装入类的路径
-jni 生成JNI样式的头文件(默认)
我这里使用的是JDK1.8,结果就报错了:
Java通过JNI调用VC的DLL总结
看来JDK1.8不支持生成JNI的.h文件了,换成JDK1.7试试,输入如下命令:

"C:\Program Files\Java\jdk1.7.0_51\bin\javah" -jni com.des.jni.CdesDll

Java通过JNI调用VC的DLL总结
由于懒得改环境变量,这里直接使用JDK1.7的javah命令所在的全路径,为什么路径加上引号呢,因为这个路径是有空格的,直接使用会因为把空格前断开处理而报错,这里使用一个小技巧。这下在src目录下生成了想要的com_des_jni_CdesDll.h文件:
Java通过JNI调用VC的DLL总结
com_des_jni_CdesDll.h文件内容如下:
Java通过JNI调用VC的DLL总结
这里为什么用src路径下的原java文件生成呢,虽然上面说了两个文件都可以使用,但因为刚才bin目录下的.class文件是用JDK1.8编译的,如果使用的话会出现编译器版本更新的警告,虽然不影响.h文件生成,为稳妥起见,还是用原java文件吧。

打开VS2010,创建一个Win32项目:
Java通过JNI调用VC的DLL总结
应用程序的类型选择DLL,这里不用勾选“导出符号”,com_des_jni_CdesDll.h文件已经为我们写好了,之后加进项目来就可以了:
Java通过JNI调用VC的DLL总结
生成项目后,不仅要把com_des_jni_CdesDll.h文件添加进来,还得把JDK1.7的include目录下jni.h文件:
Java通过JNI调用VC的DLL总结
和win32目录下的jni_md.h文件添加进项目:
Java通过JNI调用VC的DLL总结
CdesDll.cpp代码中加入头文件:

#include "jni.h"
#include "com_des_jni_CdesDll.h"

由于jni_md.h文件在jni.h文件中已经包含,不需要单独添加了:
Java通过JNI调用VC的DLL总结
编译时生成的com_des_jni_CdesDll.h文件可能报错:
Java通过JNI调用VC的DLL总结
这句改成#include "jni.h"就好了。然后写与Java对应的那个testDll的实现函数,CdesDll.cpp添加如下代码:

JNIEXPORT jint JNICALL Java_com_des_jni_CdesDll_testDll(JNIEnv *, jclass, jint value)
{
	int res = value * 4;
	return res;
}

写了一个简单的测试函数,只是把输入的值扩大四倍,然后返回。写好了编译试一下,这里需要编译Release版本,而且与JDK1.7的位数要一致,这里是64位的,所以更改项目设置,新建一个x64平台:
Java通过JNI调用VC的DLL总结
生成的dll要放在Eclipse工程的根目录下,否则加载库会失败:
Java通过JNI调用VC的DLL总结
我测试时很不幸,调用时出现如下错误:

Exception in thread "main" java.lang.UnsatisfiedLinkError: com.des.jni.CdesDll.testDll(I)I
	at com.des.jni.CdesDll.testDll(Native Method)

一般出现这种错误是因为函数名不符合格式要求造成的,Java找不到对应函数,再次核对下函数名,是照着com_des_jni_CdesDll.h文件写的啊,这个头文件是由javah命令生成的,应该不会错。
使用CFF_Explorer工具查看一下生成的CdesDll.dll文件,看看函数是否正确导出了:
Java通过JNI调用VC的DLL总结
C++编译的函数名字会有些改变的,难道这样Java就不认了吗?改为C编译器试试:

#ifdef __cplusplus
extern "C" {
#endif
JNIEXPORT jint JNICALL Java_com_des_jni_CdesDll_testDll(JNIEnv *, jclass, jint value)
{
	int res = value * 4;
	return res;
}
#ifdef __cplusplus
}
#endif

再编译这下dll也报错了:

CdesDll.cpp(12): error C2733: 不允许重载函数“Java_com_des_jni_CdesDll_testDll”的第二个 C 链接
1> CdesDll.cpp(11) : 参见“Java_com_des_jni_CdesDll_testDll”的声明

函数重复定义了?可是我就定义了一个啊。再仔细检查头文件和源文件:
Java通过JNI调用VC的DLL总结
Java通过JNI调用VC的DLL总结
发现二者的区别了吧,原来函数参数不同啊,静态方法才用jclass类型了,赶紧改为jobject吧。这次编译没错了,Java调用也正常了。那为什么刚才用C++编译的时候就不报错呢,熟悉C++的朋友应该都知道,C++有个特性叫重载(允许函数名相同而只是参数不同),当然这在C里面是不允许的。所以C++编译没报错,但是头文件的那个函数却没有对应的实现代码,Java调用它不报错就怪了。

上面写的测试函数只是简单传参int类型,实际项目中很可能传输其他更复杂类型的参数,只要找到JNI里面的对应类型即可。比如要向dll传输char数组,Java里可别直接写char[],因为Java的char是为Unicode编码考虑的,也就是说是两个字节,而非C/C++里的char,Java里要用byte[]。Javah命令会把byte[]转为jbyteArray,C或C++里要想转为char*得用到下面的转换:

/* jbyteArray dataArr 传来的参数 */
jbyte *dataBytes = env->GetByteArrayElements(dataArr, 0);
int iDataSize = env->GetArrayLength(dataArr);

unsigned char *cData= new BYTE[iDataSize+1];
memset(cData, 0, iDataSize+1);
memcpy(cData, dataBytes, iDataSize);

那要是想把char*转会jbyteArray返回去该怎么办呢?

/* 继续上面代码 */
jbyte* jbp = (jbyte*)cData;
jbyteArray resArr = env->NewByteArray(iDataSize);
env->SetByteArrayRegion(resArr, 0, iDataSize, jbp);

delete []cData;
return resArr;

详细的代码我已经上传CSDN了,如果还有不明白的地方请下载进行比对。资源下载地址:
https://download.csdn.net/download/xinxin_2011/10851026