当前位置: 首页 > article >正文

Java调用C/C++那些事(JNI)

一、引言

Java开发中,可能会遇到一些需要复用、移植C/C++库的场景。

比如说,对于某些特定功能,C/C++已有代码实现,但是Java没有。为了可以让Java成功使用该功能,有几种方式:

优势劣势
将C/C++代码翻译成Java代码1. 代码都是Java的,调试、维护比较方便。
2. 支持跨平台
1. 需要有C/C++源码才能翻译成Java
2. Java开发人员需要懂C/C++语法
3. 对于大项目来说,翻译的工作量巨大
4. 无法保证翻译之后功能的正确性
Java通过一些方式调用C/C++的本地代码1. 对于大项目来说,工作量相对较小
2. 对C/C++原有实现没有改动,功能相对比较稳定
3. 对于高性能计算、访问操作系统特定功能、利用现有 C/C++ 库或硬件资源等场景,Java代码没办法自己实现
1. 维护、调试不太方便,Java作为调用方,C/C++就像一个黑盒子,没办法对它深入了解
2. C/C++生成的库不支持跨平台,不同操作系统、处理器架构所需要的库不一样
3. C/C++代码的内存不被JVM管理,不注意的话会出现内存泄漏的情况

综上,Java调用C/C++的本地代码有一定优势的。本文主要介绍Java提供的,最常用的方式,也就是基于JNI的方式。

并且,通过对JNI的学习,也有利于阅读JDK自带的本地方法的源码。

二、JNI基础概念

JNI(Java Native Interface)是Java平台的一部分,它定义了一套编程框架和约定,使得Java代码能够与用其他编程语言(如C、C++或汇编语言)编写的本地应用程序和库进行交互。JNI允许Java程序调用本地方法(native methods),这些本地方法是用其他编程语言实现的,并编译为特定平台的机器代码。

三、环境搭建

JNI主要需要Java、C/C++的编程环境。本章节主要介绍如何快速搭建一个可以开发JNI的环境。

1. 编译/运行工具

1.1. JDK(Java)

建议安装常用、长期维护的JDK,比如说JDK8、JDK11、JDK17、JDK21。本文采用JDK8。

值得一提的是,JDK的安装目录里面有保存jni用到的头文件(包括jni.h、jni_md.h等),后续编码、编译会用到,一般在include目录下。

1.2. gcc/g++(C/C++)

C/C++一般安装gcc/g++作为编译工具。

对于windows,可通过安装MinGW-w64实现。

下载链接:https://github.com/niXman/mingw-builds-binaries/releases

建议选文件:x86_64-14.2.0-release-win32-seh-ucrt-rt_v12-rev0.7z

然后解压,将bin目录加到环境变量中

对于Linux/MacOS,大概率系统已自带。

安装之后,通过命令 gcc -v 检查是否能查出gcc版本即可。

2. IDE

2.1. Intellij IDEA(Java)

Java使用IDEA即可

2.2. Visual Studio Code(C/C++)

C/C++使用Visual Studio Code、Clion等IDE都可以。本文使用VS Code,并且建议安装以下两个插件:C/C++、C/C++ Extension Pack。

在这里插入图片描述

为了方便后续jni编码,给VS Code指定头文件路径,将jni相关的头文件的路径添加到配置中。通过ctrl/cmd+shift+p,让VS Code自动生成一个专门用于C/C++到配置文件。

在这里插入图片描述

在includePath下,新增刚刚在JDK里面找到的jni相关的头文件的目录。

在这里插入图片描述

随后,在任意一个C/C++的代码文件中新增 #include <jni.h>,鼠标点击jni.h,发现VS Code已经能跳转到jni.h文件,说明改动已生效。

在这里插入图片描述

就此,已完成JNI环境的搭建。

四、JNI编程步骤

本章节,通过介绍如何搭建一个简单的JNI helloworld,介绍JNI编程步骤。

1. 定义本地方法

例子中,创建一个HelloWorld类。

public class HelloWorld {
    public native void helloWorld();
}
  1. 本地方法声明,表示该方法的具体实现不在 Java 代码中,而是在通过 System.loadLibrary 加载的本地库中实现。

2. 生成JNI头文件

javac 命令用于编译 Java 源文件并生成 JNI 头文件

javac -h .\jni -d .\target\classes -classpath .\target\classes .\src\main\java\ltd\dujiabao\jni_tests\HelloWorld.java
  • -h .\jni:该选项指定生成的 JNI 头文件存放的目录。在这里,头文件将被放置在当前目录下的 jni 文件夹中。
  • -d .\target\classes:该选项指定编译后的 .class 文件存放的目录。在这里,编译后的类文件将被放置在当前目录下的 target/classes 文件夹中。
  • -classpath .\target\classes:该选项指定编译时的类路径。在这里,类路径指向 target/classes 文件夹,确保编译器能找到依赖的类文件。
  • .\src\main\java\ltd\dujiabao\jni_tests\HelloWorld.java:这是要编译的 Java 源文件的路径。

