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的基本步驟
- 編寫帶有 native 聲明的方法的java類。
- 使用 javah + 類名生成擴展名為.h的頭文件。
- 使用 C/C++ 實現本地方法。
- 將 C/C++ 編寫的文件生成動態鏈接庫。
- 在 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
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
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