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