在.\jni目录下,可以找到一个名为ltd_dujiabao_jni_tests_HelloWorld.h的文件,可以看出生成的这个头文件的名称就是类的全类名(通过下划线划分)

/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class ltd_dujiabao_jni_tests_HelloWorld */

#ifndef _Included_ltd_dujiabao_jni_tests_HelloWorld
#define _Included_ltd_dujiabao_jni_tests_HelloWorld
#ifdef __cplusplus
extern "C" {
    #endif
    /*
 * Class:     ltd_dujiabao_jni_tests_HelloWorld
 * Method:    helloWorld
 * Signature: ()V
 */
    JNIEXPORT void JNICALL Java_ltd_dujiabao_jni_1tests_HelloWorld_helloWorld
        (JNIEnv *, jobject);

    #ifdef __cplusplus
}
#endif
#endif
  1. 注释:提示这是一个自动生成的文件,不应手动编辑。
/* DO NOT EDIT THIS FILE - it is machine generated */
  1. 包含头文件:包含了 JNI 的标准头文件 jni.h,这是所有 JNI 程序必须包含的头文件,提供了与 Java 交互所需的所有宏、类型和函数声明。2.2 小节 VS Code已经配置该头文件的位置,因此编写代码的时候是能自动代码提示的。
#include <jni.h>
  1. 防止重复包含:使用预处理器指令防止头文件被多次包含,确保编译时不会出现重复定义的问题。
#ifndef _Included_ltd_dujiabao_jni_tests_HelloWorld
#define _Included_ltd_dujiabao_jni_tests_HelloWorld
//...
#endif
  1. C++ 兼容性处理:如果编译环境是 C++,则使用 extern "C" 来确保 C 链接方式,避免名称修饰问题。
#ifdef __cplusplus
extern "C" {
#endif
//...
#ifdef __cplusplus
}
#endif
  1. JNI 方法声明
  • 这是关键部分,声明了一个名为 Java_ltd_dujiabao_jni_1tests_HelloWorld_helloWorld 的 C 函数。
  • JNIEXPORTJNICALL 是 JNI 宏,分别用于导出符号和指定调用约定。
  • JNIEnv * 是指向 JNI 环境的指针,提供了与 JVM 交互的功能。
  • jobject 是对调用此方法的 Java 对象的引用。
  • Signature: ()V 表示该方法没有参数且返回 void
/*
 * Class:     ltd_dujiabao_jni_tests_HelloWorld
 * Method:    helloWorld
 * Signature: ()V
 */
JNIEXPORT void JNICALL Java_ltd_dujiabao_jni_1tests_HelloWorld_helloWorld
  (JNIEnv *, jobject);

3. 实现函数

新建一个ltd_dujiabao_jni_tests_HelloWorld.c文件,实现函数Java_ltd_dujiabao_jni_1tests_HelloWorld_helloWorld

#include "ltd_dujiabao_jni_tests_HelloWorld.h"
#include <stdio.h>

JNIEXPORT void JNICALL Java_ltd_dujiabao_jni_1tests_HelloWorld_helloWorld(JNIEnv *env, jobject obj)
{
    // 打印一条消息到控制台
    printf("Hello from JNI!\n");
}
  1. 包含头文件:
  • #include "ltd_dujiabao_jni_tests_HelloWorld.h": 包含自动生成的头文件,其中定义了JNI函数的声明。
  • #include <stdio.h>:标准输入输出库
  1. 实现函数 Java_ltd_dujiabao_jni_1tests_HelloWorld_helloWorld
  • printf("Hello from JNI!\n");: 使用标准C库的printf函数在控制台打印一条消息

4. 编译生成动态链接库

此步骤,将上述编写的C语言代码编译成动态链接库,以供Java程序调用。不同的操作系统的命令有少许差异。

在Linux上,动态链接库通常以.so文件的形式存在。可以使用gcc来编译生成共享库。

gcc -I${JAVA_HOME}/include -I${JAVA_HOME}/include/linux -shared -o libHelloWorld.so ltd_dujiabao_jni_tests_HelloWorld.c

在这里插入图片描述

在Windows上,动态链接库通常以.dll文件的形式存在。可以使用gcc(例如通过MinGW)来编译生成动态链接库。

gcc -I"%JAVA_HOME%\include" -I"%JAVA_HOME%\include\win32" -shared -o HelloWorld.dll ltd_dujiabao_jni_tests_HelloWorld.c

在macOS上,动态链接库通常以.dylib文件的形式存在。你可以使用gcc或clang来编译生成动态链接库。

gcc -I${JAVA_HOME}/include -I${JAVA_HOME}/include/darwin -shared -o libHelloWorld.dylib ltd_dujiabao_jni_tests_HelloWorld.c

