Windows 中實現 Java 本地方法

本文爲在 32 位 Windows 平臺上實現 Java 本地方法提供了實用的示例、步驟和準則。這些示例包括傳遞和返回常用的數據類型。

本文中的示例使用 Sun Microsystems 公司創建的 Java Development Kit (JDK) 版本 1.1.6 和 Java 本地接口 (JNI) 規範。用 C 語言編寫的本地代碼是用 Microsoft Visual C++ 編譯器編譯生成的。

簡介

本文提供調用本地 C 代碼的 Java 代碼示例,包括傳遞和返回某些常用的數據類型。本地方法包含在特定於平臺的可執行文件中。就本文中的示例而言,本地方法包含在 Windows 32 位動態鏈接庫 (DLL) 中。

不過我要提醒您,對 Java 外部的調用通常不能移植到其他平臺上,在 applet 中還可能引發安全異常。實現本地代碼將使您的 Java 應用程序無法通過 100% 純 Java 測試。但是,如果必須執行本地調用,則要考慮幾個準則:

  1. 將您的所有本地方法都封裝在單個類中,這個類調用單個 DLL。對於每種目標操作系統,都可以用特定於適當平臺的版本替換這個 DLL。這樣就可以將本地代碼的影響減至最小,並有助於將以後所需的移植問題包含在內。
  2. 本地方法要簡單。儘量將您的 DLL 對任何第三方(包括 Microsoft)運行時 DLL 的依賴減到最小。使您的本地方法儘量獨立,以將加載您的 DLL 和應用程序所需的開銷減到最小。如果需要運行時 DLL,必須隨應用程序一起提供它們。

Java 調用 C

對於調用 C 函數的 Java 方法,必須在 Java 類中聲明一個本地方法。在本部分的所有示例中,我們將創建一個名爲 MyNative 的類,並逐步在其中加入新的功能。這強調了一種思想,即將本地方法集中在單個類中,以便將以後所需的移植工作減到最少。

示例 1 -- 傳遞參數

在第一個示例中,我們將三個常用參數類型傳遞給本地函數:Stringintboolean。本例說明在本地 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 子句加載包含本地方法實現的 DLL。

下一步是生成 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 和變量類型(包括jobjectjstringjintjboolean,等等)的原型和其他聲明。

本地方法是在文件 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 的一篇論文。但是,jbooleanjint 值可以直接使用。

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;
}

再次請注意,jintint 值是可互換的。使用相同的編譯語句重新編譯這個 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 的類型創建的。基本類型有它們自已的數組類型,如 jintArrayjcharArray。非基本類型的數組使用 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 以使 showStringsgetStrings 的輸出顯示出來:

 
  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 項目來調試本地方法的步驟。

  1. 在 Visual C++ 中創建一個 Win32 DLL 項目,並將 .c 和 .h 文件添加到這個項目中。

  2. 在 Tools 下拉式菜單的 Options 設置下設置 JDK 的 include 目錄。下面的對話框顯示了這些目錄。

  3. 選擇 Build 下拉式菜單下的 Build MyNative.dll 來建立這個項目。確保將項目的活動配置設置爲調試(這通常是缺省值)。
  4. 在 Project Settings 下,設置 Debug 選項卡來調用適當的 Java 解釋器,如下所示:

當執行這個程序時,忽略“在 java.exe 中找不到任何調試信息”的消息。當調用本地方法時,在 C 代碼中設置的任何斷點將在適當的地方停止 Java 程序的執行。

其他信息

JNI 方法和 C++

上面這些示例說明了如何在 C 源文件中使用 JNI 方法。如果使用 C++,則請將相應方法的格式從:


 (*env)-&gt;JNIMethod( env, .... ); 

更改爲:


 env-&gt;JNIMethod( ... ); 

在 C++ 中,JNI 函數被看作是 JNIEnv 類的成員方法。

字符串和國家語言支持

本文中使用的技術用 UTF 方法來轉換字符串。使用這些方法只是爲了方便起見,如果應用程序需要國家語言支持 (NLS),則不能使用這些方法。有關在 Windows 和 NLS 環境中處理 Java 字符串正確方法,請參標題爲 NLS Strings and JNI 的一篇論文。

小結

本文提供的示例用最常用的數據類據(如 jintjstring)說明了如何實現本地方法,並討論了 Windows 特定的幾個問題,如顯示字符串。本文提供的示例並未包括全部 JNI,JNI 還包括其他參數類型,如 jfloatjdoublejshortjbytejfieldID,以及用來處理這些類型的方法。有關這個主題的詳細信息,請參閱 Sun Microsystems 提供的 Java 本地接口規範

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章