前言:JNI簡介
Java Native Interface(JNI)是Java語言的本地編程接口,是J2SDK的一部分。在java程序中,我們可以通過JNI實現一些用java語言不便實現的功能。通常有以下幾種情況我們需要使用JNI來實現。
- 標準的java類庫沒有提供你的應用程序所需要的功能,通常這些功能是平臺相關的。
- 你希望使用一些已經有的類庫或者應用程序,而他們並非用java語言編寫的。
- 程序的某些部分對速度要求比較苛刻,你選擇用匯編或者c語言來實現並在java語言中調用他們。
當然有時我們也會需要在別的語言項目中調用java類庫。
在《java核心技術》中,作者提到JNI的時候,建議不到萬不得已不要使用JNI技術,一方面它需要你把握更多的知識纔可以駕馭;另一方面,使用了JNI後你的程序就會喪失可移植性。在本文我們跳過JNI的底層機制,讀者最好先把它想象爲本地代碼和java代碼的粘合劑。關係如下圖所示:
下面我們介紹幾種常見的JNI使用情景。
第一章:Windows下JAVA調用C/C++庫
我們使用的是Win7 64位系統,已經配置好了JDK和VS2010開發環境。
示例demo — http://download.csdn.net/detail/youmingyu/9702138
第一步、創建JAVA主程序
首先我們在硬盤上建立一個HelloJNIPro目錄作爲我們的工作目錄,然後編寫自己的java代碼,在java代碼中我們會聲明native方法,代碼非常簡單。如下所示
class HelloJNI
{
public native int displayHelloJNI(int a,int b); //聲明外部實現函數
static {
System.loadLibrary("win64/helloJNILib"); //導入本地庫
}
public static void main(String[] args) {
System.out.println(new HelloJNI().displayHelloJNI(1,2));
}
}
注意displayHelloJNI()
方法的聲明,它有一個關鍵字native,表明這個方法使用java以外的語言實現。這個方法不包括實現,因爲我們要用c/c++語言實現它。
注意System.loadLibrary("helloJNILib")
這句代碼,它是在靜態初始化塊中定義的,用來裝載helloJNILib共享庫,這就是我們在後面生成的helloJNILib.dll(假如在其他的操作系統可能是其他的形式,比如Linux下爲helloJNILib.so)
最後用命令行:javac HelloJNI.java
編譯生成HelloJNI.class文件,編譯最後執行也可以。
第二步、創建 .h 頭文件
這一步中我們要使用javah命令生成.h文件,這個文件要在後面的c/c++代碼中用到,運行命令行:javah HelloJNI
,可以看到在工作目錄下生成了HelloJNI.h文件,文件內容如下,在此我們不對它進行太多的解釋。
/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class HelloJNI */
#ifndef _Included_HelloJNI
#define _Included_HelloJNI
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class: HelloJNI
* Method: displayHelloJNI
* Signature: (II)I
*/
JNIEXPORT jint JNICALL Java_HelloJNI_displayHelloJNI
(JNIEnv *, jobject, jint, jint);
#ifdef __cplusplus
}
#endif
#endif
第三步、編寫C/C++本地實現代碼
在這一步我們要用C/C++語言實現java中定義的方法,在VS中新建一個項目:helloJNILib,然後創建HelloJNI.cpp文件,內容如下
#include <jni.h> //導入jni頭文件
#include "HelloJNI.h" //導入jni頭文件
#include <stdio.h>
JNIEXPORT jint JNICALL Java_HelloJNI_displayHelloJNI(JNIEnv *env, jobject obj,jint a,jint b){
printf("Hello JNI!\n");
int c=a+b;
return c;
}
注意這裏包含了jni.h和上一步得到的HelloJNI.h文件。因此你要在VS裏面設置好相關路徑,jni.h在JAVA_HOME/include裏面,方法如下。
編譯生成helloJNILib.dll文件,方法參考:VS2010下靜態鏈接庫和動態鏈接庫的生成和使用,注意生成和自己Windows位數一致的dll,我的是64位。
第四步、運行JAVA程序
把上一步生成的helloJNILib.dll文件複製到工作目錄下的win64文件夾下。命令行運行:java HelloJNI
命令,則可在控制檯看到如下輸出了。
第二章:Windows下VS(C++)調用JAVA類
我們使用的是Win7 64位系統,已經配置好了JDK和VS2010開發環境。
示例demo — http://download.csdn.net/detail/youmingyu/9761243
第一步、開發JAVA類
java類沒有特殊要求,下面給出一個測試用例:JNITest.java,然後新建一個JNIJavaTest文件夾,將此文件放入其中(因爲java文件設置包名爲JNIJavaTest)。然後編譯生成class文件。
package JNIJavaTest;
//該類是爲了演示JNI如何訪問各種對象屬性等
public class JNITest{
public static int COUNT = 8;//用於演示如何訪問靜態的基本類型屬性
private String msg;//演示對象型屬性
private int[] counts;
public JNITest(){
this("缺省構造函數");
}
//演示如何訪問構造器
public JNITest(String msg){
this.msg = msg;
this.counts = null;
}
public String getMessage(){
return msg;
}
// 該方法演示如何訪問一個靜態方法
public static String getHelloWorld(){
return "Hello world!";
}
//該方法演示參數的傳入傳出及中文字符的處理
public String append(String str, int i){
return str + i;
}
//演示數組對象的訪問
public int[] getCounts(){
return counts;
}
//演示如何構造一個數組對象
public void setCounts(int[] counts){
this.counts = counts;
}
//演示異常的捕捉
public void throwExcp()throws IllegalAccessException{
throw new IllegalAccessException("exception occur.");
}
}
需要注意的是:避免在被調用的JAVA類中使用靜態final成員變量,因爲在C++中生成一個JAVA類的對象時,靜態final成員變量不會像JAVA中new對象時那樣先賦值。如果出現這種情況,在C++中調用該對象的方法時會發現該對象的靜態final成員變量值全爲0或者null(根據成員變量的類型而定)。
第三步、創建VS項目,配置環境
用VS新建一個項目,進行如下配置:
- 設置包含目錄爲:
%JAVA_HOME%\include;%JAVA_HOME%\include\win32
,這是因爲需要用到jni.h和jni_md.h兩個頭文件(不設置也可以,請把這兩個文件copy到項目源文件處)。 - 設置庫目錄爲:
%JAVA_HOME%\lib
,並附加依賴項jvm.lib(經測試這一步可以不做)。 - 設置解決方案平臺同JDK一致,我們的JDK是64位,所以設置VS解決方案平臺是x64。
第三步、編寫C/C++代碼
下面以調用JNITest類中的append()函數爲例,給出一C++代碼實現例子。
#include "windows.h"
#include "jni.h"
#include <string>
#include <iostream>
using namespace std;
jstring NewJString(JNIEnv *env, LPCTSTR str);
string JStringToCString (JNIEnv *env, jstring str);
int main(){
//定義一個函數指針,下面用來指向JVM中的JNI_CreateJavaVM函數
typedef jint (WINAPI *PFunCreateJavaVM)(JavaVM **, void **, void *);
int res;
JavaVMInitArgs vm_args;
JavaVMOption options[3];
JavaVM *jvm;
JNIEnv *env;
/*------------------------------設置初始化參數-----------------------------------------*/
//disable JIT,這是JNI文檔中的解釋,具體意義不是很清楚 ,能取哪些值也不清楚。
//從JNI文檔裏給的示例代碼中搬過來的
options[0].optionString = "-Djava.compiler=NONE";
//設置classDir,如果程序用到了第三方的JAR包,也可以在這裏麪包含進來
options[1].optionString = "-Djava.class.path=..\\..;.";
//設置顯示消息的類型,取值有gc、class和jni,如果一次取多個的話值之間用逗號格開,如-verbose:gc,class
//該參數可以用來觀察C++調用JAVA的過程,設置該參數後,程序會在標準輸出設備上打印調用的相關信息
options[2].optionString = "-verbose:NONE";
//設置版本號,版本號有JNI_VERSION_1_1,JNI_VERSION_1_2和JNI_VERSION_1_4
//選擇一個根你安裝的JRE版本最近的版本號即可,不過你的JRE版本一定要等於或者高於指定的版本號
vm_args.version = JNI_VERSION_1_4;
vm_args.nOptions = 3;
vm_args.options = options;
//該參數指定是否忽略非標準的參數,如果填JNI_FLASE,當遇到非標準參數時,JNI_CreateJavaVM會返回JNI_ERR
vm_args.ignoreUnrecognized = JNI_TRUE;
/*------------------------------創建虛擬機-----------------------------------------*/
//加載JVM.DLL動態庫
HINSTANCE hInstance = LoadLibrary("C:\\StudyProgram\\Java\\jdk1.8.0_45\\jre\\bin\\server\\jvm.dll");
if (hInstance == NULL){
return false;
}
//取得裏面的JNI_CreateJavaVM函數指針
PFunCreateJavaVM funCreateJavaVM = (PFunCreateJavaVM)::GetProcAddress(hInstance, "JNI_CreateJavaVM");
//調用JNI_CreateJavaVM創建虛擬機
res = (*funCreateJavaVM)(&jvm, (void**)&env, &vm_args);
if (res < 0){
return -1;
}
/*------------------------------獲取java類的對象-----------------------------------------*/
//查找test.Demo類,返回JAVA類的CLASS對象
jclass cls = env->FindClass("test/Demo");
//根據類的CLASS對象獲取該類的實例
jobject obj = env->AllocObject(cls);
//獲取類中的方法,最後一個參數是方法的簽名,通過命令:" javap -s -p 文件名 "可以獲得
jmethodID mid = env->GetMethodID(cls, "append","(Ljava/lang/String;I)Ljava/lang/String;");
/*------------------------------調用對象中方法-----------------------------------------*/
//構造參數並調用對象的方法
const char szTest[] = "國窖";
jstring arg = NewJString(env, szTest);
jstring msg = (jstring) env->CallObjectMethod(obj, mid, arg, 1573);
cout<<JStringToCString(env, msg)<<endl;
/*------------------------------清理內存-----------------------------------------*/
//銷燬虛擬機並釋放動態庫
jvm->DestroyJavaVM();
::FreeLibrary(hInstance);
return 0;
}
//將JString轉化爲String
string JStringToCString (JNIEnv *env, jstring str){ //(jstring str, LPTSTR desc, int desc_len)
if(str==NULL){return "";}
//在VC中wchar_t是用來存儲寬字節字符(UNICODE)的數據類型
int len = env->GetStringLength(str);
wchar_t *w_buffer = new wchar_t[len+1];
char *c_buffer = new char[2*len+1];
ZeroMemory(w_buffer,(len+1)*sizeof(wchar_t));
//使用GetStringChars而不是GetStringUTFChars
const jchar * jcharString = env->GetStringChars(str, 0);
wcscpy(w_buffer,(const wchar_t *)jcharString);
env->ReleaseStringChars(str,jcharString);
ZeroMemory(c_buffer,(2*len+1)*sizeof(char));
//調用字符編碼轉換函數(Win32 API)將UNICODE轉爲ASCII編碼格式字符串
len = WideCharToMultiByte(CP_ACP,0,w_buffer,len,c_buffer,2*len,NULL,NULL);
string cstr = c_buffer;
delete[] w_buffer;
delete[] c_buffer;
return cstr;
}
//將Char數組轉化爲JString
jstring NewJString(JNIEnv *env, LPCTSTR str)
{
if(!env || !str){return 0;}
int slen = strlen(str);
jchar* buffer = new jchar[slen];
int len = MultiByteToWideChar(CP_ACP,0,str,strlen(str),(LPWSTR)buffer,slen);
if(len>0 && len < slen){
buffer[len]=0;
}
jstring js = env->NewString(buffer,len);
delete [] buffer;
return js;
}
c++代碼中調用外部java類中的成員流程大體如下(以append()函數爲例):
- 設置JavaVMInitArgs類型的參數,注意JavaVMInitArgs裏的options[1]這個參數,它指的是java類的路徑,我們設它爲classDir,那麼java類的位置就爲:項目工作目錄(當前目錄)+classDir+java包名路徑。
- 利用LoadLibrary()函數加載jvm.dll動態庫,注意jvm.dll一般位於
%JAVA_HOME%\jre\\bin\server\
路徑下,請不要挪動它的位置,因爲JNI_CreateJavaVM函數內部會自動根據jvm.dll的路徑來獲取JRE的環境,如果要挪動,請一起挪動整個虛擬機需要的環境(整個jre文件夾,其實裏面大部分東西沒用,挪動時可以刪除,具體參考下文的:補充四)。 - 利用GetProcAddress()函數獲得JNI_CreateJavaVM函數的指針。
- 利用JNI_CreateJavaVM函數創建虛擬機JavaVM,並得到JNI的上下文環境JNIEnv。
- 利用JNIEnv->FindClass()獲得java類,注意這裏的參數是java類的(包名+類名),間隔用斜槓”
/
“,不要用”.
“。 - 利用java類jclass獲取java類的對象。
- 利用java類對象jobject獲取Java裏相應函數,GetMethodID()函數最後一個參數是相應函數的簽名,可以通過命令:” javap -s -p 文件名 “獲得。
- 構造參數(如有必要),因爲java使用的參數類型和C++不一樣,有些不能強制轉型,比如Java用Unicode編碼字符,而C++不是。例子中給出了JString和CString相互轉化的方法。
- 利用CallObjectMethod調用java方法運算。
- 釋放內存。
補充一、java類其他成員的調用
1、調用JAVA中的靜態方法
//調用靜態方法
jclass cls = env->FindClass("test/Demo");
jmethodID mid = env->GetStaticMethodID(cls, "getHelloWorld","()Ljava/lang/String;");
jstring msg = (jstring)env->CallStaticObjectMethod(cls, mid);
cout<<JStringToCString(env, msg);
2、調用JAVA中的靜態屬性
//調用靜態方法
jclass cls = env->FindClass("test/Demo");
jfieldID fid = env->GetStaticFieldID(cls, "COUNT","I");
int count = (int)env->GetStaticIntField(cls, fid);
cout<<count<<endl;
3、調用JAVA中的帶參數構造函數
//調用構造函數
jclass cls = env->FindClass("test/Demo");
jmethodID mid = env->GetMethodID(cls,"<init>","(Ljava/lang/String;)V");
const char szTest[] = "電信";
jstring arg = NewJString(env, szTest);
jobject demo = env->NewObject(cls,mid,arg);
//驗證是否構造成功
mid = env->GetMethodID(cls, "getMessage","()Ljava/lang/String;");
jstring msg = (jstring)env->CallObjectMethod(demo, mid);
cout<<JStringToCString(env, msg);
4、傳入傳出數組
//傳入傳出數組
//構造數組
long arrayCpp[] = {1,3,5,7,9};
jintArray array = env->NewIntArray(5);
env->SetIntArrayRegion(array, 0, 5, arrayCpp);
//傳入數組
jclass cls = env->FindClass("test/Demo");
jobject obj = env->AllocObject(cls);
jmethodID mid = env->GetMethodID(cls,"setCounts","([I)V");
env->CallVoidMethod(obj, mid, array);
//獲取數組
mid = env->GetMethodID(cls,"getCounts","()[I");
jintArray msg = (jintArray)env->CallObjectMethod(obj, mid, array);
int len =env->GetArrayLength(msg);
jint* elems =env-> GetIntArrayElements(msg, 0);
for(int i=0; i< len; i++) {
cout<<"ELEMENT "<<i<<" IS "<<elems[i]<<endl;
}
env->ReleaseIntArrayElements(msg, elems, 0);
補充二、異常處理
由於調用了Java的方法,因此難免產生操作的異常信息,如JAVA函數返回的異常,或者調用JNI方法(如GetMethodID)時拋出的異常。這些異常沒有辦法通過C++本身的異常處理機制來捕捉到,但JNI可以通過一些函數來獲取Java中拋出的異常信息。
//異常處理
jclass cls = env->FindClass("test/Demo");
jobject obj = env->AllocObject(cls);
jmethodID mid = env->GetMethodID(cls,"throwExcp","()V");
env->CallVoidMethod(obj, mid);
//獲取異常信息
string exceptionInfo = "";
jthrowable excp = 0;
excp = env->ExceptionOccurred();
if(excp) {
jclass cls = env->GetObjectClass(excp);
env->ExceptionClear();
jmethodID mid = env->GetMethodID(cls, "toString","()Ljava/lang/String;");
jstring msg = (jstring) env->CallObjectMethod(excp, mid);
cout<<JStringToCString(env, msg)<<endl;
env->ExceptionClear();
}
補充三、多線程
有些時候需要使用多線程的方式來訪問Java的方法。我們知道一個Java虛擬機是非常消耗系統的內存資源,差不多每個虛擬機需要內存大約在20MB左右。爲了節省資源要求每個線程使用的是同一個虛擬機,這樣在整個的JNI程序中只需要初始化一個虛擬機就可以了。
這裏面涉及到兩個概念,它們分別是虛擬機(JavaVM *jvm)和虛擬機環境(JNIEnv *env)。真正消耗大量系統資源的是jvm而不是env,jvm是允許多個線程訪問的,但是env只能被創建它本身的線程所訪問,而且每個線程必須創建自己的虛擬機環境env。主線程在初始化虛擬機的時候就創建了虛擬機環境env,爲了讓子線程能夠創建自己的env,JNI提供了兩個函數:AttachCurrentThread和DetachCurrentThread。下面代碼就是子線程訪問Java方法的框架:
// 將虛擬機通過參數傳入
DWORD WINAPI SubThreadProc(PVOID dwParam){ //PVOID:無類型指針
JavaVM *g_jvm = (JavaVM*)dwParam;
JNIEnv* g_env;
(g_jvm)-> AttachCurrentThread((void**)&g_env, NULL);
//..............
(g_jvm)-> DetachCurrentThread();
return 0;
}
補充四、關於項目的發佈
當要發佈使用了JNI的程序時,並不一定要求客戶要安裝一個Java運行環境,因爲可以在安裝程序中打包這個運行環境。爲了讓打包程序利於下載,這個包要比較小,因此要去除JRE(Java運行環境)中一些不必要的文件。下面給出一些必須的文件(注意這些文件之間的相對路徑不要變):
- jre\bin\java.dll
- jre\bin\zip.dll
- jre\bin\server\jvm.dll
- jre\lib\rt.jar
除此之外,根據項目使用的java類的不同,還會需要特定的文件,比如如果項目中用到了日曆,你會需要 jre\lib\tzmappings,如果你用了字符,你會需要jre\lib\charsets.jar,請根據自己的情況簡化jre環境。
另外rt.jar這個包較大,但是其中有很大一部分文件並不需要,可以根據實際的應用情況進行刪除。例如程序如果沒有用到Java Swing,就可以把涉及到Swing的文件都刪除後重新打包。