本文為在 32 位 Windows 平臺(tái)上實(shí)現(xiàn) Java 本地方法提供了實(shí)用的示例、步驟和準(zhǔn)則。這些示例包括傳遞和返回常用的數(shù)據(jù)類型。
本文中的示例使用 Sun Microsystems 公司創(chuàng)建的 Java Development Kit (JDK) 版本 1.1.6 和 Java 本地接口 (JNI) 規(guī)范。用 C 語(yǔ)言編寫的本地代碼是用 Microsoft Visual C++ 編譯器編譯生成的。
簡(jiǎn)介
本文提供調(diào)用本地 C 代碼的 Java 代碼示例,包括傳遞和返回某些常用的數(shù)據(jù)類型。本地方法包含在特定于平臺(tái)的可執(zhí)行文件中。就本文中的示例而言,本地方法包含在 Windows 32 位動(dòng)態(tài)鏈接庫(kù) (DLL) 中。
不過我要提醒您,對(duì) Java 外部的調(diào)用通常不能移植到其他平臺(tái)上,在 applet 中還可能引發(fā)安全異常。實(shí)現(xiàn)本地代碼將使您的 Java 應(yīng)用程序無(wú)法通過 100% 純 Java 測(cè)試。但是,如果必須執(zhí)行本地調(diào)用,則要考慮幾個(gè)準(zhǔn)則:
1. 將您的所有本地方法都封裝在單個(gè)類中,這個(gè)類調(diào)用單個(gè) DLL。對(duì)于每種目標(biāo)操作系統(tǒng),都可以用特定于適當(dāng)平臺(tái)的版本替換這個(gè) DLL。這樣就可以將本地代碼的影響減至最小,并有助于將以后所需的移植問題包含在內(nèi)。
2. 本地方法要簡(jiǎn)單。盡量將您的 DLL 對(duì)任何第三方(包括 Microsoft)運(yùn)行時(shí) DLL 的依賴減到最小。使您的本地方法盡量獨(dú)立,以將加載您的 DLL 和應(yīng)用程序所需的開銷減到最小。如果需要運(yùn)行時(shí) DLL,必須隨應(yīng)用程序一起提供它們。
Java 調(diào)用 C
對(duì)于調(diào)用 C 函數(shù)的 Java 方法,必須在 Java 類中聲明一個(gè)本地方法。在本部分的所有示例中,我們將創(chuàng)建一個(gè)名為 MyNative 的類,并逐步在其中加入新的功能。這強(qiáng)調(diào)了一種思想,即將本地方法集中在單個(gè)類中,以便將以后所需的移植工作減到最少。
示例 1 -- 傳遞參數(shù)
在第一個(gè)示例中,我們將三個(gè)常用參數(shù)類型傳遞給本地函數(shù):String、int 和 boolean。本例說明在本地 C 代碼中如何引用這些參數(shù)。
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" );
}
}
請(qǐng)注意,本地方法被聲明為專用的,并創(chuàng)建了一個(gè)包裝方法用于公用目的。這進(jìn)一步將本地方法同代碼的其余部分隔離開來(lái),從而允許針對(duì)所需的平臺(tái)對(duì)它進(jìn)行優(yōu)化。static 子句加載包含本地方法實(shí)現(xiàn)的 DLL。
下一步是生成 C 代碼來(lái)實(shí)現(xiàn) showParms0 方法。此方法的 C 函數(shù)原型是通過對(duì) .class 文件使用 javah 實(shí)用程序來(lái)創(chuàng)建的,而 .class 文件是通過編譯 MyNative.java 文件生成的。這個(gè)實(shí)用程序可在 JDK 中找到。下面是 javah 的用法:
javac MyNative.java(將 .java 編譯為 .class)
javah -jni
MyNative(生成 .h 文件)
這將生成一個(gè) MyNative.h 文件,其中包含一個(gè)本地方法原型,如下所示:
/*
* Class: MyNative
* Method: showParms0
* Signature: (Ljava/lang/String;IZ)V
*/
JNIEXPORT void JNICALL Java_MyNative_showParms0
(JNIEnv *, jobject, jstring, jint, jboolean);
第一個(gè)參數(shù)是調(diào)用 JNI 方法時(shí)使用的 JNI Environment 指針。第二個(gè)參數(shù)是指向在此 Java 代碼中實(shí)例化的 Java 對(duì)象 MyNative 的一個(gè)句柄。其他參數(shù)是方法本身的參數(shù)。請(qǐng)注意,MyNative.h 包括頭文件 jni.h。jni.h 包含 JNI API 和變量類型(包括jobject、jstring、jint、jboolean,等等)的原型和其他聲明。
本地方法是在文件 MyNative.c 中用 C 語(yǔ)言實(shí)現(xiàn)的:
#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,用來(lái)根據(jù) Java 字符串或 jstring 參數(shù)創(chuàng)建 C 字符串。這是必需的,因?yàn)樵诒镜卮a中不能直接讀取 Java 字符串,而必須將其轉(zhuǎn)換為 C 字符串或 Unicode。有關(guān)轉(zhuǎn)換 Java 字符串的詳細(xì)信息,請(qǐng)參閱標(biāo)題為 NLS Strings and JNI 的一篇論文。但是,jboolean 和 jint 值可以直接使用。
MyNative.dll 是通過編譯 C 源文件創(chuàng)建的。下面的編譯語(yǔ)句使用 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 已創(chuàng)建好,現(xiàn)在就可將其用于 MyNative 類了。可以這樣測(cè)試這個(gè)本地方法:在 MyNative 類中創(chuàng)建一個(gè) main 方法來(lái)調(diào)用 showParms 方法,如下所示:
public static void main( String[] args )
{
MyNative obj = new MyNative();
obj.showParms( "Hello", 23, true );
obj.showParms( "World", 34, false );
}
當(dāng)運(yùn)行這個(gè) Java 應(yīng)用程序時(shí),請(qǐng)確保 MyNative.dll 位于 Windows 的 PATH 環(huán)境變量所指定的路徑中或當(dāng)前目錄下。當(dāng)執(zhí)行此 Java 程序時(shí),如果未找到這個(gè) DLL,您可能會(huì)看到以下的消息:
java MyNative
Can't find class MyNative
這是因?yàn)? static 子句無(wú)法加載這個(gè) DLL,所以在初始化 MyNative 類時(shí)引發(fā)異常。Java 解釋器處理這個(gè)異常,并報(bào)告一個(gè)一般錯(cuò)誤,指出找不到這個(gè)類。如果用 -verbose 命令行選項(xiàng)運(yùn)行解釋器,您將看到它因找不到這個(gè) DLL 而加載 UnsatisfiedLinkError 異常。
如果此 Java 程序完成運(yùn)行,就會(huì)輸出以下內(nèi)容:
java MyNative
String = [Hello]
int = 23
boolean = true
String = [World]
int
= 34
boolean = false
示例 2 -- 返回一個(gè)值
本例將說明如何在本地方法中實(shí)現(xiàn)返回代碼。將這個(gè)方法添加到 MyNative 類中,這個(gè)類現(xiàn)在變?yōu)橐韵滦问剑?
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" );
}
/* 測(cè)試本地方法 */
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 方法調(diào)用本地方法 hypotenuse0 來(lái)根據(jù)傳遞的參數(shù)計(jì)算值,并將結(jié)果作為一個(gè)整數(shù)返回。這個(gè)新本地方法的原型是使用 javah 生成的。請(qǐng)注意,每次運(yùn)行這個(gè)實(shí)用程序時(shí),它將自動(dòng)覆蓋當(dāng)前目錄中的 MyNative.h。按以下方式執(zhí)行 javah:
javah -jni MyNative
生成的 MyNative.h 現(xiàn)在包含 hypotenuse0 原型,如下所示:
/*
* Class: MyNative
* Method: hypotenuse0
* Signature: (II)I
*/
JNIEXPORT jint JNICALL Java_MyNative_hypotenuse0
(JNIEnv *, jobject, jint, jint);
該方法是在 MyNative.c 源文件中實(shí)現(xiàn)的,如下所示:
#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;
}
再次請(qǐng)注意,jint 和 int 值是可互換的。使用相同的編譯語(yǔ)句重新編譯這個(gè) DLL:
cl -Ic:\jdk1.1.6\include -Ic:\jdk1.1.6\include\win32 -LD MyNative.c
-FeMyNative.dll
現(xiàn)在執(zhí)行 java MyNative 將輸出 5 和 15 作為斜邊的值。
示例 3 -- 靜態(tài)方法
您可能在上面的示例中已經(jīng)注意到,實(shí)例化的 MyNative 對(duì)象是沒必要的。實(shí)用方法通常不需要實(shí)際的對(duì)象,通常都將它們創(chuàng)建為靜態(tài)方法。本例說明如何用一個(gè)靜態(tài)方法實(shí)現(xiàn)上面的示例。更改 MyNative.java 中的方法簽名,以使它們成為靜態(tài)方法:
public static int hypotenuse( int a, int b )
{
return hypotenuse0(a,b);
}
...
private static native int hypotenuse0( int a, int b );
現(xiàn)在運(yùn)行 javah 為 hypotenuse0 創(chuàng)建一個(gè)新原型,生成的原型如下所示:
/*
* 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;
}
本質(zhì)上,jobject 參數(shù)已變?yōu)? jclass 參數(shù)。此參數(shù)是指向 MyNative.class 的一個(gè)句柄。main 方法可更改為以下形式:
public static void main( String[] args )
{
System.out.println( MyNative.hypotenuse( 3, 4 ) );
System.out.println( MyNative.hypotenuse( 9, 12 ) );
}
因?yàn)榉椒ㄊ庆o態(tài)的,所以調(diào)用它不需要實(shí)例化 MyNative 對(duì)象。本文后面的示例將使用靜態(tài)方法。
示例 4 -- 傳遞數(shù)組
本例說明如何傳遞數(shù)組型參數(shù)。本例使用一個(gè)基本類型,boolean,并將更改數(shù)組元素。下一個(gè)示例將訪問 String(非基本類型)數(shù)組。將下面的方法添加到 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 );
在本例中,布爾型數(shù)組被初始化為 true,本地方法將把特定的元素設(shè)置為 false。同時(shí),在 Java 源代碼中,我們可以更改 main 以使其包含測(cè)試代碼:
boolean[] ba = new boolean[5];
MyNative.setArray( ba );
for( int i=0; i < ba.length; i++ )
System.out.println( ba[i] );
在編譯源代碼并執(zhí)行 javah 以后,MyNative.h 頭文件包含以下的原型:
/*
* Class: MyNative
* Method: setArray0
* Signature: ([Z)V
*/
JNIEXPORT void JNICALL Java_MyNative_setArray0
(JNIEnv *, jclass, jbooleanArray);
請(qǐng)注意,布爾型數(shù)組是作為單個(gè)名為 jbooleanArray 的類型創(chuàng)建的。基本類型有它們自已的數(shù)組類型,如 jintArray 和 jcharArray。非基本類型的數(shù)組使用 jobjectArray 類型。下一個(gè)示例中包括一個(gè) jobjectArray。這個(gè)布爾數(shù)組的數(shù)組元素是通過 JNI 方法 GetBooleanArrayElements 來(lái)訪問的。針對(duì)每種基本類型都有等價(jià)的方法。這個(gè)本地方法是如下實(shí)現(xiàn)的:
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;
// 更改偶數(shù)數(shù)組元素
for( i=0; i < len; i+=2 )
pba[i] = JNI_FALSE;
(*env)->ReleaseBooleanArrayElements( env, ba, pba, 0 );
}
指向布爾型數(shù)組的指針可以使用 GetBooleanArrayElements 獲得。數(shù)組大小可以用 GetArrayLength 方法獲得。使用 ReleaseBooleanArrayElements 方法釋放數(shù)組。現(xiàn)在就可以讀取和修改數(shù)組元素的值了。jsize 聲明等價(jià)于 jint(要查看它的定義,請(qǐng)參閱 JDK 的 include 目錄下的 jni.h 頭文件)。
示例 5 -- 傳遞 Java String 數(shù)組
本例將通過最常用的非基本類型,Java String,說明如何訪問非基本對(duì)象的數(shù)組。字符串?dāng)?shù)組被傳遞給本地方法,而本地方法只是將它們顯示到控制臺(tái)上。 MyNative 類定義中添加了以下幾個(gè)方法:
public static void showStrings( String[] sa )
{
showStrings0( sa );
}
private static void showStrings0( String[] sa );
并在 main 方法中添加了兩行進(jìn)行測(cè)試:
String[] sa = new String[] { "Hello,", "world!", "JNI", "is", "fun." };
MyNative.showStrings( sa );
本地方法分別訪問每個(gè)元素,其實(shí)現(xiàn)如下所示。
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" );
}
數(shù)組元素可以通過 GetObjectArrayElement 訪問。在本例中,我們知道返回值是 jstring 類型,所以可以安全地將它從 jobject 類型轉(zhuǎn)換為 jstring 類型。字符串是通過前面討論過的方法打印的。有關(guān)在 Windows 中處理 Java 字符串的信息,請(qǐng)參閱標(biāo)題為 NLS Strings and JNI 的一篇論文。
示例 6 -- 返回 Java String 數(shù)組
最后一個(gè)示例說明如何在本地代碼中創(chuàng)建一個(gè)字符串?dāng)?shù)組并將它返回給 Java 調(diào)用者。MyNative.java 中添加了以下幾個(gè)方法:
public static String[] getStrings()
{
return getStrings0();
}
private static native String[] getStrings0();
更改 main 以使 showStrings 將 getStrings 的輸出顯示出來(lái):
MyNative.showStrings( MyNative.getStrings() );
實(shí)現(xiàn)的本地方法返回五個(gè)字符串。
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;
}
字符串?dāng)?shù)組是通過調(diào)用 NewObjectArray 創(chuàng)建的,同時(shí)傳遞了 String 類和數(shù)組長(zhǎng)度兩個(gè)參數(shù)。Java String 是使用 NewStringUTF 創(chuàng)建的。String 元素是使用 SetObjectArrayElement 存入數(shù)組中的。
調(diào)試
現(xiàn)在您已經(jīng)為您的應(yīng)用程序創(chuàng)建了一個(gè)本地 DLL,但在調(diào)試時(shí)還要牢記以下幾點(diǎn)。如果使用 Java 調(diào)試器 java_g.exe,則還需要?jiǎng)?chuàng)建 DLL 的一個(gè)“調(diào)試”版本。這只是表示必須創(chuàng)建同名但帶有一個(gè) _g 后綴的 DLL 版本。就 MyNative.dll 而言,使用 java_g.exe 要求在 Windows 的 PATH 環(huán)境指定的路徑中有一個(gè) MyNative_g.dll 文件。在大多數(shù)情況下,這個(gè) DLL 可以通過將原文件重命名或復(fù)制為其名稱帶綴 _g 的文件。
現(xiàn)在,Java 調(diào)試器不允許您進(jìn)入本地代碼,但您可以在 Java 環(huán)境外使用 C 調(diào)試器(如 Microsoft Visual C++)調(diào)試本地方法。首先將源文件導(dǎo)入一個(gè)項(xiàng)目中。將編譯設(shè)置調(diào)整為在編譯時(shí)將 include 目錄包括在內(nèi):
c:\jdk1.1.6\include;c:\jdk1.1.6\include\win32
將配置設(shè)置為以調(diào)試模式編譯 DLL。在 Project Settings 中的 Debug 下,將可執(zhí)行文件設(shè)置為 java.exe(或者 java_g.exe,但要確保您生成了一個(gè) _g.dll 文件)。程序參數(shù)包括包含 main 的類名。如果在 DLL 中設(shè)置了斷點(diǎn),則當(dāng)調(diào)用本地方法時(shí),執(zhí)行將在適當(dāng)?shù)牡胤酵V埂?
下面是設(shè)置一個(gè) Visual C++ 6.0 項(xiàng)目來(lái)調(diào)試本地方法的步驟。
1. 在 Visual C++ 中創(chuàng)建一個(gè) Win32 DLL 項(xiàng)目,并將 .c 和 .h 文件添加到這個(gè)項(xiàng)目中。
2. 在 Tools 下拉式菜單的 Options 設(shè)置下設(shè)置 JDK 的 include 目錄。下面的對(duì)話框顯示了這些目錄。
3. 選擇 Build 下拉式菜單下的 Build MyNative.dll 來(lái)建立這個(gè)項(xiàng)目。確保將項(xiàng)目的活動(dòng)配置設(shè)置為調(diào)試(這通常是缺省值)。
4. 在 Project Settings 下,設(shè)置 Debug 選項(xiàng)卡來(lái)調(diào)用適當(dāng)?shù)? Java 解釋器,如下所示:
當(dāng)執(zhí)行這個(gè)程序時(shí),忽略“在 java.exe 中找不到任何調(diào)試信息”的消息。當(dāng)調(diào)用本地方法時(shí),在 C 代碼中設(shè)置的任何斷點(diǎn)將在適當(dāng)?shù)牡胤酵V? Java 程序的執(zhí)行。
其他信息
JNI 方法和 C++
上面這些示例說明了如何在 C 源文件中使用 JNI 方法。如果使用 C++,則請(qǐng)將相應(yīng)方法的格式從:
(*env)->JNIMethod( env, .... );
更改為:
env->JNIMethod( ... );
在 C++ 中,JNI 函數(shù)被看作是 JNIEnv 類的成員方法。
字符串和國(guó)家語(yǔ)言支持
本文中使用的技術(shù)用 UTF 方法來(lái)轉(zhuǎn)換字符串。使用這些方法只是為了方便起見,如果應(yīng)用程序需要國(guó)家語(yǔ)言支持 (NLS),則不能使用這些方法。有關(guān)在 Windows 和 NLS 環(huán)境中處理 Java 字符串正確方法,請(qǐng)參標(biāo)題為 NLS Strings and JNI 的一篇論文。
小結(jié)
本文提供的示例用最常用的數(shù)據(jù)類據(jù)(如 jint 和 jstring)說明了如何實(shí)現(xiàn)本地方法,并討論了 Windows 特定的幾個(gè)問題,如顯示字符串。本文提供的示例并未包括全部 JNI,JNI 還包括其他參數(shù)類型,如 jfloat、jdouble、jshort、jbyte 和 jfieldID,以及用來(lái)處理這些類型的方法。有關(guān)這個(gè)主題的詳細(xì)信息,請(qǐng)參閱 Sun Microsystems 提供的 Java 本地接口規(guī)范。