在Windows中實現Java呼叫DLL
本文提供呼叫本地 C 程式碼的 Java 程式碼示例,包括傳遞和返回某些常用的資料型別。本地方法包含在特定於平臺的可執行檔案中。就本文中的示例而言,本地方法包含在 Windows 32 位動態連結庫 (DLL) 中。
不過我要提醒您,對 Java 外部的呼叫通常不能移植到其他平臺上,在 applet 中還可能引發安全異常。實現原生代碼將使您的 Java 應用程式無法通過 100% 純 Java 測試。但是,如果必須執行本地呼叫,則要考慮幾個準則:
- 將您的所有本地方法都封裝在單個類中,這個類呼叫單個 DLL。對於每種目標作業系統,都可以用特定於適當平臺的版本替換這個 DLL。這樣就可以將原生代碼的影響減至最小,並有助於將以後所需的移植問題包含在內。
- 本地方法要簡單。儘量將您的 DLL 對任何第三方(包括 Microsoft)執行時 DLL 的依賴減到最小。使您的本地方法儘量獨立,以將載入您的 DLL 和應用程式所需的開銷減到最小。如果需要執行時 DLL,必須隨應用程式一起提供它們。
對於呼叫 C 函式的 Java 方法,必須在 Java 類中宣告一個本地方法。在本部分的所有示例中,我們將建立一個名為 MyNative 的類,並逐步在其中加入新的功能。這強調了一種思想,即將本地方法集中在單個類中,以便將以後所需的移植工作減到最少。
在第一個示例中,我們將三個常用引數型別傳遞給本地函式: String、 int和 boolean 。本例說明在本地 C 程式碼中如何引用這些引數。
public class MyNative { public void showParms( String s, int i, boolean b ) { showParms0( s, i , b ); } private native void showParms0( String s, int i, boolean b ); static { System.loadLibrary( "MyNative" ); } } |
請注意,本地方法被宣告為專用的,並建立了一個包裝方法用於公用目的。這進一步將本地方法同程式碼的其餘部分隔離開來,從而允許針對所需的平臺對它進行優化。 static
下一步是生成 C 程式碼來實現 showParms0 方法。此方法的 C 函式原型是通過對 .class 檔案使用 javah 實用程式來建立的,而 .class 檔案是通過編譯 MyNative.java 檔案生成的。這個實用程式可在 JDK 中找到。下面是 javah 的用法:
javac MyNative.java(將 .java 編譯為 .class) javah -jni MyNative(生成 .h 檔案) |
這將生成一個 MyNative.h 檔案,其中包含一個本地方法原型,如下所示:
/* * Class: MyNative * Method: showParms0 * Signature: (Ljava/lang/String;IZ)V */ JNIEXPORT void JNICALL Java_MyNative_showParms0 (JNIEnv *, jobject, jstring, jint, jboolean); |
第一個引數是呼叫 JNI 方法時使用的 JNI Environment 指標。第二個引數是指向在此 Java 程式碼中例項化的 Java 物件 MyNative 的一個控制代碼。其他引數是方法本身的引數。請注意,MyNative.h 包括標頭檔案 jni.h。jni.h 包含 JNI API 和變數型別(包括jobject、jstring、jint、jboolean,等等)的原型和其他宣告。
本地方法是在檔案 MyNative.c 中用 C 語言實現的:
#include <stdio.h> #include "MyNative.h" JNIEXPORT void JNICALL Java_MyNative_showParms0 (JNIEnv *env, jobject obj, jstring s, jint i, jboolean b) { const char* szStr = (*env)->GetStringUTFChars( env, s, 0 ); printf( "String = [%s]/n", szStr ); printf( "int = %d/n", i ); printf( "boolean = %s/n", (b==JNI_TRUE ? "true" : "false") ); (*env)->ReleaseStringUTFChars( env, s, szStr ); } |
JNI API,GetStringUTFChars,用來根據 Java 字串或 jstring 引數建立 C 字串。這是必需的,因為在原生代碼中不能直接讀取 Java 字串,而必須將其轉換為 C 字串或 Unicode。有關轉換 Java 字串的詳細資訊,請參閱標題為 NLS Strings and JNI 的一篇論文。但是,jboolean 和 jint 值可以直接使用。
MyNative.dll 是通過編譯 C 原始檔建立的。下面的編譯語句使用 Microsoft Visual C++ 編譯器:
cl -Ic:/jdk1.1.6/include -Ic:/jdk1.1.6/include/win32 -LD MyNative.c -FeMyNative.dll |
其中 c:/jdk1.1.6 是 JDK 的安裝路徑。
MyNative.dll 已建立好,現在就可將其用於 MyNative 類了。
可以這樣測試這個本地方法:在 MyNative 類中建立一個 main 方法來呼叫 showParms 方法,如下所示:
public static void main( String[] args ) { MyNative obj = new MyNative(); obj.showParms( "Hello", 23, true ); obj.showParms( "World", 34, false ); } |
當執行這個 Java 應用程式時,請確保 MyNative.dll 位於 Windows 的 PATH 環境變數所指定的路徑中或當前目錄下。當執行此 Java 程式時,如果未找到這個 DLL,您可能會看到以下的訊息:
java MyNative Can't find class MyNative |
這是因為 static 子句無法載入這個 DLL,所以在初始化 MyNative 類時引發異常。Java 直譯器處理這個異常,並報告一個一般錯誤,指出找不到這個類。
如果用 -verbose 命令列選項執行直譯器,您將看到它因找不到這個 DLL 而載入 UnsatisfiedLinkError 異常。
如果此 Java 程式完成執行,就會輸出以下內容:
java MyNative String = [Hello] int = 23 boolean = true String = [World] int = 34 |
boolean = false
示例 2 -- 返回一個值
本例將說明如何在本地方法中實現返回程式碼。
將這個方法新增到 MyNative 類中,這個類現在變為以下形式:
public class MyNative { public void showParms( String s, int i, boolean b ) { showParms0( s, i , b ); } public int hypotenuse( int a, int b ) { return hyptenuse0( a, b ); } private native void showParms0( String s, int i, boolean b ); private native int hypotenuse0( int a, int b ); static { System.loadLibrary( "MyNative" ); } /* 測試本地方法 */ public static void main( String[] args ) { MyNative obj = new MyNative(); System.out.println( obj.hypotenuse(3,4) ); System.out.println( obj.hypotenuse(9,12) ); } } |
公用的 hypotenuse 方法呼叫本地方法 hypotenuse0 來根據傳遞的引數計算值,並將結果作為一個整數返回。這個新本地方法的原型是使用 javah 生成的。請注意,每次執行這個實用程式時,它將自動覆蓋當前目錄中的 MyNative.h。按以下方式執行 javah:
javah -jni MyNative |
生成的 MyNative.h 現在包含 hypotenuse0 原型,如下所示:
/* * Class: MyNative * Method: hypotenuse0 * Signature: (II)I */ JNIEXPORT jint JNICALL Java_MyNative_hypotenuse0 (JNIEnv *, jobject, jint, jint); |
該方法是在 MyNative.c 原始檔中實現的,如下所示:
#include <stdio.h> #include <math.h> #include "MyNative.h" JNIEXPORT void JNICALL Java_MyNative_showParms0 (JNIEnv *env, jobject obj, jstring s, jint i, jboolean b) { const char* szStr = (*env)->GetStringUTFChars( env, s, 0 ); printf( "String = [%s]/n", szStr ); printf( "int = %d/n", i ); printf( "boolean = %s/n", (b==JNI_TRUE ? "true" : "false") ); (*env)->ReleaseStringUTFChars( env, s, szStr ); } JNIEXPORT jint JNICALL Java_MyNative_hypotenuse0 (JNIEnv *env, jobject obj, jint a, jint b) { int rtn = (int)sqrt( (double)( (a*a) + (b*b) ) ); return (jint)rtn; } |
再次請注意,jint 和 int 值是可互換的。
使用相同的編譯語句重新編譯這個 DLL:
cl -Ic:/jdk1.1.6/include -Ic:/jdk1.1.6/include/win32 -LD MyNative.c -FeMyNative.dll |
現在執行 java MyNative 將輸出 5 和 15 作為斜邊的值。
示例 3 -- 靜態方法
您可能在上面的示例中已經注意到,例項化的 MyNative 物件是沒必要的。實用方法通常不需要實際的物件,通常都將它們建立為靜態方法。本例說明如何用一個靜態方法實現上面的示例。更改 MyNative.java 中的方法簽名,以使它們成為靜態方法:
public static int hypotenuse( int a, int b ) { return hypotenuse0(a,b); } ... private static native int hypotenuse0( int a, int b ); |
現在執行 javah 為 hypotenuse0建立一個新原型,生成的原型如下所示:
/* * Class: MyNative * Method: hypotenuse0 * Signature: (II)I */ JNIEXPORT jint JNICALL Java_MyNative_hypotenuse0 (JNIEnv *, jclass, jint, jint); |
C 原始碼中的方法簽名變了,但程式碼還保持原樣:
JNIEXPORT jint JNICALL Java_MyNative_hypotenuse0 (JNIEnv *env, jclass cls, jint a, jint b) { int rtn = (int)sqrt( (double)( (a*a) + (b*b) ) ); return (jint)rtn; } |
本質上,jobject 引數已變為 jclass 引數。此引數是指向 MyNative.class 的一個控制代碼。main 方法可更改為以下形式:
public static void main( String[] args ) { System.out.println( MyNative.hypotenuse( 3, 4 ) ); System.out.println( MyNative.hypotenuse( 9, 12 ) ); } |
因為方法是靜態的,所以呼叫它不需要例項化 MyNative 物件。本文後面的示例將使用靜態方法。
示例 4 -- 傳遞陣列
本例說明如何傳遞陣列型引數。本例使用一個基本型別,boolean,並將更改陣列元素。下一個示例將訪問 String(非基本型別)陣列。將下面的方法新增到 MyNative.java 原始碼中:
public static void setArray( boolean[] ba ) { for( int i=0; i < ba.length; i++ ) ba[i] = true; setArray0( ba ); } ... private static native void setArray0( boolean[] ba ); |
在本例中,布林型陣列被初始化為 true,本地方法將把特定的元素設定為 false。同時,在 Java 原始碼中,我們可以更改 main 以使其包含測試程式碼:
boolean[] ba = new boolean[5]; MyNative.setArray( ba ); for( int i=0; i < ba.length; i++ ) System.out.println( ba[i] ); |
在編譯原始碼並執行 javah 以後,MyNative.h 標頭檔案包含以下的原型:
/* * Class: MyNative * Method: setArray0 * Signature: ([Z)V */ JNIEXPORT void JNICALL Java_MyNative_setArray0 (JNIEnv *, jclass, jbooleanArray); |
請注意,布林型陣列是作為單個名為 jbooleanArray 的型別建立的。
基本型別有它們自已的陣列型別,如 jintArray 和 jcharArray。
非基本型別的陣列使用 jobjectArray 型別。下一個示例中包括一個 jobjectArray。這個布林陣列的陣列元素是通過 JNI 方法 GetBooleanArrayElements 來訪問的。
針對每種基本型別都有等價的方法。這個本地方法是如下實現的:
JNIEXPORT void JNICALL Java_MyNative_setArray0 (JNIEnv *env, jclass cls, jbooleanArray ba) { jboolean* pba = (*env)->GetBooleanArrayElements( env, ba, 0 ); jsize len = (*env)->GetArrayLength(env, ba); int i=0; // 更改偶數陣列元素 for( i=0; i < len; i+=2 ) pba[i] = JNI_FALSE; (*env)->ReleaseBooleanArrayElements( env, ba, pba, 0 ); } |
指向布林型陣列的指標可以使用 GetBooleanArrayElements 獲得。
陣列大小可以用 GetArrayLength 方法獲得。使用 ReleaseBooleanArrayElements 方法釋放陣列。現在就可以讀取和修改陣列元素的值了。jsize 宣告等價於 jint(要檢視它的定義,請參閱 JDK 的 include 目錄下的 jni.h 標頭檔案)。
示例 5 -- 傳遞 Java String 陣列
本例將通過最常用的非基本型別,Java String,說明如何訪問非基本物件的陣列。字串陣列被傳遞給本地方法,而本地方法只是將它們顯示到控制檯上。
MyNative 類定義中添加了以下幾個方法:
public static void showStrings( String[] sa ) { showStrings0( sa ); } private static void showStrings0( String[] sa ); |
並在 main
方法中添加了兩行進行測試:
String[] sa = new String[] { "Hello,", "world!", "JNI", "is", "fun." }; MyNative.showStrings( sa ); |
本地方法分別訪問每個元素,其實現如下所示。
JNIEXPORT void JNICALL Java_MyNative_showStrings0 (JNIEnv *env, jclass cls, jobjectArray sa) { int len = (*env)->GetArrayLength( env, sa ); int i=0; for( i=0; i < len; i++ ) { jobject obj = (*env)->GetObjectArrayElement(env, sa, i); jstring str = (jstring)obj; const char* szStr = (*env)->GetStringUTFChars( env, str, 0 ); printf( "%s ", szStr ); (*env)->ReleaseStringUTFChars( env, str, szStr ); } printf( "/n" ); } |
陣列元素可以通過 GetObjectArrayElement 訪問。
在本例中,我們知道返回值是 jstring 型別,所以可以安全地將它從 jobject 型別轉換為 jstring 型別。字串是通過前面討論過的方法列印的。有關在 Windows 中處理 Java 字串的資訊,請參閱標題為 NLS Strings and JNI 的一篇論文。
示例 6 -- 返回 Java String 陣列
最後一個示例說明如何在原生代碼中建立一個字串陣列並將它返回給 Java 呼叫者。MyNative.java 中添加了以下幾個方法:
public static String[] getStrings() { return getStrings0(); } private static native String[] getStrings0(); |
更改 main
以使 showStrings
將 getStrings
的輸出顯示出來:
MyNative.showStrings( MyNative.getStrings() ); |
實現的本地方法返回五個字串。
JNIEXPORT jobjectArray JNICALL Java_MyNative_getStrings0 (JNIEnv *env, jclass cls) { jstring str; jobjectArray args = 0; jsize len = 5; char* sa[] = { "Hello,", "world!", "JNI", "is", "fun" }; int i=0; args = (*env)->NewObjectArray(env, len, (*env)->FindClass(env, "java/lang/String"), 0); for( i=0; i < len; i++ ) { str = (*env)->NewStringUTF( env, sa[i] ); (*env)->SetObjectArrayElement(env, args, i, str); } return args; } |
字串陣列是通過呼叫 NewObjectArray 建立的,同時傳遞了 String 類和陣列長度兩個引數。Java String 是使用 NewStringUTF 建立的。String 元素是使用 SetObjectArrayElement 存入陣列中的。
|
現在您已經為您的應用程式建立了一個本地 DLL,但在除錯時還要牢記以下幾點。如果使用 Java 偵錯程式 java_g.exe,則還需要建立 DLL 的一個“除錯”版本。這只是表示必須建立同名但帶有一個 _g 字尾的 DLL 版本。就 MyNative.dll 而言,使用 java_g.exe 要求在 Windows 的 PATH 環境指定的路徑中有一個 MyNative_g.dll 檔案。在大多數情況下,這個 DLL 可以通過將原檔案重新命名或複製為其名稱帶綴 _g 的檔案。
現在,Java 偵錯程式不允許您進入原生代碼,但您可以在 Java 環境外使用 C 偵錯程式(如 Microsoft Visual C++)除錯本地方法。首先將原始檔匯入一個專案中。
將編譯設定調整為在編譯時將 include 目錄包括在內:
c:/jdk1.1.6/include;c:/jdk1.1.6/include/win32 |
將配置設定為以除錯模式編譯 DLL。在 Project Settings 中的 Debug 下,將可執行檔案設定為 java.exe(或者 java_g.exe,但要確保您生成了一個 _g.dll 檔案)。程式引數包括包含 main 的類名。如果在 DLL 中設定了斷點,則當呼叫本地方法時,執行將在適當的地方停止。
下面是設定一個 Visual C++ 6.0 專案來除錯本地方法的步驟。
- 在 Visual C++ 中建立一個 Win32 DLL 專案,並將 .c 和 .h 檔案新增到這個專案中。
- 在 Tools 下拉式選單的 Options 設定下設定 JDK 的 include 目錄。下面的對話方塊顯示了這些目錄。
- 選擇 Build 下拉式選單下的 Build MyNative.dll 來建立這個專案。確保將專案的活動配置設定為除錯(這通常是預設值)。
- 在 Project Settings 下,設定 Debug 選項卡來呼叫適當的 Java 直譯器,如下所示:
當執行這個程式時,忽略“在 java.exe 中找不到任何除錯資訊”的訊息。當呼叫本地方法時,在 C 程式碼中設定的任何斷點將在適當的地方停止 Java 程式的執行。
JNI 方法和 C++
上面這些示例說明了如何在 C 原始檔中使用 JNI 方法。如果使用 C++,則請將相應方法的格式從:
(*env)->JNIMethod( env, .... ); |
更改為:
env->JNIMethod( ... ); |
在 C++ 中,JNI 函式被看作是 JNIEnv 類的成員方法。
字串和國家語言支援
本文中使用的技術用 UTF 方法來轉換字串。使用這些方法只是為了方便起見,如果應用程式需要國家語言支援 (NLS),則不能使用這些方法。有關在 Windows 和 NLS 環境中處理 Java 字串正確方法,請參標題為 NLS Strings and JNI 的一篇論文。
|
本文提供的示例用最常用的資料類據(如 jint 和 jstring)說明了如何實現本地方法,並討論了 Windows 特定的幾個問題,如顯示字串。本文提供的示例並未包括全部 JNI,JNI 還包括其他引數型別,如 jfloat、jdouble、jshort、jbyte 和 jfieldID,以及用來處理這些型別的方法。有關這個主題的詳細資訊,請參閱 Sun Microsystems 提供的 Java 本地介面規範。