用 JNI 進行 Java 編程(2)

從 Java 程序調用 C/C++ 代碼

概述 java

當沒法用 Java 語言編寫整個應用程序時,JNI 容許您使用本機代碼。在下列典型狀況下,您可能決定使用本機代碼: linux

  • 但願用更低級、更快的編程語言去實現對時間有嚴格要求的代碼。

  • 但願從 Java 程序訪問舊代碼或代碼庫。

  • 須要標準 Java 類庫中不支持的依賴於平臺的特性。

 

從 Java 代碼調用 C/C++ 的六個步驟

從 Java 程序調用 C 或 C ++ 代碼的過程由六個步驟組成。 咱們將在下面幾頁中深刻討論每一個步驟,但仍是先讓咱們迅速地瀏覽一下它們。 shell


  1. 編寫 Java 代碼。咱們將從編寫 Java 類開始,這些類執行三個任務:聲明將要調用的本機方法;裝入包含本機代碼的共享庫;而後調用該本機方法。

  2. 編譯 Java 代碼。在使用 Java 類以前,必須成功地將它們編譯成字節碼。

  3. 建立 C/C++ 頭文件。C/C++ 頭文件將聲明想要調用的本機函數說明。而後,這個頭文件與 C/C++ 函數實現(請參閱步驟 4)一塊兒來建立共享庫(請參閱步驟 5)。

  4. 編寫 C/C++ 代碼。這一步實現 C 或 C++ 源代碼文件中的函數。C/C++ 源文件必須包含步驟 3 中建立的頭文件。

  5. 建立共享庫文件。從步驟 4 中建立的 C 源代碼文件來建立共享庫文件。

  6. 運行 Java 程序。運行該代碼,並查看它是否有用。咱們還將討論一些用於解決常見錯誤的技巧。

 

步驟 1:編寫 Java 代碼

咱們從編寫 Java 源代碼文件開始,它將聲明本機方法(或方法),裝入包含本機代碼的共享庫,而後實際調用本機方法。 編程

這裏是名爲 Sample1.java 的 Java 源代碼文件的示例: jvm

public class Sample1
{
   public native int intMethod(int n);
   public native boolean booleanMethod(boolean bool);
   public native String stringMethod(String text);
   public native int intArrayMethod(int[] intArray);

   public static void main(String[] args)
   {
     System.loadLibrary("Sample1");
     Sample1 sample = new Sample1();
     int     square = sample.intMethod(5);
     boolean bool   = sample.booleanMethod(true);
     String  text   = sample.stringMethod("JAVA");
     int     sum    = sample.intArrayMethod(
                         new int[]{1,1,2,3,5,8,13} );

     System.out.println("intMethod: " + square);
     System.out.println("booleanMethod: " + bool);
     System.out.println("stringMethod: " + text);
     System.out.println("intArrayMethod: " + sum);
   }
}


這段代碼作了些什麼?

首先,請注意對 native 關鍵字的使用,它只能隨方法一塊兒使用。native 關鍵字告訴 Java 編譯器:方法是用 Java 類以外的本機代碼實現的,但其聲明卻在 Java 中。只能在 Java 類中聲明本機方法,而不能實現它,因此本機方法不能擁有方法主體。 編程語言

如今,讓咱們逐行研究一下代碼: 函數

  • 從第 3 行到第 6 行,咱們聲明瞭四個 native 方法。

  • 在第 10 行,咱們裝入了包含這些本機方法的實現的共享庫文件。(到步驟 5 時,咱們將建立該共享庫文件。)

  • 最終,從第 12 行到第 15 行,咱們調用了本機方法。注:這個操做和調用非本機 Java 方法的操做沒有差別。

:基於 UNIX 的平臺上的共享庫文件一般含有前綴「lib」。在本例中,第 10 行多是 System.loadLibrary("libSample1"); 。請必定要注意您在步驟 5:建立共享庫文件中生成的共享庫文件名。 工具

 

Jerikc:
        Java不是完美的,Java的不足除了體如今運行速度上要比傳統的C++慢以外,Java沒法直接訪問到操做系統底層(如系統硬件等),爲此Java使用native方法來擴展Java程序的功能。
        能夠將native方法比做Java程序同C/C++程序的接口。

步驟 2:編譯 Java 代碼

接下來,咱們須要將 Java 代碼編譯成字節碼。完成這一步的方法之一是使用隨 SDK 一塊兒提供的 Java 編譯器 javac。 用來將 Java 代碼編譯成字節碼的命令是: spa

javac Sample1.java


步驟 3:建立 C/C++ 頭文件

