跳轉到內容

Java 本地介面

100% developed
來自華夏公益教科書,開放書籍,開放世界

導航 高階 主題:v  d  e )


Java 本地介面 (JNI) 使在 Java 虛擬機器 (JVM) 中執行的 Java 程式碼能夠呼叫和被其他語言(如 C、C++ 和彙編)編寫的本機應用程式(特定於硬體和作業系統平臺的程式)和庫呼叫。

JNI 可以用於

  • 實現或使用特定於平臺的功能。
  • 實現或使用標準 Java 類庫不支援的功能。
  • 使用另一種程式語言編寫的現有應用程式可供 Java 應用程式訪問。
  • 讓本機方法以與 Java 程式碼使用這些物件相同的方式使用 Java 物件(本機方法可以建立 Java 物件,然後檢查和使用這些物件來執行其任務)。
  • 讓本機方法檢查和使用由 Java 應用程式程式碼建立的物件。
  • 用於時間關鍵的計算或操作,例如解決複雜的數學方程(本機程式碼可能比 JVM 程式碼更快)。

另一方面,依賴 JNI 的應用程式會失去 Java 提供的平臺可移植性。因此,您需要為每個平臺編寫 JNI 程式碼的單獨實現,並讓 Java 在執行時檢測作業系統並載入正確的實現。許多標準庫類依賴 JNI 為開發人員和使用者提供功能(檔案 I/O、聲音功能...)。在標準庫中包含效能和平臺敏感的 API 實現允許所有 Java 應用程式以安全且與平臺無關的方式訪問此功能。只有應用程式和簽名的小程式可以呼叫 JNI。應謹慎使用 JNI。使用 JNI 時出現的細微錯誤可能會以非常難以重現和除錯的方式使整個 JVM 不穩定。錯誤檢查是必須的,否則它有可能使 JNI 端和 JVM 崩潰。

此頁面只解釋如何從 JVM 呼叫本機程式碼,而不是如何從本機程式碼呼叫 JVM。

從 JVM 呼叫本機程式碼

[編輯 | 編輯原始碼]

在 JNI 框架中,本機函式在單獨的 .c 或 .cpp 檔案中實現。C++ 提供了一個與 JNI 相比略微更簡單的介面。當 JVM 呼叫函式時,它會傳遞一個 JNIEnv 指標、一個 jobject 指標以及 Java 方法宣告的任何 Java 引數。JNI 函式可能如下所示

 JNIEXPORT void JNICALL Java_ClassName_MethodName
   (JNIEnv *env, jobject obj)
 {
     /*Implement Native Method Here*/
 }

env 指標是一個結構,其中包含與 JVM 的介面。它包含所有與 JVM 互動和處理 Java 物件所需的函式。示例 JNI 函式是將本機陣列轉換為/從 Java 陣列、將本機字串轉換為/從 Java 字串、例項化物件、丟擲異常等。基本上,Java 程式碼可以執行的任何操作都可以使用 JNIEnv 完成,儘管要容易得多。

在 Linux 和 Solaris 平臺上,如果本機程式碼將自己註冊為訊號處理程式,它可能會攔截針對 JVM 的訊號。應使用訊號連結以允許本機程式碼更好地與 JVM 互動。在 Windows 平臺上,可以使用結構化異常處理 (SEH) 將本機程式碼包裝在 SEH try/catch 塊中,以便在將中斷傳播回 JVM(即 Java 端程式碼)之前捕獲機器(CPU/FPU)生成的軟體中斷(例如 NULL 指標訪問違規和除零運算),並處理這些情況,這很可能會導致未處理的異常。

C++ 程式碼

[編輯 | 編輯原始碼]

例如,以下將 Java 字串轉換為本機字串

 extern "C"
 JNIEXPORT void JNICALL Java_ClassName_MethodName
   (JNIEnv *env, jobject obj, jstring javaString)
 {
     //Get the native string from javaString
     const char *nativeString = env->GetStringUTFChars(javaString, 0);

     //Do something with the nativeString

     //DON'T FORGET THIS LINE!!!
     env->ReleaseStringUTFChars(javaString, nativeString);
 }

