1. 程式人生 > >Java通過JNI呼叫VC的DLL總結

Java通過JNI呼叫VC的DLL總結

Java下有時一些需要效率的操作要用C++來完成,呼叫C/C++的庫一般有兩種方式,JNI和JNA。自己學習JNI時也遇到不少坑,這裡總結一下JNI的使用過程。
建立Java Project專案:
Java Project專案
建立Java類檔案,填入包名和類名:
填入包名和類名
寫入如下程式碼:
CdesDll 程式碼
如果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;:
CLASSPATH環境變數


這裡的環境變數不區分大小寫,注意到第一個路徑為“.”即當前路徑,那麼使用javah命令時要進入java類檔案的當前路徑,由於這裡的CdesDll類是帶有包名的(com.des.jni),所以要進入com資料夾所在路徑(E:\workspace\CDESDLL\bin):
包路徑
其實.java和.class檔案都可以識別的,因此換為com\des\jni\CdesDll.java所在的src路徑一樣:
原始碼路徑
使用命令列進入專案的bin目錄,輸入命令:

javah -jni com.des.jni.CdesDll

如果沒設定CLASSPATH環境變數,需要指定-classpath引數,同樣使用命令列進入專案的bin目錄,輸入命令:

javah -classpath . -jni com.des.jni.CdesDll  (注意“.”兩邊各有一個空格)

說明
-classpath <路徑>用於裝入類的路徑
-jni 生成JNI樣式的標頭檔案(預設)
我這裡使用的是JDK1.8,結果就報錯了:
JDK1.8報錯
看來JDK1.8不支援生成JNI的.h檔案了,換成JDK1.7試試,輸入如下命令:

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

JDK1.7命令
由於懶得改環境變數,這裡直接使用JDK1.7的javah命令所在的全路徑,為什麼路徑加上引號呢,因為這個路徑是有空格的,直接使用會因為把空格前斷開處理而報錯,這裡使用一個小技巧。這下在src目錄下生成了想要的com_des_jni_CdesDll.h檔案:
標頭檔案生成


com_des_jni_CdesDll.h檔案內容如下:
標頭檔案內容
這裡為什麼用src路徑下的原java檔案生成呢,雖然上面說了兩個檔案都可以使用,但因為剛才bin目錄下的.class檔案是用JDK1.8編譯的,如果使用的話會出現編譯器版本更新的警告,雖然不影響.h檔案生成,為穩妥起見,還是用原java檔案吧。

開啟VS2010,建立一個Win32專案:
Win32專案
應用程式的型別選擇DLL,這裡不用勾選“匯出符號”,com_des_jni_CdesDll.h檔案已經為我們寫好了,之後加進專案來就可以了:
DLL設定
生成專案後,不僅要把com_des_jni_CdesDll.h檔案新增進來,還得把JDK1.7的include目錄下jni.h檔案:
jni.h檔案
和win32目錄下的jni_md.h檔案新增進專案:
專案結構
CdesDll.cpp程式碼中加入標頭檔案:

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

由於jni_md.h檔案在jni.h檔案中已經包含,不需要單獨添加了:
jni_md.h檔案
編譯時生成的com_des_jni_CdesDll.h檔案可能報錯:
標頭檔案報錯
這句改成#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平臺:
x64平臺
生成的dll要放在Eclipse工程的根目錄下,否則載入庫會失敗:
工程根目錄
我測試時很不幸,呼叫時出現如下錯誤:

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檔案,看看函式是否正確匯出了:
分析庫函式
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”的宣告

函式重複定義了?可是我就定義了一個啊。再仔細檢查標頭檔案和原始檔:
標頭檔案定義
原始檔實現
發現二者的區別了吧,原來函式引數不同啊,靜態方法才用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