第三步是建立 C/C++ 頭文件,它定義本機函數說明。完成這一步的方法之一是使用 javah.exe,它是隨 SDK 一塊兒提供的本機方法 C 存根生成器工具。這個工具被設計成用來建立頭文件,該頭文件爲在 Java 源代碼文件中所找到的每一個 native 方法定義 C 風格的函數。這裏使用的命令是: 操作系統

javah Sample1


Sample1.java 上運行 javah.exe 的結果

下面的 Sample1.h 是對咱們的 Java 代碼運行 javah 工具所生成的 C/C++ 頭文件:

/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class Sample1 */
 
#ifndef _Included_Sample1
#define _Included_Sample1
#ifdef __cplusplus
extern "C" {
#endif

JNIEXPORT jint JNICALL Java_Sample1_intMethod
  (JNIEnv *, jobject, jint);

JNIEXPORT jboolean JNICALL Java_Sample1_booleanMethod
  (JNIEnv *, jobject, jboolean);
 
JNIEXPORT jstring JNICALL Java_Sample1_stringMethod
  (JNIEnv *, jobject, jstring);

JNIEXPORT jint JNICALL Java_Sample1_intArrayMethod
  (JNIEnv *, jobject, jintArray);
 
#ifdef __cplusplus
}
#endif
#endif


關於 C/C++ 頭文件

正如您可能已經注意到的那樣,Sample1.h 中的 C/C++ 函數說明和 Sample1.java 中的 Java native 方法聲明有很大差別。JNIEXPORTJNICALL 是用於導出函數的、依賴於編譯器的指示符。返回類型是映射到 Java 類型的 C/C++ 類型。附錄 A:JNI 類型中完整地說明了這些類型。

除了 Java 聲明中的通常參數之外,全部這些函數的參數表中都有一個指向 JNIEnvjobject 的指針。指向 JNIEnv 的指針其實是一個指向函數指針表的指針。正如將要在步驟 4 中看到的,這些函數提供各類用來在 C 和 C++ 中操做 Java 數據的能力。

jobject 參數引用當前對象。所以,若是 C 或 C++ 代碼須要引用 Java 函數,則這個 jobject 充當引用或指針,返回調用的 Java 對象。函數名自己是由前綴「Java_」加全限定類名,再加下劃線和方法名構成的。

 

步驟 4:編寫 C/C++ 代碼

當談到編寫 C/C++ 函數實現時,有一點須要牢記:說明必須和 Sample1.h 的函數聲明徹底同樣。咱們將研究用於 C 實現和 C++ 實現的完整代碼,而後討論二者之間的差別。

 

C 函數實現

如下是 Sample1.c,它是用 C 編寫的實現:

#include "Sample1.h"
#include <string.h>
 
JNIEXPORT jint JNICALL Java_Sample1_intMethod
  (JNIEnv *env, jobject obj, jint num) {
    return num * num;
}
 
JNIEXPORT jboolean JNICALL Java_Sample1_booleanMethod
  (JNIEnv *env, jobject obj, jboolean boolean) {
   return !boolean;
}

JNIEXPORT jstring JNICALL Java_Sample1_stringMethod
  (JNIEnv *env, jobject obj, jstring string) {
    const char *str = (*env)->GetStringUTFChars(env, string, 0);        
    char cap[128];
    strcpy(cap, str);
    (*env)->ReleaseStringUTFChars(env, string, str);
    return (*env)->NewStringUTF(env, strupr(cap));
}
 
JNIEXPORT jint JNICALL Java_Sample1_intArrayMethod
   (JNIEnv *env, jobject obj, jintArray array) {
   int i, sum = 0;
   jsize len = (*env)->GetArrayLength(env, array);
   jint *body = (*env)->GetIntArrayElements(env, array, 0);
   for (i=0; i<len; i++)
   {	sum += body[i];    }
   (*env)->ReleaseIntArrayElements(env, array, body, 0);
   return sum;
}
 
void main(){}


C++ 函數實現

如下是 Sample1.cpp(C++ 實現)

1. #include "Sample1.h"
 2. #include <string.h>
 3.
 4.JNIEXPORT jint JNICALL Java_Sample1_intMethod
 5.  (JNIEnv *env, jobject obj, jint num) {
 6.   return num * num;
 7. }
 8.
 9. JNIEXPORT jboolean JNICALL Java_Sample1_booleanMethod
