JNI接口的實現


JNI接口的實現

什么是JNI

說明:JNI 是 Java Native Interface 的縮寫,它提供了若干的API實現了Java和其他語言的通信(主要是C&C++,但是它並不妨礙你使用其他編程語言,只要調用約定受支持就可以了)。從Java1.1開始,JNI 標准成為 java 平台的一部分,它允許 Java 代碼和其他語言寫的代碼進行交互。總的來說,JNI 就是一個允許Java語言和其他編程語言(主要是C/C++)通信的接口。

原因:C/C++ 是系統級的編程語言,可以用來開發任何和系統相關的程序和類庫,效率也很高。而 Java 本身編寫底層的應用比較難以實現,使用 JNI 可以調用現有的本地庫,極大地靈活了 Java 的開發。

缺點

1、使用java與本地已編譯的代碼交互,通常會喪失平台可移植性。

2、程序不再是絕對安全的,本地代碼的不當使用可能導致整個程序崩潰。

注:對於上面所說的java使用了JNI 接口會喪失平台的可移植性解釋如下

JNI 提供出來一個功能接口,但是這個功能是使用本地語言進行實現的,通常是C或者C++。

以 linux 系統和 window 系統的 printf 函數為例,雖然這兩個系統都提供了這個打印函數,並且名字也一樣,但是在實現上可能會有各自的不同點。同時在 window 下的動態庫為 dll 文件,linux 下的動態庫為 so 文件。

所以我原本在 linux 下可以正常使用的一套 JNI 功能,一旦需要轉移到 windows 上執行的時候就需要重新編譯實現接口的動態庫。雖然 java 是跨平台的,但是使用 jni 調用的本地方法卻是與平台相依賴的,會在進行編譯的過程中會出現這樣或者那樣的兼容性問題,一般不能直接拿來即用。

實現JNI的基本步驟

  1. 編寫帶有 native 聲明的方法的java類。
  2. 使用 javah + 類名生成擴展名為.h的頭文件。
  3. 使用 C/C++ 實現本地方法。
  4. 將 C/C++ 編寫的文件生成動態鏈接庫。
  5. 在 java 類中引用該動態鏈接庫並完成調用。

注:可以先寫 java 的調用,也可以先寫 C/C++ 的實現,只要兩邊約定好接口的名稱,參數,返回值等信息即可。

Java 和 JNI 類型對照表及轉換示例

1、基本類型

java的基本類型可以直接與C/C++的基本類型映射。
https://upload-images.jianshu.io/upload_images/2718191-8b382192b0c7f230?imageMogr2/auto-orient/strip|imageView2/2/w/1200/format/webp

img

2、引用類型:

與Java基本類型不同,引用類型對開發人員是不透明的。Java內部數據結構並不直接向原生代碼開放。也就是說 C/C++代碼並不能直接訪問Java代碼的字段和方法。
https://upload-images.jianshu.io/upload_images/2718191-20631ac92f6e32e9?imageMogr2/auto-orient/strip|imageView2/2/w/1200/format/webp

img

3、轉換示例:

1)JNI操作字符串:

java 類 TestNatvie.java

/**
* 字符串相關測試代碼
* @param str
*/
public native void testJstring(String str);

C++文件 natvie-lib.cpp

extern "C"
JNIEXPORT void JNICALL
Java_com_example_feifei_testjni_TestNatvie_testJstring(JNIEnv *env, jobject instance,
                                                       jstring str_) { 
    //(1)生成JNI String
    char const * str = "hello world!";
    jstring  jstring = env->NewStringUTF(str);

    // (2) jstring 轉換成 const char * charstr
    const char *charstr = env->GetStringUTFChars(str_, 0);
    
    // (3) 釋放 const char *
    env->ReleaseStringUTFChars(str_, charstr);

    // (4) 獲取字符串子集
    char * subStr = new char;
    env->GetStringUTFRegion(str_,0,3,subStr); //截取字符串char*;

    env->ReleaseStringUTFChars(str_, subStr);
}

2)JNI操作數組:

java 類 TestNatvie.java

