• 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

  • 相关阅读:
    Python进程池multiprocessing.Pool的用法
    基于opencv的车牌提取项目
    Srapy 爬取知乎用户信息
    Scrapy框架简介及小项目应用
    豆瓣爬取图书标签
    CSS选择器使用
    关于 urlencode 的使用和 json 模块的介绍
    urllib库使用方法
    猫眼电影的各种爬取方法
    淘宝商品信息爬取
  • 原文地址:https://www.cnblogs.com/selonsy/p/15842914.html
Copyright © 2020-2023  润新知