當前位置:才華齋>計算機>C語言>

如何在C/C++中呼叫Java

C語言 閱讀(5.77K)

java跨平臺的特性使Java越來越受開發人員的歡迎,但也往往會聽到不少的抱怨:用Java開發的圖形使用者視窗介面每次在啟動的時候都會跳出一個控制檯視窗,這個控制檯視窗讓本來非常棒的介面失色不少。怎麼能夠讓通過Java開發的GUI程式不彈出Java的控制檯視窗呢?下面是小編為大家帶來的關於如何在C/C++中呼叫Java的知識,歡迎閱讀。

如何在C/C++中呼叫Java
  如何在C/C++中呼叫Java

java跨平臺的特性使Java越來越受開發人員的歡迎,但也往往會聽到不少的抱怨:用Java開發的圖形使用者視窗介面每次在啟動的時候都會跳出一個控制檯視窗,這個控制檯視窗讓本來非常棒的介面失色不少。怎麼能夠讓通過Java開發的GUI程式不彈出Java的控制檯視窗呢?其實現在很多流行的開發環境例如JBuilder、Eclipse都是使用純Java開發的整合環境。這些整合環境啟動的時候並不會開啟一個命令視窗,因為它使用了JNI(Java Native Interface)的技術。通過這種技術,開發人員不一定要用命令列來啟動Java程式,可以通過編寫一個本地GUI程式直接啟動Java程式,這樣就可避免另外開啟一個命令視窗,讓開發的Java程式更加專業。

JNI答應執行在虛擬機器的Java程式能夠與其它語言(例如C和C++)編寫的程式或者類庫進行相互間的呼叫。同時JNI提供的一整套的API,答應將Java虛擬機器直接嵌入到本地的應用程式中。圖1是Sun站點上對JNI的基本結構的描述。

本文將介紹如何在C/C++中呼叫Java方法,並結合可能涉及到的問題介紹整個開發的步驟及可能碰到的難題和解決方法。本文所採用的工具是Sun公司建立的 Java Development Kit (JDK) 版本 1.3.1,以及微軟公司的Visual C++ 6開發環境。

  環境搭建

為了讓本文以下部分的程式碼能夠正常工作,我們必須建立一個完整的開發環境。首先需要下載並安裝JDK 1.3.1,其下載地址為“”。假設安裝路徑為C:JDK。下一步就是設定整合開發環境,通過Visual C++ 6的選單Tools→Options開啟選項對話方塊如圖2。

將目錄C:JDKinclude和C:JDKincludewin32加入到開發環境的Include Files目錄中,同時將C:JDKlib目錄新增到開發環境的Library Files目錄中。這三個目錄是JNI定義的一些常量、結構及方法的標頭檔案和庫檔案。整合開發環境已經設定完畢,同時為了執行程式需要把Java虛擬機器所用到的動態連結庫所在的目錄C:JDK jreinclassic設定到系統的Path環境變數中。這裡需要提出的是,某些開發人員為了方便直接將JRE所用到的DLL檔案直接拷貝到系統目錄下。這樣做是不行的,將導致初始化Java虛擬機器環境失敗(返回值-1),原因是Java虛擬機器是以相對路徑來尋找所用到的庫檔案和其它一些相關檔案的。至此整個JNI的開發環境設定完畢,為了讓此次JNI旅程能夠順利進行,還必須先預備一個Java類。在這個類中將用到Java中幾乎所有有代表性的屬性及方法,如靜態方法與屬性、陣列、異常丟擲與捕捉等。我們定義的Java程式()如下,本文中所有的程式碼演示都將基於該Java程式,程式碼如下:

