JNI接口的实现

博客 动态
0 308
优雅殿下
优雅殿下 2022-01-25 15:55:29
悬赏:0 积分 收藏

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 JNICALLJava_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 JNICALLJava_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 JNICALLJava_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.TestAddjavah -classpath . -jni uds.common.rgm.client.api.RgmClientApijavah -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 __cplusplusextern "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_cppset(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 程序:

// 输出结果为33 

注:如果不拷贝,则会报出下面的错误,提示 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.classCompiled 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

posted on 2022-01-25 14:38 彦承 阅读(29) 评论(0) 编辑 收藏 举报
回帖
    优雅殿下

    优雅殿下 (王者 段位)

    2018 积分 (2)粉丝 (47)源码

    小小码农,大大世界

     

    温馨提示

    亦奇源码

    最新会员