可以看出,各平台的gcc命令实际上没有太大的差异:

  • -I${JAVA_HOME}/include:包含Java的头文件。
  • -I${JAVA_HOME}/include/x:包含Linux/Windows/macOS特定的JNI头文件。
  • -shared:生成共享库。
  • -o libHelloWorld.so:指定输出文件名为libHelloWorld.so的动态链接库。(其他平台后缀有差异)
    • 注意:这个动态链接库的名称要和System.loadLibrary加载的名称一致
  • ltd_dujiabao_jni_tests_HelloWorld.c:要编译的源文件。

5. 调用本地方法

这一步主要是要将动态链接库加载进行,然后调用本地方法

public class Caller {
    static {
        System.loadLibrary("HelloWorld");
    }

    public static void main(String[] args) {
        new HelloWorld().helloWorld();
    }
}
  • 静态代码块:在类加载时执行一次。System.loadLibrary("HelloWorld"),加载名为 HelloWorld 的动态链接库(也就是上一步生成的动态链接库,注意名称要和动态链接库的名称一致)。这个库包含了实现 HelloWorld 类中声明的本地方法的代码
  • new HelloWorld().helloWorld():创建 HelloWorld 类的一个实例。调用该实例的 helloWorld 方法,这是一个本地方法,具体实现由前面加载的 HelloWorld 库提供。

在执行前,需要新增JVM参数-Djava.library.path,用于指定动态链接库的目录地址

-Djava.library.path=F:\blog\jna\demo_java\jni_tests\jni

如果使用IDEA执行,可以这样改:

在这里插入图片描述

随后启动发现打印成功

在这里插入图片描述

至此,完成了JNI的HelloWorld

五、代码示例展示

下面通过一些代码示例,介绍编写JNI代码的方法。并且在介绍JNI编写方法的过程中,对jni.h文件进行简单介绍。

1. 输入输出基本数据类型

1.1. 定义本地方法

定义了四个本地方法,输入和输出都是基本数据类型int

public class FourOperations {
    static {
        System.loadLibrary("four_operations");
    }

    public native int add(int v1, int v2);
    public native int sub(int v1, int v2);
    public native int mul(int v1, int v2);
    public native int div(int v1, int v2);
}

1.2. JNI头文件

从头文件可以看出,和HelloWorld的头文件差异主要在四个函数。

从函数输入输出,可以看出jint对应的就是Java的int数据类型。

/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class ltd_dujiabao_jni_tests_FourOperations */

#ifndef _Included_ltd_dujiabao_jni_tests_FourOperations
#define _Included_ltd_dujiabao_jni_tests_FourOperations
#ifdef __cplusplus
extern "C" {
    #endif
    /*
 * Class:     ltd_dujiabao_jni_tests_FourOperations
 * Method:    add
 * Signature: (II)I
 */
    JNIEXPORT jint JNICALL Java_ltd_dujiabao_jni_1tests_FourOperations_add
        (JNIEnv *, jobject, jint, jint);

    /*
 * Class:     ltd_dujiabao_jni_tests_FourOperations
 * Method:    sub
 * Signature: (II)I
 */
    JNIEXPORT jint JNICALL Java_ltd_dujiabao_jni_1tests_FourOperations_sub
        (JNIEnv *, jobject, jint, jint);

    /*
 * Class:     ltd_dujiabao_jni_tests_FourOperations
 * Method:    mul
 * Signature: (II)I
 */
    JNIEXPORT jint JNICALL Java_ltd_dujiabao_jni_1tests_FourOperations_mul
        (JNIEnv *, jobject, jint, jint);

    /*
 * Class:     ltd_dujiabao_jni_tests_FourOperations
 * Method:    div
 * Signature: (II)I
 */
    JNIEXPORT jint JNICALL Java_ltd_dujiabao_jni_1tests_FourOperations_div
        (JNIEnv *, jobject, jint, jint);

    #ifdef __cplusplus
}
#endif
#endif

点击jint的定义,可以看出它实际上就是对C/C++的数据类型的封装。

封装的原因主要是为了确保Java和本地代码之间的数据类型一致性和可移植性。不同平台的数据类型大小不同:不同操作系统和架构(如32位和64位)对基本数据类型的大小有不同的定义。例如,在32位系统上,int通常是32位,而在64位系统上,int也通常是32位,但long可能是64位。

在这里插入图片描述

在这里插入图片描述

1.3. 实现函数

#include <jni.h>
#include "ltd_dujiabao_jni_tests_FourOperations.h"

JNIEXPORT jint JNICALL Java_ltd_dujiabao_jni_1tests_FourOperations_add(JNIEnv *env, jobject obj, jint a, jint b)
{
    return a + b;
}

JNIEXPORT jint JNICALL Java_ltd_dujiabao_jni_1tests_FourOperations_sub(JNIEnv *env, jobject obj, jint a, jint b)
{
    return a - b;
}

JNIEXPORT jint JNICALL Java_ltd_dujiabao_jni_1tests_FourOperations_mul(JNIEnv *env, jobject obj, jint a, jint b)
{
    return a * b;
}