/**
  * 整形數組相關代碼
  * @param array
  */
 public native void testIntArray(int []array);

 /**
  *
  * Object Array 相關測試 代碼
  * @param strArr
  */
 public native void testObjectArray(String[]strArr);

C++文件 natvie-lib.cpp

extern "C"
JNIEXPORT void JNICALL
Java_com_example_feifei_testjni_TestNatvie_testIntArray(JNIEnv *env, jobject instance,
                                                     jintArray array_) {

    //----獲取數組元素
    //(1)獲取數組中元素
    jint * intArray = env->GetIntArrayElements(array_,NULL);

    int len = env->GetArrayLength(array_); //(2)獲取數組長度

    LOGD("feifei len:%d",len);

    for(int i = 0; i < len; i++){
        jint item = intArray[i];
        LOGD("feifei item[%d]:%d",i,item);
    }

    env->ReleaseIntArrayElements(array_, intArray, 0);

    //----- 獲取子數組
    jint *subArray = new jint;
    env->GetIntArrayRegion(array_,0,3,subArray);
    for(int i = 0;i<3;i++){
        subArray[i]= subArray[i]+5;
        LOGD("feifei subArray:[%d]:",subArray[i]);
    }

    //用子數組修改原數組元素
    env->SetIntArrayRegion(array_,0,3,subArray);

    env->ReleaseIntArrayElements(array_,subArray,0);//釋放子數組元素

}
extern "C"
JNIEXPORT void JNICALL
Java_com_example_feifei_testjni_TestNatvie_testObjectArray(JNIEnv *env, jobject instance,

                                                           jobjectArray strArr) {
    //獲取數組長度
    int len = env->GetArrayLength(strArr);
    for(int i = 0;i< len;i++){
        //獲取Object數組元素
        jstring item = (jstring)env->GetObjectArrayElement(strArr,i);

        const char * charStr = env->GetStringUTFChars(item, false);
        LOGD("feifei strArray item:%s",charStr);

        jstring jresult = env->NewStringUTF("HaHa");
        //設置Object數組元素
        env->SetObjectArrayElement(strArr,i,jresult);
        env->ReleaseStringUTFChars(item,charStr);
    }

}

3)JNI 訪問Java類的方法和字段

JNI 中訪問java類的方法和字段都是 通過反射來實現的。

JNI獲取Java類的方法ID和字段ID,都需要一個很重要的參數,就是Java類的方法和字段的簽名。

參考:https://www.jianshu.com/p/6cbdda111570

使用JNI機制來實現 java 和 C 的接口示例

說明:使用一個測試例子來進行演示 JNI 的基本流程,以java調用C提供的一個簡單的加法函數為例。首先使用 javah 來生成一個 jni 的接口,然后使用 C 語言將這個接口進行實現,然后編譯生成 DLL 后,提供給 java 進行調用。

1、環境信息:

CLion:2021.2,Build #CL-212.4746.93, built on July 27, 2021

IDEA:2021.1.3,Build #IU-211.7628.21, built on June 30, 2021

編程語言:Java8 + C11

2、基本步驟:

1)在 idea 中新建 java 工程,在 src/test 目錄下面新建 TestAdd.java 文件,內容如下:

package test;

public class TestAdd {
    private native int add(int x, int y);

    public static void main(String[] args) {
        // 加載由 C 編譯器生成的DLL文件
        System.loadLibrary("libjava_jni_test_cpp");

        // 打印系統屬性java.library.path的值
        for (String s : System.getProperty("java.library.path").split(";")) {
            System.out.println(s);
        }

        TestAdd ta = new TestAdd();
        // 調用 C 實現的加法函數,並將值輸出到控制台中
        int res = ta.add(1, 2);
        System.out.println(res);
    }
}

注:System.load 和 System.loadLibrary 詳解

1、它們都可以用來裝載庫文件,不論是 JNI 庫文件還是非 JNI 庫文件。在任何本地方法被調用之前必須先用這個兩個方法之一把相應的 JNI 庫文件裝載。