package ; /** * 該類是為了演示JNI如何訪問各種物件屬性等 * @author liudong */ public class Demo { //用於演示如何訪問靜態的基本型別屬性 public static int COUNT = 8; //演示物件型屬性 public String msg; PRivate int[] counts; public Demo() { this("預設建構函式"); } /** * 演示如何訪問構造器 */ public Demo(String msg) { tln(":" + msg); = msg; ts = null; } /** * 該方法演示如何訪問一個訪問以及中文字元的處理 */ public String getMessage() { return msg; } /** * 演示陣列物件的訪問 */ public int[] getCounts() { return counts; } /** * 演示如何構造一個數組物件 */ public void setCounts(int[] counts) { ts = counts; } /** * 演示異常的捕捉 */ public void throwExcp() throws IllegalaccessException { throw new IllegalAccessException("exception occur."); } }

  初始化虛擬機器

原生代碼在呼叫Java方法之前必須先載入Java虛擬機器,而後所有的Java程式都在虛擬機器中執行。為了初始化Java虛擬機器,JNI提供了一系列的介面函式Invocation API。通過這些API可以很方便地將虛擬機器載入到記憶體中。建立虛擬機器可以用函式 jint JNI_CreateJavaVM(JavaVM **pvm, void **penv, void *args)。對於這個函式有一點需要注重的是,在JDK 1.1中第三個引數總是指向一個結構JDK1_ 1InitArgs, 這個結構無法完全在所有版本的虛擬機器中進行無縫移植。在JDK 1.2中已經使用了一個標準的初始化結構JavaVMInitArgs來替代JDK1_1InitArgs。下面我們分別給出兩種不同版本的示例程式碼。

  在JDK 1.1初始化虛擬機器:

#include int main() { JNIEnv *env; JavaVM *jvm; JDK1_1InitArgs vm_args; jint res; /* IMPORTANT: 版本號設定一定不能漏 */ vm_ion = 0x00010001; /*獲取預設的虛擬機器初始化引數*/ JNI_GetDefaultJavaVMInitArgs(&vm_args); /* 新增自定義的類路徑 */ sprintf(classpath, "%s%c%s", vm_spath, PATH_SEPARATOR, USER_CLASSPATH); vm_spath = classpath; /*設定一些其他的初始化引數*/ /* 建立虛擬機器 */ res = JNI_CreateJavaVM(&jvm,&env,&vm_args); if (res < 0) { fprintf(stderr, "Can't create Java VM "); exit(1); } /*釋放虛擬機器資源*/ (*jvm)->DestroyJavaVM(jvm); }

  JDK 1.2初始化虛擬機器:

/* invoke2.c */ #include int main() { int res; JavaVM *jvm; JNIEnv *env; JavaVMInitArgs vm_args; JavaVMOption options[3]; vm_ion=JNI_VERSION_1_2;//這個欄位必須設定為該值 /*設定初始化引數*/ options[0]onString = "iler=NONE"; options[1]onString = "=."; options[2]onString = "-verbose:jni";//用於跟蹤執行時的資訊 /*版本號設定不能漏*/ vm_ion = JNI_VERSION_1_2; vm_ions = 3; vm_ons = options; vm_reUnrecognized = JNI_TRUE; res = JNI_CreateJavaVM(&jvm, (void**)&env, &vm_args); if (res < 0) { fprintf(stderr, "Can't create Java VM "); exit(1); } (*jvm)->DestroyJavaVM(jvm); fprintf(stdout, "Java VM destory. "); }

為了保證JNI程式碼的可移植性,建議使用JDK 1.2的方法來建立虛擬機器。JNI_CreateJavaVM函式的第二個引數JNIEnv *env,就是貫穿整個JNI始末的一個引數,因為幾乎所有的函式都要求一個引數就是JNIEnv *env。

  訪問類方法

初始化了Java虛擬機器後,就可以開始呼叫Java的方法。要呼叫一個Java物件的方法必須經過幾個步驟:

  1.獲取指定物件的類定義(jclass)

有兩種途徑來獲取物件的類定義:第一種是在已知類名的情況下使用FindClass來查詢對應的類。但是要注重類名並不同於平時寫的Java程式碼,例如要得到類的定義必須呼叫如下程式碼:

jclass cls = (*env)->FindClass(env, "jni/test/Demo");//把點號換成斜槓

然後通過物件直接得到其所對應的類定義:

jclass cls = (*env)-> GetObjectClass(env, obj); //其中obj是要引用的物件,型別是jobject

  2.讀取要呼叫方法的定義(jmethodID)

我們先來看看JNI中獲取方法定義的函式:

jmethodID (JNICALL *GetMethodID)(JNIEnv *env, jclass clazz, const char *name, const char *sig); jmethodID (JNICALL *GetStaticMethodID)(JNIEnv *env, jclass class, const char *name, const char *sig);

這兩個函式的區別在於GetStaticMethodID是用來獲取靜態方法的定義,GetMethodID則是獲取非靜態的方法定義。這兩個函式都需要提供四個引數:env就是初始化虛擬機器得到的JNI環境;第二個引數class是物件的類定義,也就是第一步得到的obj;第三個引數是方法名稱;最重要的是第四個引數,這個引數是方法的定義。因為我們知道Java中答應方法的多型,僅僅是通過方法名並沒有辦法定位到一個具體的方法,因此需要第四個引數來指定方法的具體定義。但是怎麼利用一個字串來表示方法的具體定義呢?JDK中已經預備好一個反編譯工具javap,通過這個工具就可以得到類中每個屬性、方法的定義。下面就來看看的定義:

開啟命令列視窗並執行 javap -s -p 得到執行結果如下:

Compiled from public class extends ct { public static int COUNT; /* I */ public ng msg; /* Ljava/lang/String; */ private int counts[]; /* [I */ public (); /* ()V */ public (ng); /* (Ljava/lang/String;)V */ public ng getMessage(); /* ()Ljava/lang/String; */ public int getCounts()[]; /* ()[I */ public void setCounts(int[]); /* ([I)V */ public void throwExcp() throws galAccessException; /* ()V */ static {}; /* ()V */ }

我們看到類中每個屬性和方法下面都有一段註釋。註釋中不包含空格的內容就是第四個引數要填的內容(關於javap具體引數請查詢JDK的使用幫助)。下面這段程式碼演示如何訪問的getMessage方法:

/* 假設我們已經有一個的例項obj */ jmethodID mid; jclass cls = (*env)-> GetObjectClass (env, obj);//獲取例項的類定義 mid=(*env)->GetMethodID(env,cls,"getMessage"," ()Ljava/lang/String; "); /*假如mid為0表示獲取方法定義失敗*/ jstring msg = (*env)-> CallObjectMethod(env, obj, mid); /* 假如該方法是靜態的方法那隻需要將最後一句程式碼改為以下寫法即可: jstring msg = (*env)-> CallStaticObjectMethod(env, cls, mid); */

  3.呼叫方法

為了呼叫物件的某個方法,可以使用函式CallMethod或者CallStaticMethod(訪問類的靜態方法),根據不同的返回型別而定。這些方法都是使用可變引數的定義,假如訪問某個方法需要引數時,只需要把所有引數按照順序填寫到方法中就可以。在講到建構函式的訪問時,將演示如何訪問帶引數的建構函式。

  訪問類屬性

訪問類的屬性與訪問類的方法大體上是一致的,只不過是把方法變成屬性而已。

  1.獲取指定物件的類(jclass)

這一步與訪問類方法的第一步完全相同,具體使用參看訪問類方法的第一步。

  2.讀取類屬性的定義(jfieldID)

在JNI中是這樣定義獲取類屬性的方法的:

jfieldID (JNICALL *GetFieldID) (JNIEnv *env, jclass clazz, const char *name, const char *sig); jfieldID (JNICALL *GetStaticFieldID) (JNIEnv *env, jclass clazz, const char *name, const char *sig);

這兩個函式中第一個引數為JNI環境;clazz為類的定義;name為屬性名稱;第四個引數同樣是為了表達屬性的型別。前面我們使用javap工具獲取類的具體定義的時候有這樣兩行:

public ng msg; /* Ljava/lang/String; */

其中第二行註釋的內容就是第四個引數要填的資訊,這跟訪問類方法時是相同的。

  3.讀取和設定屬性值

有了屬性的定義要訪問屬性值就很輕易了。有幾個方法用來讀取和設定類的屬性,它們是:GetField、SetField、GetStaticField、SetStaticField。比如讀取Demo類的msg屬性就可以用GetObjectField,而訪問COUNT用GetStaticIntField,相關程式碼如下:

jfieldID field = (*env)->GetFieldID(env,obj,"msg"," Ljava/lang/String;"); jstring msg = (*env)-> GetObjectField(env, cls, field);//msg就是對應Demo的'msg jfieldID field2 = (*env)->GetStaticFieldID(env,obj,"COUNT","I"); jint count = (*env)->GetStaticIntField(env,cls,field2);

  訪問建構函式

很多人剛剛接觸JNI的時候往往會在這一節碰到問題,查遍了整個jni.h看到這樣一個函式NewObject,它應該是可以用來訪問類的建構函式。但是該函式需要提供建構函式的方法定義,其型別是jmethodID。從前面的內容我們知道要獲取方法的定義首先要知道方法的名稱,但是建構函式的名稱怎麼來填寫呢?其實訪問建構函式與訪問一個普通的類方法大體上是一樣的,惟一不同的只是方法名稱不同及方法呼叫時不同而已。訪問類的建構函式時方法名必須填寫“”。下面的程式碼演示如何構造一個Demo類的例項:

jclass cls = (*env)->FindClass(env, "jni/test/Demo"); /** 首先通過類的名稱獲取類的定義,相當於Java中的ame方法 */ if (cls == 0) jmethodID mid = (*env)->GetMethodID(env,cls,"","(Ljava/lang/String;)V "); if(mid == 0) jobject demo = jenv->NewObject(cls,mid,0); /** 訪問建構函式必須使用NewObject的函式來呼叫前面獲取的建構函式的定義 上面的程式碼我們構造了一個Demo的例項並傳一個空串null */

  陣列處理

  建立一個新陣列

要建立一個數組,我們首先應該知道陣列元素的型別及陣列長度。JNI定義了一批陣列的型別jArray及陣列操作的函式NewArray,其中就是陣列中元素的型別。例如,要建立一個大小為10並且每個位置值分別為1-10的整數陣列,編寫程式碼如下:

int i = 1; jintArray array;//定義陣列物件 (*env)-> NewIntArray(env, 10); for(; i<= 10; i++) (*env)->SetIntArrayRegion(env, array, i-1, 1, &i);

  訪問陣列中的資料

訪問陣列首先應該知道陣列的長度及元素的型別。現在我們把建立的陣列中的每個元素值打印出來,程式碼如下:

int i; /* 獲取陣列物件的元素個數 */ int len = (*env)->GetArrayLength(env, array); /* 獲取陣列中的所有元素 */ jint* elems = (*env)-> GetIntArrayElements(env, array, 0); for(i=0; i< len; i++) printf("ELEMENT %d IS %d ", i, elems[i]);

  中文處理

中文字元的處理往往是讓人比較頭疼的事情,非凡是使用Java語言開發的軟體,在JNI這個問題更加突出。由於Java中所有的字元都是Unicode編碼,但是在本地方法中,例如用VC編寫的程式,假如沒有非凡的定義一般都沒有使用Unicode的編碼方式。為了讓本地方法能夠訪問Java中定義的中文字元及Java訪問本地方法產生的中文字串,我定義了兩個方法用來做相互轉換。

  · 方法一,將Java中文字串轉為本地字串

/** 第一個引數是虛擬機器的環境指標第二個引數為待轉換的Java字串定義第三個引數是本地儲存轉換後字串的記憶體塊第三個引數是記憶體塊的大小 */ int JStringToChar(JNIEnv *env, jstring str, LPTSTR desc, int desc_len) { int len = 0; if(desc==NULLstr==NULL) return -1; //在VC中wchar_t是用來儲存寬位元組字元(UNICODE)的資料型別 wchar_t *w_buffer = new wchar_t[1024]; ZeroMemory(w_buffer,1024*sizeof(wchar_t)); //使用GetStringChars而不是GetStringUTFChars wcscpy(w_buffer,env->GetStringChars(str,0)); env->ReleaseStringChars(str,w_buffer); ZeroMemory(desc,desc_len); //呼叫字元編碼轉換函式(Win32 API)將UNICODE轉為ASCII編碼格式字串 //關於函式WideCharToMultiByte的使用請參考MSDN len = WideCharToMultiByte(CP_ACP,0,w_buffer,1024,desc,desc_len,NULL,NULL); //len = wcslen(w_buffer); if(len>0 && len

  · 方法二,將C的字串轉為Java能識別的Unicode字串

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),buffer,slen); if(len>0 && len < slen) buffer[len]=0; jstring js = env->NewString(buffer,len); [] buffer; return js; }

  異常