JNI 框架不為本機側程式碼執行的非 JVM 記憶體資源分配提供任何自動垃圾回收。因此,本機側程式碼(例如 C、C++ 或組合語言)必須承擔顯式釋放其本身獲取的任何此類記憶體資源的責任。

C 程式碼

[編輯 | 編輯原始碼]
 JNIEXPORT void JNICALL Java_ClassName_MethodName
   (JNIEnv *env, jobject obj, jstring javaString)
 {
     /*Get the native string from javaString*/
     const char *nativeString = (*env)->GetStringUTFChars(env, javaString, 0);

     /*Do something with the nativeString*/

     /*DON'T FORGET THIS LINE!!!*/
     (*env)->ReleaseStringUTFChars(env, javaString, nativeString);
 }

需要注意的是,C++ JNI 程式碼在語法上比 C JNI 程式碼更簡潔,因為像 Java 一樣,C++ 使用物件方法呼叫語義。這意味著在 C 中,env 引數使用 (*env)-> 解引用,並且 env 必須顯式傳遞給 JNIEnv 方法。在 C++ 中,env 引數使用 env-> 解引用,並且 env 引數作為物件方法呼叫語義的一部分隱式傳遞。

Objective-C 程式碼

[編輯 | 編輯原始碼]
 JNIEXPORT void JNICALL Java_ClassName_MethodName(JNIEnv *env, jobject obj, jstring javaString)
 {
     /*DON'T FORGET THIS LINE!!!*/
     JNF_COCOA_ENTER(env);

     /*Get the native string from javaString*/
     NSString* nativeString = JNFJavaToNSString(env, javaString);

     /*Do something with the nativeString*/

     /*DON'T FORGET THIS LINE!!!*/
     JNF_COCOA_EXIT(env);
 }

JNI 還允許直接訪問彙編程式碼,甚至不需要經過 C 橋接。

型別對映

[編輯 | 編輯原始碼]

本地資料型別可以對映到/從 Java 資料型別。對於物件、陣列和字串等複合型別,原生代碼必須透過呼叫 JNIEnv 中的方法來顯式轉換資料。下表顯示了 Java (JNI) 和原生代碼之間型別的對映。

本地型別 JNI 型別 描述 型別簽名
unsigned char jboolean 無符號 8 位 Z
signed char jbyte 有符號 8 位 B
unsigned short jchar 無符號 16 位 C
short jshort 有符號 16 位 S
long jint 有符號 32 位 I

long long
__int64

jlong 有符號 64 位 J
float jfloat 32 位 F
double jdouble 64 位 D

