Java JNI 學習筆記
JNI(Java Native Interface)是 Java 提供的一種介面,使得 java 程式碼可以與其他語言(如 C 和 C++)編寫的程式碼進行互動。具體來說,JNI 允許你在 Java 中呼叫本地(Native)程式碼,或者從原生代碼呼叫 Java 方法。
基本概念
jni.h
:這是 JNI 的標頭檔案,使用javac
生成,定義了 JNI 的介面函式和資料結構jni.cpp
:這是實現了本地方法的 C++ 檔案,包含了具體的原生代碼jni.so
:這是生成的共享庫檔案(在 windows 上為.dll
檔案),java 程式透過它呼叫本地方法
Demo:java 呼叫 C++ 程式碼
編寫 java 類
建立一個 Java 類並宣告一個本地方法。
// HelloJNI.java
public class HelloJNI {
// 載入本地庫
static {
System.loadLibrary("hello"); // Load native library at runtime
// hello.dll (Windows) or libhello.so (Unixes)
}
// 宣告一個本地方法
private native void sayHello();
// 主方法
public static void main(String[] args) {
new HelloJNI().sayHello(); // 呼叫本地方法
}
}
上面程式碼的靜態程式碼塊在這個類被類載入器載入的時候呼叫了 System.loadLibrary
庫來載入一個 native 庫 “hello”,這個庫實現了 sayHello
函式。接下來,我們使用 native 關鍵字將 sayHello()
方法宣告為本地例項方法。注意,一個 native 方法不包含方法體,只有宣告。上面程式碼中的 main 方法例項化了一個 HelloJJNI 類的例項,然後呼叫了本地方法 sayHello()
。
生成 C++ 標頭檔案
編譯 Java 類並使用 javac
生成 JNI 所需要的 C++ 標頭檔案。
# 編譯 Java 類並生成 C++ 標頭檔案
javac -h . HelloJNI.java
此時會生成一個名為 HelloJNI.h
的標頭檔案:
/* DO NOT EDIT THIS FILE - it is machine generated */
# include <jni.h>
/* Header for class HelloJNI */
# ifndef _Included_HelloJNI
# define _Included_HelloJNI
# ifdef __cplusplus
extern "C" {
# endif
/*
* Class: HelloJNI
* Method: sayHello
* Signature: ()V
*/
JNIEXPORT void JNICALL Java_HelloJNI_sayHello(JNIEnv *, jobject);
# ifdef __cplusplus
}
# endif
# endif
上面的標頭檔案生成了一個 Java_HelloJNI_sayHello
的 C 函式:
JNIEXPORT void JNICALL Java_HelloJNI_sayHello(JNIEnv *, jobject);
將 java 的 native 方法轉換成 C 函式宣告的規則是這樣的:Java_{package_and_classname}_{function_name}(JNI arguments)
。包名中的點換成單下劃線。需要說明的是生成函式中的兩個引數:
JNIEnv *
:這是一個指向 JNI 執行環境的指標,我們可以透過這個指標訪問 JNI 函式jobject
:指代 java 中的 this 物件
標頭檔案中有一個 extern “C”,同時上面還有 C++ 的條件編譯語句,這裡的函式宣告是要告訴 C++ 編譯器:這個函式是 C 函式,請使用 C 函式的簽名協議規則去編譯!因為我們知道 C++ 的函式簽名協議規則和 C 的是不一樣的,因為 C++ 支援重寫和過載等物件導向的函式語法。
編寫 C++ 實現
建立一個 C++ 檔案,實現標頭檔案中宣告的本地方法。
// HelloJNI.cpp
# include <jni.h>
# include <iostream>
# include "HelloJNI.h"
// 實現本地方法:注意函式名稱需要和標頭檔案中的函式名稱保持一致
JNIEXPORT void JNICALL Java_HelloJNI_sayHello(JNIEnv *env, jobject obj) {
std::cout << "Hello from C++!" << std::endl;
}
編譯 C++ 程式碼生成共享庫
將 C++ 檔案編譯為共享庫檔案:
# Linux/macOS
g++ -shared -fpic -o libhello.so -I${JAVA_HOME}/include -I${JAVA_HOME}/include/linux HelloJNI.cpp
g++ -shared -fpic -o libllm_jni.so -I${JAVA_HOME}/include -I${JAVA_HOME}/include/linux llm_jni.cpp
# Windows
g++ -shared -o hello.dll -I"%JAVA_HOME%\include" -I"%JAVA_HOME%\include\win32" HelloJNI.cpp
執行 Java 程式
確保共享庫檔案位於 Java 的庫路徑中,然後執行 java 程式:
# 執行 Java 程式
java -Djava.library.path=. -cp . HelloJNI
# -Djava.library.path=. : 指定 java 查詢本地庫的路徑
# -cp . :設定 java 的類路徑(class path),即查詢 java 類檔案的路徑
執行上述命令後,你應該會看到輸出:
Hello from C++!
在 java 和 Native 程式碼之間傳遞引數和返回值
傳遞基本型別
傳遞 java 的基本型別是非常簡單而直接的,一個 jxxx 之類的型別已經定義在本地系統中了,比如:jint,jbyte,jshort,jlong,jfloat,jdouble,jchar 和 jboolean 分別對應 java 的 int,byte,short,long,float,double,char 和 boolean 基本型別。
Java JNI 程式:
public class TestJNIPrimitive {
static {
System.loadLibrary("myjni"); // myjni.dll (Windows) or libmyjni.so (Unixes)
}
// Declare a native method average() that receives two ints and return a double containing the average
private native double average(int n1, int n2);
// Test Driver
public static void main(String args[]) {
System.out.println("In Java, the average is " + new TestJNIPrimitive().average(3, 2));
}
}
生成標頭檔案:javac -h . TestJNIPrimitive.java
標頭檔案 TestJNIPrimitive.h 中包含了一個函式宣告:
JNIEXPORT jdouble JNICALL Java_TestJNIPrimitive_average(JNIEnv *, jobject, jint, jint);
可以看到,這裡的 jint 和 jdouble 分別表示 java 中的 int 和 double。
TestJNIPrimitive.cpp
的實現如下:
// TestJNIPrimitive.cpp
# include <jni.h>
# include <iostream>
# include "HelloJNI.h"
JNIEXPORT jdouble JNICALL Java_TestJNIPrimitive_average(JNIEnv *env, jobject obj, jint n1, jint n2) {
std::cout << "In C++, the numbers are " << n1 << " and " << n2 << std::endl;
jdouble result;
result = ((jdouble)n1 + n2) / 2.0;
return result;
}
編譯為共享庫並執行:
g++ -shared -fpic -o libmyjni.so -I${JAVA_HOME}/include -I${JAVA_HOME}/include/linux TestJNIPrimitive.cpp
java -Djava.library.path=. -cp . TestJNIPrimitive
傳遞字串
Java JNI 程式:
public class HelloJNI {
static {
System.loadLibrary("hello");
}
public native String sayHello(String msg);
public static void main(String[] args) {
String res = new HelloJNI().sayHello("Hello, JNI");
System.out.println("JNI Results: " + res);
}
}
生成標頭檔案:javac -h . HelloJNI.java
JNIEXPORT jstring JNICALL Java_HelloJNI_sayHello(JNIEnv *, jobject, jstring);
編寫 C++ 實現:
// HelloJNI.cpp
# include <jni.h>
# include <iostream>
# include "HelloJNI.h"
JNIEXPORT jstring JNICALL Java_HelloJNI_sayHello(JNIEnv *env, jobject obj, jstring inJNIStr) {
if (inJNIStr == NULL) {
return NULL;
}
// Convert the JNI String (jstring) into C-String (char*)
const char* inCStr = env->GetStringUTFChars(inJNIStr, NULL);
if (inCStr == NULL) {
return NULL;
}
// Log the received string
std::cout << "In C++, the received string is: " << inCStr << std::endl;
// Perform operations on the string
std::string resultStr = std::string(inCStr) + " from C++";
// Release the JNI String resources
env->ReleaseStringUTFChars(inJNIStr, inCStr);
// Convert the modified C-string back into JNI String (jstring) and return
return env->NewStringUTF(resultStr.c_str());
}
注意,傳遞一個字串比傳遞基本型別要複雜得多,因為 java 的 String 是一個物件,而 C 的 string 是一個 NULL 結尾的 char 陣列。因此,我們需要將 java 的 String 物件轉換成 C 的字串表示形式:char *。
前面我們提到,JNI 環境指標 JNIEnv * 已經為我們定義了非常豐富的介面函式來處理資料的轉換:
- 呼叫
const char* GetStringUTFChars(jstring, jboolean*)
來將 JNI 的 jstring 轉換成 C 的 char * - 呼叫
jstring NewStringUTF(char*)
來將 C 的 char * 轉換成 JNI 的 jstring
編譯生成共享庫並執行:
g++ -shared -fpic -o libhello.so -I${JAVA_HOME}/include -I${JAVA_HOME}/include/linux HelloJNI.cpp
java -Djava.library.path=. -cp . HelloJNI
參考資料
- Java Native Interface (JNI) 從零開始詳細教程
- 一篇文章教你完全掌握 jni 技術