JNIEXPORT jint JNICALL Java_ltd_dujiabao_jni_1tests_FourOperations_div(JNIEnv *env, jobject obj, jint a, jint b)
{
    if (b == 0)
    {
        // 抛出IllegalArgumentException
        jclass illegalArgumentException = (*env)->FindClass(env, "java/lang/IllegalArgumentException");
        if (illegalArgumentException != NULL)
        {
            (*env)->ThrowNew(env, illegalArgumentException, "Division by zero");
            return -1;
        }
    }
    return a / b;
}

从实现上来看,其实加减乘除的代码都很简单,因为jint实际上就是C语言的数据类型long的别名,所以运算和一般的long没有区别。

比较有参考价值的是除法,检查除数为0时,抛异常这段代码。以下是这段代码的详细解释:

  1. 查找异常类
  • jclass: 这是一个指向Java类的引用。
  • FindClass: 这是一个JNI函数,用于查找并返回指定名称的Java类的引用。
  • "java/lang/IllegalArgumentException": 这是Java中IllegalArgumentException类的全限定名。
  • illegalArgumentException: 这是一个指向IllegalArgumentException类的指针,用于后续操作。
jclass illegalArgumentException = (*env)->FindClass(env, "java/lang/IllegalArgumentException");
  1. 检查类是否成功找到
  • if (illegalArgumentException != NULL): 检查FindClass函数是否成功找到了IllegalArgumentException类。如果找到了,则illegalArgumentException不为NULL
if (illegalArgumentException != NULL)
  1. 抛出异常
  • ThrowNew: 这是一个JNI函数,用于抛出一个新的Java异常。
  • illegalArgumentException: 这是要抛出的异常类的引用。
  • "Division by zero": 这是异常的详细消息,描述了异常的原因。
(*env)->ThrowNew(env, illegalArgumentException, "Division by zero");
  1. 返回值
  • return -1;: 在抛出异常后,函数返回一个值。在这个例子中,返回-1。可能这里有人会有疑问,为什么抛异常之后,还要return。这是因为对于C语言代码来说,上述抛异常只是调用了一个函数,并不是异常,如果不return的话,调用完抛异常的函数之后,还会继续往下执行。
return -1;

从上述代码中,也可以看出JNI是一个非常重要的结构体。可以简单浏览一下,其实就是包含各种JNI函数指针的结构体,可以类比成Java中的接口。

JNIEnv 是一个指向 JNIEnv 结构体的指针,该结构体包含了指向各种JNI函数的指针。通过这些函数,本地代码可以调用Java方法、访问Java对象、操作Java数组等。JNIEnv 是本地代码与JVM交互的主要桥梁。

typedef const struct JNINativeInterface_ *JNIEnv;
struct JNINativeInterface_{
    //...
    jclass (JNICALL *FindClass)
      (JNIEnv *env, const char *name);
    //...
    jint (JNICALL *ThrowNew)
      (JNIEnv *env, jclass clazz, const char *msg);
    //...
}

2. 输入输出字符串

以下例子展示如何在JNI中操作Java的字符串

2.1. 定义本地方法

定义了两个方法,一个是输入字符串,一个是输出字符串。

public class StrUtils {
    static {
        System.loadLibrary("str_utils");
    }

    public native int length(String str);

    public native String createStr(int length, byte filled);
}

2.2. JNI头文件

从头文件,可以看到Java的String对应的是JNI的jstring;Java的int对应的是JNI的jint;Java的byte对应的是JNI的jbyte。

/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class ltd_dujiabao_jni_tests_StrUtils */

#ifndef _Included_ltd_dujiabao_jni_tests_StrUtils
#define _Included_ltd_dujiabao_jni_tests_StrUtils
#ifdef __cplusplus
extern "C" {
    #endif
    /*
 * Class:     ltd_dujiabao_jni_tests_StrUtils
 * Method:    length
 * Signature: (Ljava/lang/String;)I
 */
    JNIEXPORT jint JNICALL Java_ltd_dujiabao_jni_1tests_StrUtils_length
        (JNIEnv *, jobject, jstring);

    /*
 * Class:     ltd_dujiabao_jni_tests_StrUtils
 * Method:    createStr
 * Signature: (IB)Ljava/lang/String;
 */
    JNIEXPORT jstring JNICALL Java_ltd_dujiabao_jni_1tests_StrUtils_createStr
        (JNIEnv *, jobject, jint, jbyte);

    #ifdef __cplusplus
}
#endif
#endif

从jni.h可以看出,实际上jstring实际上对应的就是一个_jobject指针,可以理解为Java对象

在这里插入图片描述

  1. struct _jobject:这是一个不完整的结构体声明,表示Java对象的内部结构。_jobject 是一个不透明的结构体,JNI用户不需要知道其内部细节。
  2. typedef struct _jobject *jobject;:这是一个指向_jobject结构的指针,表示一个通用的Java对象。jobject 是JNI中所有对象类型的基类型。
  3. 类型别名:为了更好地表示不同的Java对象类型,JNI定义了一系列类型别名,这些别名都基于jobject。这些别名使得代码更具可读性和可维护性,明确表示不同类型的Java对象。

