JNI接口的实现
阅读原文时间:2023年07月08日阅读:3

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

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