此外,簽名 "L fully-qualified-class ;" 表示由該名稱唯一指定的類;例如,簽名 "Ljava/lang/String;" 指的是類 java.lang.String。此外,在簽名前面加上 [ 將構成該型別的陣列;例如,[I 表示 int 陣列型別。最後,void 簽名使用 V 程式碼。在這裡,這些型別可以互換。您可以在通常使用 int 的地方使用 jint,反之亦然,無需任何型別轉換。

但是,Java 字串和陣列到本地字串和陣列之間的對映不同。如果在需要 char * 的地方使用 jstring,程式碼可能會使 JVM 崩潰。

JNIEXPORT void JNICALL Java_ClassName_MethodName
        (JNIEnv *env, jobject obj, jstring javaString) {
    // printf("%s", javaString);        // INCORRECT: Could crash VM!

    // Correct way: Create and release native string from Java string
    const char *nativeString = (*env)->GetStringUTFChars(env, javaString, 0);
    printf("%s", nativeString);
    (*env)->ReleaseStringUTFChars(env, javaString, nativeString);
}

NewStringUTFGetStringUTFLengthGetStringUTFCharsReleaseStringUTFCharsGetStringUTFRegion 函式使用的編碼不是標準 UTF-8,而是經過修改的 UTF-8。空字元 (U+0000) 和大於或等於 U+10000 的程式碼點在經過修改的 UTF-8 中的編碼方式不同。許多程式實際上錯誤地使用了這些函式,並將返回或傳遞給函式的 UTF-8 字串視為標準 UTF-8 字串,而不是經過修改的 UTF-8 字串。程式應該使用 NewStringGetStringLengthGetStringCharsReleaseStringCharsGetStringRegionGetStringCriticalReleaseStringCritical 函式,這些函式在小端架構上使用 UTF-16LE 編碼,在大端架構上使用 UTF-16BE 編碼,然後使用 UTF-16 到標準 UTF-8 的轉換例程。

程式碼與 Java 陣列類似,如下面的示例所示,該示例計算陣列中所有元素的總和。

JNIEXPORT jint JNICALL Java_IntArray_sumArray
        (JNIEnv *env, jobject obj, jintArray arr) {
    jint buf[10];
    jint i, sum = 0;
    // This line is necessary, since Java arrays are not guaranteed
    // to have a continuous memory layout like C arrays.
    env->GetIntArrayRegion(arr, 0, 10, buf);
    for (i = 0; i < 10; i++) {
        sum += buf[i];
    }
    return sum;
}

當然,它遠不止這些。

JNI 環境指標 (JNIEnv*) 作為引數傳遞給每個對映到 Java 方法的本地函式,允許在本地方法中與 JNI 環境進行互動。這個 JNI 介面指標可以儲存,但只在當前執行緒中有效。其他執行緒必須首先呼叫 AttachCurrentThread() 將自身附加到 VM 並獲取 JNI 介面指標。附加後,本地執行緒就像在本地方法中執行的普通 Java 執行緒一樣。本地執行緒保持附加到 VM,直到它呼叫 DetachCurrentThread() 將自身分離。

要附加到當前執行緒並獲取 JNI 介面指標

JNIEnv *env;
(*g_vm)->AttachCurrentThread (g_vm, (void **) &env, NULL);

要從當前執行緒分離

(*g_vm)->DetachCurrentThread (g_vm);

HelloWorld

[編輯 | 編輯原始碼]
Computer code 程式碼清單 10.1:HelloWorld.java
public class HelloWorld {
 private native void print();

 public static void main(String[] args) {
  new HelloWorld().print();
 }

 static {
  System.loadLibrary("HelloWorld");
 }
}

HelloWorld.h

/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class HelloWorld */

#ifndef _Included_HelloWorld
#define _Included_HelloWorld
#ifdef __cplusplus
extern "C" {
#endif
/*
 * Class:     HelloWorld
 * Method:    print
 * Signature: ()V
 */
JNIEXPORT void JNICALL Java_HelloWorld_print
  (JNIEnv *, jobject);

#ifdef __cplusplus
}
#endif
#endif

libHelloWorld.c

 #include <stdio.h>
 #include "HelloWorld.h"

 JNIEXPORT void JNICALL
 Java_HelloWorld_print(JNIEnv *env, jobject obj)
 {
     printf("Hello World!\n");
     return;
 }

make.sh

#!/bin/sh

# openbsd 4.9
# gcc 4.2.1
# openjdk 1.7.0
JAVA_HOME=$(readlink -f /usr/bin/javac | sed "s:bin/javac::")
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:.
javac HelloWorld.java
javah HelloWorld
gcc -I${JAVA_HOME}/include -shared libHelloWorld.c -o libHelloWorld.so
java HelloWorld
Computer code 在 POSIX 上執行的命令
chmod +x make.sh
./make.sh

高階用法

[編輯 | 編輯原始碼]

原生代碼不僅可以與 Java 互動,還可以利用 Java API:java.awt.Canvas,這可以透過 Java AWT 本地介面實現。該過程幾乎相同,只是稍有不同。Java AWT 本地介面僅從 J2SE 1.3 開始可用。


華夏公益教科書