2.3. 实现函数

以下是函数的实现:

#include "ltd_dujiabao_jni_tests_StrUtils.h"
#include <stdlib.h>
#include <string.h>

JNIEXPORT jint JNICALL Java_ltd_dujiabao_jni_1tests_StrUtils_length(JNIEnv *env, jobject obj, jstring str)
{
    // 将 Java 字符串转换为 C 字符串
    const char *nativeString = (*env)->GetStringUTFChars(env, str, 0);
    if (nativeString == NULL)
    {
        return 0; // 如果内存不足,返回 0
    }

    // 计算字符串长度
    jint length = (jint)strlen(nativeString);

    // 释放 C 字符串
    (*env)->ReleaseStringUTFChars(env, str, nativeString);

    return length;
}

JNIEXPORT jstring JNICALL Java_ltd_dujiabao_jni_1tests_StrUtils_createStr(JNIEnv *env, jobject obj, jint length, jbyte value)
{ // 创建一个 C 字符串,长度为 length + 1 以容纳终止符 '\0'
    char *nativeString = (char *)malloc((length + 1) * sizeof(char));
    if (nativeString == NULL)
    {
        return NULL; // 如果内存不足,返回 NULL
    }

    // 填充字符串
    for (jint i = 0; i < length; i++)
    {
        nativeString[i] = (char)value;
    }
    nativeString[length] = '\0'; // 添加终止符

    // 将 C 字符串转换为 Java 字符串
    jstring result = (*env)->NewStringUTF(env, nativeString);

    // 释放 C 字符串
    free(nativeString);

    return result;
}
2.3.1. 输入字符串
  1. 将Java字符串转换为C字符串
  • GetStringUTFChars: 这是一个JNI函数,用于将Java字符串(jstring)转换为UTF-8编码的C字符串(const char *)。
  • env: JNI环境指针。
  • str: 要转换的Java字符串。
  • 0: 这是一个指向jboolean的指针,用于指示是否复制字符串。传递0表示不复制字符串。
  • nativeString: 转换后的C字符串。
const char *nativeString = (*env)->GetStringUTFChars(env, str, 0);
  1. 检查内存分配是否成功
  • if (nativeString == NULL): 检查GetStringUTFChars是否成功返回C字符串。如果返回NULL,表示内存不足或转换失败。
  • return 0: 如果内存不足,返回0作为错误码。
if (nativeString == NULL)
{
    return 0; // 如果内存不足,返回 0
}
  1. 计算字符串长度
  • strlen: 这是C标准库中的函数,用于计算C字符串的长度。
  • nativeString: 要计算长度的C字符串。
  • jint: 将strlen的返回值(size_t类型)转换为jint类型。
jint length = (jint)strlen(nativeString);
  1. 释放C字符串
  • ReleaseStringUTFChars: 这是一个JNI函数,用于释放之前通过GetStringUTFChars获取的C字符串。必须释放内存,不然会出现内存泄漏!!
  • env: JNI环境指针。
  • str: 原始的Java字符串。
  • nativeString: 要释放的C字符串。
(*env)->ReleaseStringUTFChars(env, str, nativeString);
  1. 返回字符串长度
  • return length: 返回计算得到的字符串长度。
return length;
2.3.2. 输出字符串
  1. 分配内存
  • malloc: 这是C标准库中的函数,用于动态分配内存。
  • (length + 1) * sizeof(char): 分配的内存大小为 length + 1 字节,以容纳字符串的终止符 \0
  • nativeString: 指向分配的内存的指针。
  • if (nativeString == NULL): 检查内存分配是否成功。如果分配失败,返回 NULL
char *nativeString = (char *)malloc((length + 1) * sizeof(char));
if (nativeString == NULL)
{
    return NULL; // 如果内存不足,返回 NULL
}
  1. 填充字符串
  • for 循环: 遍历从 0length - 1 的索引,将每个位置设置为 value
  • nativeString[length] = '\0': 在字符串的末尾添加终止符 \0,表示字符串的结束。
for (jint i = 0; i < length; i++)
{
    nativeString[i] = (char)value;
}
nativeString[length] = '\0'; // 添加终止符
  1. 将C字符串转换为Java字符串
  • NewStringUTF: 这是一个JNI函数,用于将UTF-8编码的C字符串转换为Java字符串。Java字符串是被JVM管理的,因此不需要考虑内存泄漏的问题。
  • env: JNI环境指针。
  • nativeString: 要转换的C字符串。
  • result: 转换后的Java字符串。
jstring result = (*env)->NewStringUTF(env, nativeString);
  1. 释放C字符串
  • free: 这是C标准库中的函数,用于释放之前分配的内存。
  • nativeString: 要释放的C字符串。
free(nativeString);
  1. 返回Java字符串
  • return result: 返回转换后的Java字符串。
return result;

3. 输入输出对象

以下例子展示如何在JNI中操作Java对象

3.1. 定义本地方法

定义一个对象,表示点,保存坐标x、y,提供get、set方法

public class Point {
    private double x;
    private double y;

    public Point() {
    }

    public Point(double x, double y) {
        this.x = x;
        this.y = y;
    }
    // get、set、toString方法省略
}

定义本地方法,输入、输出对象

public class PointUtils {

    static {
        System.loadLibrary("point_utils");
    }

    public native double distanceBetweenPoints(Point a, Point b);

    public native Point newPoint(double x, double y);
}

3.2. JNI头文件

从头文件可以看出,Java对象在JNI中用jobject表示。jobject在上一小节已简单介绍,在此不再赘述。

/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class ltd_dujiabao_jni_tests_PointUtils */

#ifndef _Included_ltd_dujiabao_jni_tests_PointUtils
#define _Included_ltd_dujiabao_jni_tests_PointUtils
#ifdef __cplusplus
extern "C" {
#endif
    
/*
 * Class:     ltd_dujiabao_jni_tests_PointUtils
 * Method:    distanceBetweenPoints
 * Signature: (Lltd/dujiabao/jni_tests/Point;Lltd/dujiabao/jni_tests/Point;)D
 */
JNIEXPORT jdouble JNICALL Java_ltd_dujiabao_jni_1tests_PointUtils_distanceBetweenPoints
  (JNIEnv *, jobject, jobject, jobject);

/*
 * Class:     ltd_dujiabao_jni_tests_PointUtils
 * Method:    newPoint
 * Signature: (DD)Lltd/dujiabao/jni_tests/Point;
 */
JNIEXPORT jobject JNICALL Java_ltd_dujiabao_jni_1tests_PointUtils_newPoint
  (JNIEnv *, jobject, jdouble, jdouble);

#ifdef __cplusplus
}
#endif
#endif

3.3. 实现函数

#include "ltd_dujiabao_jni_tests_PointUtils.h"
#include <stdio.h>
#include <math.h>
#include <stdlib.h>

typedef struct
{
    double x;
    double y;
} Point;

Point *getPoint(JNIEnv *env, jobject point)
{
    Point *p = (Point *)malloc(sizeof(Point));
    jclass pointClazz = (*env)->GetObjectClass(env, point);
    jmethodID getX_method_id = (*env)->GetMethodID(env, pointClazz, "getX", "()D");
    jmethodID getY_method_id = (*env)->GetMethodID(env, pointClazz, "getY", "()D");
    p->x = (*env)->CallDoubleMethod(env, point, getX_method_id);
    p->y = (*env)->CallDoubleMethod(env, point, getY_method_id);
    return p;
}

JNIEXPORT jdouble JNICALL Java_ltd_dujiabao_jni_1tests_PointUtils_distanceBetweenPoints(JNIEnv *env, jobject obj, jobject a, jobject b)
{
    Point *p1 = getPoint(env, a);
    Point *p2 = getPoint(env, b);
    return sqrt(pow(p1->x - p2->x, 2) + pow(p1->y - p2->y, 2));
}

JNIEXPORT jobject JNICALL Java_ltd_dujiabao_jni_1tests_PointUtils_newPoint(JNIEnv *env, jobject obj, jdouble x, jdouble y)
{
    // 获取Point类
    jclass pointClass = (*env)->FindClass(env, "ltd/dujiabao/jni_tests/Point");
    // 获取Point类的构造方法ID
    jmethodID constructorID = (*env)->GetMethodID(env, pointClass, "<init>", "(DD)V");
    // 创建Point对象
    jobject pointObj = (*env)->NewObject(env, pointClass, constructorID, x, y);
    return pointObj;
}
3.3.1. 结构体Point
  1. 结构体定义
  • typedef: 这是一个关键字,用于为类型创建一个新的名称。
  • struct: 这是一个关键字,用于定义结构体。
  • { ... }: 结构体的主体部分,包含结构体的成员。
  • double x;: 结构体的第一个成员,表示 x 坐标,类型为 double
  • double y;: 结构体的第二个成员,表示 y 坐标,类型为 double
  • Point: 结构体的新名称,用于后续引用该结构体。
typedef struct
{
    double x;
    double y;
} Point;
3.3.2. Java对象转换为结构体 getPoint
  1. 分配内存
  • malloc: 这是C标准库中的函数,用于动态分配内存。
  • sizeof(Point): 分配的内存大小为 Point 结构体的大小。
  • p: 指向分配的内存的指针。
  • if (p == NULL): 检查内存分配是否成功。如果分配失败,返回 NULL
Point *p = (Point *)malloc(sizeof(Point));
if (p == NULL)
{
    return NULL; // 如果内存不足,返回 NULL
}
  1. 获取Java类