2、System.load 參數為庫文件的絕對路徑,可以是任意路徑。例如你可以這樣載入一個 windows 平台下 JNI 庫文件:
System.load("C://Documents and Settings//TestJNI.dll");

3、System.loadLibrary 參數為庫文件名,不包含庫文件的擴展名。例如你可以這樣載入一個 windows 平台下 JNI 庫文件:
System.loadLibrary ("TestJNI");

2) 使用 javah 命令生成接口的頭文件:

D:\code\my\java-jni-test\src>javah -classpath . -jni test.TestAdd

javah -classpath . -jni uds.common.rgm.client.api.RgmClientApi
javah -classpath . -jni selonsy.HelloWorld

注意:需要跳轉到src目錄執行命令。具體參數含義如下:

1、src為包名開始的位置。
2、-classpath 后跟類所在的路徑名,如果路徑名與命令行所在的位置相同,則可以使用"."表示。
3、-jni 后跟完整的類名。

執行完成之后,會在 src 目錄下生成 test_TestAdd.h 頭文件,該文件不需要修改,直接使用即可,內容如下:

/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class test_TestAdd */

#ifndef _Included_test_TestAdd
#define _Included_test_TestAdd
#ifdef __cplusplus
extern "C" {
#endif
/*
 * Class:     test_TestAdd
 * Method:    add
 * Signature: (II)I
 */
JNIEXPORT jint JNICALL Java_test_TestAdd_add
  (JNIEnv *, jobject, jint, jint);

#ifdef __cplusplus
}
#endif
#endif

3) 使用 CLion 創建 C 程序並生成 dll 動態鏈接庫:

1> 新建工程:File--》New Project--》C++ Library--》[C++11 & shared]

2> 將上一步生成的 test_TestAdd.h 頭文件添加到 C 工程中。

3> 新建 nativeadd.c 文件,引入該頭文件,並進行加法函數的本地實現,內容如下所示:

#include "test_TestAdd.h"

# 此方法為加法函數的真正實現
int add(int x, int y) {
    return x + y;
}

JNIEXPORT jint JNICALL Java_test_TestAdd_add
        (JNIEnv *env, jobject obj, jint a, jint b) {
    return add(a, b);
}

4> 修改 CMakeLists.txt 內容,主要是設置一下 jni 本身的頭文件位置。由於是生成動態鏈接庫 DLL 文件,因此並不需要執行代碼,修改完成之后,即可在 cmake-build-debug 目錄中找到名為:lib+工程名+.dll 的動態鏈接庫文件了,本例中為:libjava_jni_test_cpp.dll

cmake_minimum_required(VERSION 3.0)
project(java_jni_test_cpp) # 工程名:java_jni_test_cpp

set(CMAKE_CXX_STANDARD 11)

# 添加頭文件目錄,原因是 test_TestAdd.h 頭文件引入了 jni.h 
include_directories("D:/dev/java/jdk1.8.0_172/include")
include_directories("D:/dev/java/jdk1.8.0_172/include/win32")

add_library(java_jni_test_cpp SHARED nativeadd.c)
// 第一個參數是so/dll庫的名字。第二個參數是要生成的so庫的類型,靜態so庫是STATIC,共享so庫是SHARED。第三個參數是C/C++源文件,可以包括多個源文件。

4) 將上一步生成的 dll 文件,拷貝到 java 的系統屬性 java.library.path 對應的任意目錄中,即可運行該 java 程序:

// 輸出結果為3
3 

注:如果不拷貝,則會報出下面的錯誤,提示 dll 找不到。

Exception in thread "main" java.lang.UnsatisfiedLinkError: no libjava_jni_test_cpp in java.library.path

注:除了將 dll 文件拷貝到 java 的系統屬性 java.library.path 對應的任意目錄中,還可以在 IDEA--》File--》Project Structure--》Project Settings--》Libraries 中,添加該 dll 的目錄,比如,D:\native_dll,添加完成之后執行程序,查看執行命令,可以發現增加了:-Djava.library.path=D:\native_dll 的參數。此外,還可以將 dll 文件直接拷貝到 java 程序的根目錄下面,效果是一樣的。

