從 C/C++ 邁向 Java,再透過 JNI 回頭呼叫 C/C++,這就是連接兩個世界的關鍵橋樑。JNI (Java Native Interface) 讓 Java 程式能夠直接調用原生 C/C++ 函式庫,對於效能敏感型應用或 Java 無法直接處理的底層任務,這是一種極佳的解決方案。本文將以一個完整的「Java 呼叫 C 函數」範例,詳細記錄從編碼、編譯到執行的全流程。
假設 Java 類別為 J2C,C 來源檔為 J2C.c,互動方法命名為 write2proc。其聲明在 Java 端,實作在 C 端。完整步驟如下:
javac J2C.java)javah J2C → J2C.h)J2C.c)gcc -I... -fPIC -c J2C.c)gcc -shared ... -o libj2c.so.1.0)cp libj2c.so.1.0 libj2c.so)export LD_LIBRARY_PATH=.:$LD_LIBRARY_PATH)java J2C)import java.lang.management.ManagementFactory;
import java.lang.management.RuntimeMXBean;
public class J2C {
static {
try {
// 載入名為 "j2c" 的共享庫,對應檔案名 libj2c.so
System.loadLibrary("j2c");
} catch (UnsatisfiedLinkError e) {
System.err.println("無法載入 J2C 函式庫:\n" + e);
}
}
// 宣告 native 方法
public static native int write2proc(int pid);
public static void main(String[] args) {
// 取得目前 JVM 進程 ID
final RuntimeMXBean runtime = ManagementFactory.getRuntimeMXBean();
final String info = runtime.getName(); // 格式通常為 "pid@hostname"
final int index = info.indexOf("@");
if (index != -1) {
final int pid = Integer.parseInt(info.substring(0, index));
System.out.println("Process Info: " + info);
System.out.println("PID: " + pid);
// 呼叫 C 函數
write2proc(pid);
}
try { Thread.sleep(8000); } catch (InterruptedException e) { e.printStackTrace(); }
}
}
關鍵點: System.loadLibrary("j2c") 會自動尋找 libj2c.so (Linux) 或 j2c.dll (Windows),庫名必須與編譯產出的共享庫名稱一致。
執行 javah J2C 後產生(請勿手動修改):
/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class J2C */
#ifndef _Included_J2C
#define _Included_J2C
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class: J2C
* Method: write2proc
* Signature: (I)I
*/
JNIEXPORT jint JNICALL Java_J2C_write2proc
(JNIEnv *, jclass, jint);
#ifdef __cplusplus
}
#endif
#endif
函數簽名 Java_J2C_write2proc 遵循命名規則:Java_類別名_方法名。C 端實作必須完全符合此簽名。
#include <stdio.h>
#include "J2C.h"
JNIEXPORT jint JNICALL Java_J2C_write2proc(JNIEnv *env, jclass cls, jint pid) {
printf("C 語言端收到 PID: %d\n", pid);
return 0;
}
# 1. 編譯為目標檔,需指定 JDK 的 include 路徑 (含 jni.h 與平台相關頭檔)
gcc -I/usr/lib/jvm/java-6-openjdk-amd64/include \
-I/usr/lib/jvm/java-6-openjdk-amd64/include/linux \
-fPIC -c J2C.c -o J2C.o
# 2. 產生共享庫
gcc -shared -Wl,-soname,libj2c.so.1 -o libj2c.so.1.0 J2C.o
# 3. 建立連結供執行期載入
cp libj2c.so.1.0 libj2c.so
# 4. 設定庫搜尋路徑 (當前目錄)
export LD_LIBRARY_PATH=.:$LD_LIBRARY_PATH
# 5. 執行
java J2C
提示: 若遇到連結錯誤,可嘗試改用 g++ 編譯 C++ 相容模式。JDK 版本路徑請依實際安裝位置調整 (如 $JAVA_HOME/include)。
native 宣告、javah 產生的頭檔、C 端實作三者必須完全一致 (類別名、方法名、參數類型)。System.loadLibrary("name") 對應 libname.so (Linux) 或 name.dll (Windows),前綴 lib 由系統自動添加。LD_LIBRARY_PATH (Linux) 或 PATH (Windows) 包含共享庫所在目錄。JNI 雖然強大,但也引入了平台相依性與記憶體管理複雜度。實務上建議:僅在必要時使用 (如高效能運算、硬體控制、舊有 C/C++ 資產復用),並妥善封裝邊界、處理異常,避免 JVM 崩潰。