Java 本地介面
| 導航 高階 主題: |
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。
在 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 指標訪問違規和除零運算),並處理這些情況,這很可能會導致未處理的異常。
例如,以下將 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++ 或組合語言)必須承擔顯式釋放其本身獲取的任何此類記憶體資源的責任。
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 引數作為物件方法呼叫語義的一部分隱式傳遞。
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 |
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);
}
NewStringUTF、GetStringUTFLength、GetStringUTFChars、ReleaseStringUTFChars、GetStringUTFRegion 函式使用的編碼不是標準 UTF-8,而是經過修改的 UTF-8。空字元 (U+0000) 和大於或等於 U+10000 的程式碼點在經過修改的 UTF-8 中的編碼方式不同。許多程式實際上錯誤地使用了這些函式,並將返回或傳遞給函式的 UTF-8 字串視為標準 UTF-8 字串,而不是經過修改的 UTF-8 字串。程式應該使用 NewString、GetStringLength、GetStringChars、ReleaseStringChars、GetStringRegion、GetStringCritical 和 ReleaseStringCritical 函式,這些函式在小端架構上使用 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);
程式碼清單 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
在 POSIX 上執行的命令
chmod +x make.sh ./make.sh |
原生代碼不僅可以與 Java 互動,還可以利用
Java API:java.awt.Canvas,這可以透過 Java AWT 本地介面實現。該過程幾乎相同,只是稍有不同。Java AWT 本地介面僅從 J2SE 1.3 開始可用。