Java 「一次編寫,到處運行」的跨平台特性,源於 JVM 屏蔽了作業系統差異。但這也意味著 Java 無法直接訪問硬體、呼叫作業系統原生 API、使用高效能 C/C++ 庫、整合舊有 C 代碼庫。**Java Native Interface (JNI)** 打破了這層屏障,允許 Java 代碼調用本地共享庫 (.dll Windows / .so Linux/Unix / .dylib macOS) 中的函數。本文完整演示 JNI 開發全流程:Java 宣告 → javah 生成頭文件 → C/C++ 實現 → 編譯鏈接 → Java 載入調用,並解析關鍵資料類型映射、記憶體管理、異常處理、效能優化與現代替代方案。
native 方法,System.loadLibrary 載入庫。javac 編譯 Java 源碼生成 .class。javah -jni ClassName (JDK 8-) 或 javac -h . ClassName.java (JDK 10+) 生成 C/C++ 頭文件 .h。.c/.cpp),包含頭文件,實現 JNIEXPORT 函數。.dll/.so/.dylib)。java.library.path (或 -Djava.library.path=...),Java 運行調用。
public class Counter {
// 靜態載入庫 (無副檔名,平台自動匹配)
static {
System.loadLibrary("counter"); // Windows: counter.dll, Linux: libcounter.so
}
// 宣告 native 方法 (僅宣告,無實現)
public native static void reset();
public native static int get();
public native static void increment();
public native static void add(int value);
// 測試主程式
public static void main(String[] args) {
reset();
System.out.println("Initial: " + get()); // 0
increment();
System.out.println("After inc: " + get()); // 1
add(10);
System.out.println("After add 10: " + get()); // 11
}
}
# JDK 8 及之前
javac Counter.java
javah -jni Counter
# JDK 10+ (javah 已移除,javac 內建支援)
javac -h . Counter.java
生成 Counter.h (關鍵片段):
/* DO NOT EDIT - machine generated */
#include
#ifndef _Included_Counter
#define _Included_Counter
#ifdef __cplusplus
extern "C" {
#endif
JNIEXPORT void JNICALL Java_Counter_reset
(JNIEnv *, jclass);
JNIEXPORT jint JNICALL Java_Counter_get
(JNIEnv *, jclass);
JNIEXPORT void JNICALL Java_Counter_increment
(JNIEnv *, jclass);
JNIEXPORT void JNICALL Java_Counter_add
(JNIEnv *, jclass, jint);
#ifdef __cplusplus
}
#endif
#endif
#include "Counter.h"
#include
static int counter = 0; // 本地靜態變數,生命週期 = 庫載入期間
JNIEXPORT void JNICALL Java_Counter_reset(JNIEnv *env, jclass cls) {
counter = 0;
printf("[Native] Counter reset to 0\n");
}
JNIEXPORT jint JNICALL Java_Counter_get(JNIEnv *env, jclass cls) {
return (jint)counter;
}
JNIEXPORT void JNICALL Java_Counter_increment(JNIEnv *env, jclass cls) {
counter++;
printf("[Native] Counter incremented to %d\n", counter);
}
JNIEXPORT void JNICALL Java_Counter_add(JNIEnv *env, jclass cls, jint value) {
counter += value;
printf("[Native] Counter added %d, now %d\n", value, counter);
}
| 平台 | 編譯命令 (GCC/Clang/MSVC) | 輸出檔案 | |
|---|---|---|---|
| Linux/macOS | gcc -shared -fPIC -I${JAVA_HOME}/include -I${JAVA_HOME}/include/linux Counter.c -o libcounter.so | libcounter.so | |
| Windows (MinGW) | gcc -shared -I"%JAVA_HOME%\include" -I"%JAVA_HOME%\include\win32" Counter.c -o counter.dll | counter.dll | |
| Windows (MSVC) | cl /LD /I"%JAVA_HOME%\include" /I"%JAVA_HOME%\include\win32" Counter.c /Fecounter.dll | counter.dll |
| Java 類型 | JNI 類型 | C/C++ 類型 | 說明 |
|---|---|---|---|
| boolean | jboolean | unsigned char (8-bit) | JNI_TRUE/JNI_FALSE |
| byte | jbyte | signed char (8-bit) | |
| char | jchar | unsigned short (16-bit) | Unicode 碼單元 |
| short | jshort | short (16-bit) | |
| int | jint | int (32-bit) | |
| long | jlong | long long / int64_t (64-bit) | |
| float | jfloat | float (32-bit) | |
| double | jdouble | double (64-bit) | |
| Object | jobject | _jobject* | 所有物件統一類型 |
| Class | jclass | _jclass* | |
| String | jstring | _jstring* | 需轉換才能訪問內容 |
| int[] | jintArray | _jintArray* | 陣列類型各異 |
| Throwable | jthrowable | _jthrowable* |
// Java String → C char* (UTF-8)
const char *cstr = (*env)->GetStringUTFChars(env, jstr, NULL);
// 使用 cstr...
(*env)->ReleaseStringUTFChars(env, jstr, cstr); // 必須釋放!
// Java String → C jchar* (UTF-16)
const jchar *uchars = (*env)->GetStringChars(env, jstr, NULL);
jsize len = (*env)->GetStringLength(env, jstr);
// 使用 uchars[0..len-1]...
(*env)->ReleaseStringChars(env, jstr, uchars);
// C char* (UTF-8) → Java String
jstring jstr = (*env)->NewStringUTF(env, "Hello JNI");
// C jchar* (UTF-16) → Java String
jstring jstr = (*env)->NewString(env, uchars, len);
警告:GetStringUTFChars 返回的指針可能指向 JVM 內部緩衝區或複製,**必須配對 Release**,否則記憶體洩漏/崩潰。
// 獲取陣列元素指針 (可能複製)
jint *arr = (*env)->GetIntArrayElements(env, jarr, NULL);
jsize len = (*env)->GetArrayLength(env, jarr);
// 修改 arr[0..len-1]...
// 模式: 0=複製回去並釋放, JNI_COMMIT=複製回去不釋放, JNI_ABORT=不複製直接釋放
(*env)->ReleaseIntArrayElements(env, jarr, arr, 0);
// 獲取直接指針 (無複製,高效能,但可能阻塞 GC 移動物件) - 謹慎使用
jint *arr = (*env)->GetPrimitiveArrayCritical(env, jarr, NULL);
// ... 短時間內快速讀寫 ...
(*env)->ReleasePrimitiveArrayCritical(env, jarr, arr, 0);
// 獲取類別
jclass cls = (*env)->GetObjectClass(env, obj); // 實例
// 或 jclass cls = (*env)->FindClass(env, "java/lang/String"); // 靜態
// 獲取欄位 ID
jfieldID fid = (*env)->GetFieldID(env, cls, "fieldName", "I"); // 簽名 "I"=int
// 讀寫 int 欄位
jint val = (*env)->GetIntField(env, obj, fid);
(*env)->SetIntField(env, obj, fid, newVal);
// 調用方法
jmethodID mid = (*env)->GetMethodID(env, cls, "methodName", "(I)V"); // 簽名
(*env)->CallVoidMethod(env, obj, mid, arg);
// 靜態方法
jmethodID smid = (*env)->GetStaticMethodID(env, cls, "staticMethod", "()Ljava/lang/String;");
jstring result = (jstring)(*env)->CallStaticObjectMethod(env, cls, smid);
簽名格式:()V 無參無返回,(I)V 一個 int 參數無返回,(ILjava/lang/String;)Z int+String 參數返回 boolean。用 javap -s ClassName 查看。
// 拋出 Java 異常
(*env)->ThrowNew(env, (*env)->FindClass(env, "java/lang/IllegalArgumentException"), "Invalid value from native");
// 檢查/清除異常
if ((*env)->ExceptionCheck(env)) {
(*env)->ExceptionDescribe(env); // 列印堆疊
(*env)->ExceptionClear(env); // 清除,繼續執行
}
ByteBuffer.allocateDirect() + GetDirectBufferAddress 實現零拷貝共享記憶體。System.loadLibrary 載入延遲,靜態連結 (GraalVM Native Image) 消除啟動開銷。close()/release() native 方法,配合 try-with-resources 或 Cleaner 確保釋放。| 方案 | 特點 | 適用場景 |
|---|---|---|
| JNA (Java Native Access) | 純 Java,無需編譯 C 代碼,反射調用本地庫,效能略低於 JNI | 快速整合現有 .dll/.so、原型驗證、跨平台部署簡化 |
| JNR-FFI | JNA 衍生,更現代 API、更好效能、支援更多類型 | 高效能本地互操作、Netty、JRuby 等專案採用 |
| GraalVM Native Image | AOT 編譯 Java 為原生可執行檔,本地互調零開銷 | 微服務、CLI 工具、Serverless、極致啟動速度 |
| Project Panama (Foreign Function & Memory API) | JDK 19+ 預覽/21+ 正式,純 Java 安全訪問本地記憶體/函數 | **未來標準**,替代 JNI/JNA,無 unsafe、無編譯、記憶體安全 |