Java JNI 學習筆記

Lockegogo發表於2024-07-15

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)。包名中的點換成單下劃線。需要說明的是生成函式中的兩個引數:

  1. JNIEnv *:這是一個指向 JNI 執行環境的指標,我們可以透過這個指標訪問 JNI 函式
  2. 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 * 已經為我們定義了非常豐富的介面函式來處理資料的轉換:

  1. 呼叫 const char* GetStringUTFChars(jstring, jboolean*) 來將 JNI 的 jstring 轉換成 C 的 char *
  2. 呼叫 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

參考資料

  1. Java Native Interface (JNI) 從零開始詳細教程
  2. 一篇文章教你完全掌握 jni 技術

相關文章