10.   (JNIEnv *env, jobject obj, jboolean boolean) {
11.   return !boolean;
12. }
13.
14. JNIEXPORT jstring JNICALL Java_Sample1_stringMethod
15.   (JNIEnv *env, jobject obj, jstring string) {
16.     const char *str = env->GetStringUTFChars(string, 0);
17.     char cap[128];
18.     strcpy(cap, str);
19.     env->ReleaseStringUTFChars(string, str);
20.     return env->NewStringUTF(strupr(cap));
21. }
22. 
23. JNIEXPORT jint JNICALL Java_Sample1_intArrayMethod
24.   (JNIEnv *env, jobject obj, jintArray array) {
25.     int i, sum = 0;
26.     jsize len = env->GetArrayLength(array);
27.     jint *body = env->GetIntArrayElements(array, 0);
28.     for (i=0; i<len; i++)
29.     {	sum += body[i];
30.     }
31.     env->ReleaseIntArrayElements(array, body, 0);
32.     return sum;
33. }
34.
35. void main(){}


C 和 C++ 函數實現的比較

C 和 C++ 代碼幾乎相同;惟一的差別在於用來訪問 JNI 函數的方法。在 C 中,JNI 函數調用由「(*env)->」做前綴,目的是爲了取出函數指針所引用的值。在 C++ 中,JNIEnv 類擁有處理函數指針查找的內聯成員函數。下面將說明這個細微的差別,其中,這兩行代碼訪問同一函數,但每種語言都有各自的語法。

C 語法: 	jsize len = (*env)->GetArrayLength(env,array);
C++ 語法: 	jsize len =env->GetArrayLength(array);




步驟 5:建立共享庫文件

接下來,咱們建立包含本機代碼的共享庫文件。大多數 C 和 C++ 編譯器除了能夠建立機器代碼可執行文件之外,也能夠建立共享庫文件。用來建立共享庫文件的命令取決於您使用的編譯器。下面是在 Windows 和 Solaris 系統上執行的命令。

Windows: 	cl -Ic:\jdk\include -Ic:\jdk\include\win32 -LD Sample1.c -FeSample1.dll
Solaris: 	cc -G -I/usr/local/jdk/include -I/user/local/jdk/include/solaris Sample1.c -o 
                  Sample1.so



Jerikc:
     Ubuntu Linux :
gcc -shared -I /usr/lib/jvm/java-6-sun-1.6.0.20/include -I /usr/lib/jvm/java-6-sun-1.6.0.20/include/linux -I /usr/include Sample1.c -o Sample1.so
解釋一下上面的操做,固然個人系統是Ubuntu10.04,不過關於jdk安裝以後的路徑應該大同小異,本身作下適當的調整便可
/usr/lib/jvm/java-6-sun-1.6.0.20/include下包含的jni.h
/usr/lib/jvm/java-6-sun-1.6.0.20/include/linux下包含的是jni-mid.h
-shared 是設置生成share object的標誌
libtest.so 即爲咱們須要的動態連接庫(等同於Windows下的*.dll文件)


步驟 6:運行 Java 程序

最後一步是運行 Java 程序,並確保代碼正確工做。由於必須在 Java 虛擬機中執行全部 Java 代碼,因此須要使用 Java 運行時環境。完成這一步的方法之一是使用 java,它是隨 SDK 一塊兒提供的 Java 解釋器。所使用的命令是:

java Sample1

當運行 Sample1.class 程序時,應該得到下列結果:

PROMPT>java Sample1
intMethod: 25
booleanMethod: false
stringMethod: JAVA
intArrayMethod: 33

PROMPT>


故障排除

當使用 JNI 從 Java 程序訪問本機代碼時,您會遇到許多問題。您會遇到的三個最多見的錯誤是:

  • 沒法找到動態連接。它所產生的錯誤消息是:java.lang.UnsatisfiedLinkError。這一般指沒法找到共享庫,或者沒法找到共享庫內特定的本機方法。

  • 沒法找到共享庫文件。當用 System.loadLibrary(String libname) 方法(參數是文件名)裝入庫文件時,請確保文件名拼寫正確以及沒有指定擴展名。還有,確保庫文件的位置在類路徑中,從而確保 JVM 能夠訪問該庫文件。

  • 沒法找到具備指定說明的方法。確保您的 C/C++ 函數實現擁有與頭文件中的函數說明相同的說明。

 

結束語

從 Java 調用 C 或 C++ 本機代碼(雖然不簡單)是 Java 平臺中一種良好集成的功能。雖然 JNI 支持 C 和 C++,但 C++ 接口更清晰一些而且一般比 C 接口更可取。

正如您已經看到的,調用 C 或 C++ 本機代碼須要賦予函數特殊的名稱,並建立共享庫文件。當利用現有代碼庫時,更改代碼一般是不可取的。要避免這一點,在 C++ 中,一般建立代理代碼或代理類,它們有專門的 JNI 所需的命名函數。而後,這些函數能夠調用底層庫函數,這些庫函數的說明和實現保持不變。

相關文章
相關標籤/搜索