-classpath:設置 CLASSPATH 變量的目的就是讓 Java 執行環境找到指定的 Java 程序對應的 class 文件以及程序中引用的其他 class 文件。

-Djava.library.path:指定非java類包的位置(如:dll,so等)

注:默認情況下,在Windows平台下, java 的系統屬性 java.library.path 對應的目錄一般包括如下位置:

1)和jre相關的一些目錄。

2)程序當前目錄。

3)Windows目錄。

4)系統目錄(system32)。

5)系統環境變量path指定目錄。

使用idea+clion來調試jni接口

1、使用clion編譯生成so/dll文件,此文件提供給idea里面的native方法使用。(保證使用的就是生成的那個文件,路徑要對。)

2、在idea中啟動調試,斷點到調用jni接口之前,暫停。

3、在clion中,菜單Run--attach to process--choose pid,點擊右邊的箭頭,選擇“LLDB”。(注意不要選擇默認的GDB,這個調試會報錯。),然后選擇下面的java進程。

4、上一步中的java進程的pid,通過在cmd窗口,執行jps命令進行查找。

5、在clion中的c/c++代碼中打斷點。

6、idea中進入斷點,就可以跳轉到clion中的代碼了,然后就可以愉快的進行調試了~

ref:attach to process choose LLDB not GBD https://www.jetbrains.com/help/clion/attaching-to-local-process.html

注意事項

1、錯誤:Member reference base type 'JNIEnv' (aka 'const struct JNINativeInterface_ *') is not a structure or union

原因是:env變量在C和C++ 語法表達不一致引起。

FindClass("java/lang/String")
C語言:(*env)->FindClass(env, "java/lang/String")

2、調用JNI的GetMethodID函數獲取一個jmethodID時,需要傳入一個方法名稱和方法的簽名,方法名稱就是在java中定義的方法名,方法簽名的格式為:

(形參參數類型列表)返回值,舉例如下:

()Ljava/lang/String;-------------String f();

(ILjava/lang/Class;)J-------------long f(int i, Class c);

([B])V----------------------------String(byte[] bytes);

描述符 java語言類型

Z boolean

B byte

C char

S short

I int

J long

F float

D double

3、可以使用 javap -s 來查看java的方法簽名,先編譯生成字節碼.class文件,然后執行:javap -s -p xxx.class,結果如下:// -p 顯示所有類和成員,-s 輸出內部類型簽名。

$ javap -s RgmClientApi.class
Compiled from "RgmClientApi.java"
public class uds.common.rgm.client.api.RgmClientApi {
  public uds.common.rgm.client.api.RgmClientApi();
    descriptor: ()V

  public static native int getRgInfoByName(java.lang.String, uds.common.rgm.client.entity.RgInfo);
    descriptor: (Ljava/lang/String;Luds/common/rgm/client/entity/RgInfo;)I

  public static native int getRgInfoById(int, uds.common.rgm.client.entity.RgInfo);
    descriptor: (ILuds/common/rgm/client/entity/RgInfo;)I

  public static native int bindRepRelation(java.lang.String, int, uds.common.rgm.client.entity.RgmBindRepRelationRsp);
    descriptor: (Ljava/lang/String;ILuds/common/rgm/client/entity/RgmBindRepRelationRsp;)I

  public static native int getSiteInfosByRgName(java.lang.String, java.util.List<uds.common.rgm.client.entity.SiteInfo>);
    descriptor: (Ljava/lang/String;Ljava/util/List;)I

  public static native int getSiteInfosByRgId(int, java.util.List<uds.common.rgm.client.entity.SiteInfo>);
    descriptor: (ILjava/util/List;)I

  static {};
    descriptor: ()V
}

參考文獻

看下面這個最好最完善。

http://web.archive.org/web/20120626135526/http://java.sun.com/docs/books/jni/html/jniTOC.html

https://www.jianshu.com/p/6cbdda111570

https://blog.csdn.net/kgdwbb/article/details/72810251

https://www.runoob.com/w3cnote/jni-getting-started-tutorials.html


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM