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

news/2025/1/20 14:34:10/

一、引言

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类。

java">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. 调用本地方法

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

java">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

java">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){// 抛出IllegalArgumentExceptionjclass 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. 定义本地方法

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

java">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方法

java">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方法省略
}

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

java">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类的构造方法IDjmethodID 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指定的目录下面。

java">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.ppmy.cn/news/1564675.html

相关文章

基于 K-Means 聚类分析实现人脸照片的快速分类

注:本文在创作过程中得到了 ChatGPT、DeepSeek、Kimi 的智能辅助支持,由作者本人完成最终审阅。 在 “视频是不能 P 的” 系列文章中,博主曾先后分享过人脸检测、人脸识别等相关主题的内容。今天,博主想和大家讨论的是人脸分类问题。你是否曾在人群中认错人,或是盯着熟人的…

【氮化镓】香港科技大学陈Kevin-单片集成GaN比较器

一、引言(Introduction) GaN HEMT的重要性 文章开篇便强调了氮化镓(GaN)高电子迁移率晶体管(HEMT)在下一代功率转换系统中的巨大潜力。GaN HEMT具备高开关频率、低导通电阻、高击穿电压以及宽工作温度范围等优势,使其成为功率电子领域的热门研究对象。这些特性使得GaN…

2025.1.18——1300

2025.1.18——1300 A 1300 There are n n n cities located on the number line, the i i i-th city is in the point a i a_i ai​. The coordinates of the cities are given in ascending order, so a 1 < a 2 < ⋯ < a n a_1 < a_2 < \dots < a_n a…

浅谈 JVM

JVM 内存划分 JVM 内存划分为 四个区域&#xff0c;分别为 程序计数器、元数据区、栈、堆 程序计数器是记录当前指令执行到哪个地址 元数据区存储存储的是当前类加载好的数据&#xff0c;包括常量池和类对象的信息&#xff0c;.java 编译之后产生 .class 文件&#xff0c;运…

【springboot 集成 mybatis-plus】

springboot 集成 mybatis-plus 前言实战代码生成器自动填充字段 前言 正如MyBatis-Plus官网所说&#xff0c;MyBatis-Plus 是一个 MyBatis 的增强工具&#xff0c;提供了强大的CRUD操作&#xff0c;支持主键自动生成&#xff0c;代码生成器&#xff0c;自动填充字段等等&#…

【ComfyUI专栏】ComfyUI的环境配置

对于常规的用户来说,我们碰到需要非常注意的问题,就是我们的ComfyUI的各个节点可能会有不兼容的情况,因此我们最好建立独立的Python虚拟环境。如何建立虚拟的环境呢?其实非常简单。 执行的命令如下: Python -m venv venv #创建Venv名称的虚拟环境 cd venv #进入到Venv …

InVideo AI技术浅析(四):机器学习

一、视频剪辑与合成 1. 工作原理 视频剪辑与合成是视频编辑中的核心任务,旨在将多个视频片段、音频和字幕等元素组合成一个连贯且富有吸引力的视频。InVideo AI 使用机器学习技术自动化这一过程,通过分析视频内容、识别重要片段并进行智能剪辑和合成。其核心目标是提升观众…

【Linux系统编程】—— 深度解析进程等待与终止:系统高效运行的关键

文章目录 进程创建再次认识fork()函数fork()函数返回值 写时拷贝fork常规⽤法以及调用失败的原因 进程终⽌进程终止对应的三种情况进程常⻅退出⽅法_exit函数exit函数return退出 进程等待进程等待的必要性进程等待的⽅法 进程创建 再次认识fork()函数 fork函数初识&#xff1…