jclass pointClazz = (*env)->GetObjectClass(env, point);
  • GetObjectClass: 这是一个JNI函数,用于获取指定Java对象的类。
  • env: JNI环境指针。
  • point: Java对象实例。
  • pointClazz: 获取到的Java类的引用。
  1. 获取方法ID
  • GetMethodID: 这是一个JNI函数,用于获取指定类的方法ID。
  • env: JNI环境指针。
  • pointClazz: Java类的引用。
  • "getX""getY": 方法名称。
  • "()D": 方法签名,表示方法没有参数且返回一个 double 类型的值。
  • getX_method_idgetY_method_id: 获取到的方法ID。
jmethodID getX_method_id = (*env)->GetMethodID(env, pointClazz, "getX", "()D");
jmethodID getY_method_id = (*env)->GetMethodID(env, pointClazz, "getY", "()D");

获取方法签名,可以通过命令javap -s -p Point.class

  1. 调用Java方法
  • CallDoubleMethod: 这是一个JNI函数,用于调用Java对象的 double 类型的方法。
  • env: JNI环境指针。
  • point: Java对象实例。
  • getX_method_idgetY_method_id: 方法ID。
  • p->xp->y: 将调用方法返回的值存储到 Point 结构体的相应字段中。
p->x = (*env)->CallDoubleMethod(env, point, getX_method_id);
p->y = (*env)->CallDoubleMethod(env, point, getY_method_id);
  1. 返回 Point 结构体指针
  • return p: 返回指向 Point 结构体的指针。
return p;

JNI调用Java方法的方式,可以总结为几步:获取类,再根据类、方法名获取方法id,最终传入对象、方法名调用方法。和Java反射有那么一点相似。

3.3.3. 创建对象 Java_ltd_dujiabao_jni_1tests_PointUtils_newPoint
  1. 获取Java类
  • FindClass: 这是一个JNI函数,用于查找并返回指定名称的Java类的引用。
  • env: JNI环境指针。
  • "ltd/dujiabao/jni_tests/Point": 这是Java中Point类的全限定名,使用斜杠/分隔包名和类名。
  • pointClass: 获取到的Point类的引用。
jclass pointClass = (*env)->FindClass(env, "ltd/dujiabao/jni_tests/Point");
  1. 获取构造方法ID
  • GetMethodID: 这是一个JNI函数,用于获取指定类的方法ID。
  • env: JNI环境指针。
  • pointClass: Java类的引用。
  • "<init>": 这是构造方法的名称,构造方法的名称固定为<init>
  • "(DD)V": 这是构造方法的签名,表示构造方法有两个double类型的参数且没有返回值。
  • constructorID: 获取到的构造方法ID。
jmethodID constructorID = (*env)->GetMethodID(env, pointClass, "<init>", "(DD)V");
  1. 创建Java对象
  • NewObject: 这是一个JNI函数,用于创建一个新的Java对象。
  • env: JNI环境指针。
  • pointClass: Java类的引用。
  • constructorID: 构造方法ID。
  • xy: 传递给构造方法的参数。
  • pointObj: 创建的Java对象。
jobject pointObj = (*env)->NewObject(env, pointClass, constructorID, x, y);
  1. 返回Java对象
  • return pointObj: 返回创建的Java对象。
return pointObj;

4. 调用已有C/C++代码库

对于已有代码库,有几种方式可以调用:

  1. JNI代码作为桥接程序,和已有的本地代码的源码一起编译成一个动态链接库
  2. JNI代码作为桥接程序编译成一个动态链接库,已有本地代码提供另外的动态链接库

第一种方式实际上和上面代码示例差别不大,在此不再赘述。本小节仅介绍第二种方式。

假设我们已有一个用于四则运算的本地代码,动态链接库叫libfour_operations.so,其头文件为

#ifndef FOUR_OPERATIONS_H
#define FOUR_OPERATIONS_H

#include <stdlib.h>

// 加法
int add(int a, int b);

// 减法
int subtract(int a, int b);

// 乘法
int multiply(int a, int b);

// 除法
double divide(int a, int b);

#endif // FOUR_OPERATIONS_H

我们通过JNI定义了本地方法

public class FourOperations {
    public native int add(int v1, int v2);

    public native int sub(int v1, int v2);

    public native int mul(int v1, int v2);

    public native int div(int v1, int v2);
}

JNI头文件为:

/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class ltd_dujiabao_jni_tests_FourOperations */

#ifndef _Included_ltd_dujiabao_jni_tests_FourOperations
#define _Included_ltd_dujiabao_jni_tests_FourOperations
#ifdef __cplusplus
extern "C" {
#endif
/*
 * Class:     ltd_dujiabao_jni_tests_FourOperations
 * Method:    add
 * Signature: (II)I
 */
JNIEXPORT jint JNICALL Java_ltd_dujiabao_jni_1tests_FourOperations_add
  (JNIEnv *, jobject, jint, jint);

/*
 * Class:     ltd_dujiabao_jni_tests_FourOperations
 * Method:    sub
 * Signature: (II)I
 */
JNIEXPORT jint JNICALL Java_ltd_dujiabao_jni_1tests_FourOperations_sub
  (JNIEnv *, jobject, jint, jint);

/*
 * Class:     ltd_dujiabao_jni_tests_FourOperations
 * Method:    mul
 * Signature: (II)I
 */
JNIEXPORT jint JNICALL Java_ltd_dujiabao_jni_1tests_FourOperations_mul
  (JNIEnv *, jobject, jint, jint);

/*
 * Class:     ltd_dujiabao_jni_tests_FourOperations
 * Method:    div
 * Signature: (II)I
 */
JNIEXPORT jint JNICALL Java_ltd_dujiabao_jni_1tests_FourOperations_div
  (JNIEnv *, jobject, jint, jint);

#ifdef __cplusplus
}
#endif
#endif