由於呼叫了Java的方法,因此難免產生操作的異常資訊。這些異常沒有辦法通過C++本身的異常處理機制來捕捉到,但JNI可以通過一些函式來獲取Java中丟擲的異常資訊。之前我們在Demo類中定義了一個方法throwExcp,下面將訪問該方法並捕捉其丟擲來的異常資訊,程式碼如下:

/** 假設我們已經構造了一個Demo的例項obj,其類定義為cls */ jthrowable excp = 0;/* 異常資訊定義 */ jmethodID mid=(*env)->GetMethodID(env,cls,"throwExcp","()V"); /*假如mid為0表示獲取方法定義失敗*/ jstring msg = (*env)-> CallVoidMethod(env, obj, mid); /* 在呼叫該方法後會有一個IllegalAccessException的異常丟擲 */ excp = (*env)->ExceptionOccurred(env); if(excp){ (*env)->ExceptionClear(env); //通過訪問excp來獲取具體異常資訊 /* 在Java中,大部分的異常資訊都是擴充套件類ption,因此可以訪問excp的toString 或者getMessage來獲取異常資訊的內容。訪問這兩個方法同前面講到的如何訪問類的方法是相同的。 */ }

  執行緒和同步訪問

有些時候需要使用多執行緒的方式來訪問Java的方法。我們知道一個Java虛擬機器是非常消耗系統的記憶體資源,差不多每個虛擬機器需要記憶體大約在20MB左右。為了節省資源要求每個執行緒使用的是同一個虛擬機器,這樣在整個的JNI程式中只需要初始化一個虛擬機器就可以了。所有人都是這樣想的,但是一旦子執行緒訪問主執行緒建立的虛擬機器環境變數,系統就會出現錯誤對話方塊,然後整個程式終止。

其實這裡面涉及到兩個概念,它們分別是虛擬機器(JavaVM *jvm)和虛擬機器環境(JNIEnv *env)。真正消耗大量系統資源的是jvm而不是env,jvm是答應多個執行緒訪問的,但是env只能被建立它本身的執行緒所訪問,而且每個執行緒必須建立自己的虛擬機器環境env。這時候會有人提出疑問,主執行緒在初始化虛擬機器的時候就建立了虛擬機器環境env。為了讓子執行緒能夠建立自己的env,JNI提供了兩個函式:AttachCurrentThread和DetachCurrentThread。下面程式碼就是子執行緒訪問Java方法的框架:

DWord WINAPI ThreadProc(PVOID dwParam) { JavaVM jvm = (JavaVM*)dwParam;/* 將虛擬機器通過引數傳入 */ JNIEnv* env; (*jvm)-> AttachCurrentThread(jvm, (void**)&env, NULL); ......... (*jvm)-> DetachCurrentThread(jvm); }

  時間

關於時間的話題是我在實際開發中碰到的一個問題。當要釋出使用了JNI的程式時,並不一定要求客戶要安裝一個Java執行環境,因為可以在安裝程式中打包這個執行環境。為了讓打包程式利於下載,這個包要比較小,因此要去除JRE(Java執行環境)中一些不必要的檔案。但是假如程式中用到Java中的日曆型別,例如ndar等,那麼有個檔案一定不能去掉,這個檔案就是[JRE]lib zmappings。它是一個時區對映檔案,一旦沒有該檔案就會發現時間操作上經常出現與正確時間相差幾個小時的情況。下面是打包JRE中必不可少的檔案列表(以Windows環境為例),其中[JRE]為執行環境的目錄,同時這些檔案之間的相對路徑不能變。

檔名目錄 [JRE]in [JRE]in [JRE]in [JRE]in [JRE]in [JRE]in [JRE]inclassic [JRE]lib tzmappings [JRE]lib

由於有差不多10MB,但是其中有很大一部分檔案並不需要,可以根據實際的應用情況進行刪除。例如程式假如沒有用到Java Swing,就可以把涉及到Swing的檔案都刪除後重新打包。