头文件对应的.c文件,引入JNI的头文件将其实现,引入现有本地方法库的头文件,调用其函数。

#include <jni.h>
#include "ltd_dujiabao_jni_tests_FourOperations.h"
#include "four_operations.h"

JNIEXPORT jint JNICALL Java_ltd_dujiabao_jni_1tests_FourOperations_add(JNIEnv *env, jobject obj, jint a, jint b) {
    return add(a, b);
}

JNIEXPORT jint JNICALL Java_ltd_dujiabao_jni_1tests_FourOperations_sub(JNIEnv *env, jobject obj, jint a, jint b) {
    return subtract(a, b);
}

JNIEXPORT jint JNICALL Java_ltd_dujiabao_jni_1tests_FourOperations_mul(JNIEnv *env, jobject obj, jint a, jint b) {
    return multiply(a, b);
}

JNIEXPORT jint JNICALL Java_ltd_dujiabao_jni_1tests_FourOperations_div(JNIEnv *env, jobject obj, jint a, jint b) {
    return divide(a, b);
}

随后可以生成桥接程序的动态链接库

gcc -I"%JAVA_HOME%\include" -I"%JAVA_HOME%\include\win32"
    -L. -lfour_operations
    -shared -o libbridge.so ltd_dujiabao_jni_tests_FourOperations.c
  • -L:指定其依赖的动态链接库目录
  • -l:指定其依赖的动态链接库

最终,在Java程序中将现有的动态链接库、桥接程序生成的动态链接库加载进来即可。(两个动态链接库都需要放在java.library.path指定的目录下面。

public class FourOperations {

    static {
        System.loadLibrary("four_operations");
        System.loadLibrary("bridge");
    }

    public native int add(int v1, int v2);

    public native int sub(int v1, int v2);

    public native int mul(int v1, int v2);

    public native int div(int v1, int v2);

    public static void main(String[] args) {
        FourOperations fourOperations = new FourOperations();
        System.out.println(fourOperations.add(4, 2));
        System.out.println(fourOperations.sub(4, 2));
        System.out.println(fourOperations.mul(4, 2));
        System.out.println(fourOperations.div(4, 2));
    }
}

六、结论

本文介绍了JNI编程的基本概念、环境搭建,通过HelloWorld介绍了Java调用C/C++代码的完整步骤,通过代码示例,介绍一些常见的使用场景,并且简单介绍了JNI的头文件。


http://www.kler.cn/a/510356.html

相关文章:

  • 【王树森搜素引擎技术】相关性03:文本匹配(TF-IDF、BM25、词距)
  • 《自动驾驶与机器人中的SLAM技术》ch8:基于预积分和图优化的紧耦合 LIO 系统
  • STM32 FreeRTOS 信号量
  • 简历_基于 Cache Aside 模式解决数据库与缓存一致性问题。
  • lvm快照备份技术详细知识点
  • Excel中函数SIGN()的用法
  • 【算法】算法基础课模板大全——第二篇
  • 各种获取数据接口
  • 基于python的财务数据分析与可视化设计与实现
  • Python Pyside6 加Sqlite3 写一个 通用 进销存 系统 初型
  • Unity3D BEPUphysicsint定点数3D物理引擎详解
  • 在 Windows 下利用 `.pem` 文件配置 VS Code Remote-SSH 连接远程服务器
  • 基于协方差交叉(CI)的多传感器融合算法matlab仿真,对比单传感器和SCC融合
  • 用sklearn运行分类模型,选择AUC最高的模型保存模型权重并绘制AUCROC曲线(以逻辑回归、随机森林、梯度提升、MLP为例)
  • 【威联通】FTP服务提示:服务器回应不可路由的地址。被动模式失败。
  • 如何下载对应城市的地理json文件
  • springboot医院信管系统
  • MYSQL学习笔记(二):基本的SELECT语句使用(基本、条件、聚合函数查询)
  • 蓝桥杯3526 子树的大小 | 数学规律
  • 数据仓库经典面试题
  • oracle使用case when报错ORA-12704字符集不匹配原因分析及解决方法
  • 三电平空间矢量详解
  • Vue3 整合 ArcGIS 技术指南
  • 计算机网络 (49)网络安全问题概述
  • ELF2开发板(飞凌嵌入式)基本使用的搭建
  • 统信V20 1070e X86系统编译安装mysql-5.7.44版本以及主从构建