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

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

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

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

优势

劣势

将C/C++代码翻译成Java代码

代码都是Java的,调试、维护比较方便。

2. 支持跨平台

需要有C/C++源码才能翻译成Java

2. Java开发人员需要懂C/C++语法

3. 对于大项目来说,翻译的工作量巨大

4. 无法保证翻译之后功能的正确性

Java通过一些方式调用C/C++的本地代码

对于大项目来说,工作量相对较小

2. 对C/C++原有实现没有改动,功能相对比较稳定

3. 对于高性能计算、访问操作系统特定功能、利用现有 C/C++ 库或硬件资源等场景,Java代码没办法自己实现

维护、调试不太方便,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. IDE2.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,发现VS Code已经能跳转到jni.h文件,说明改动已生效。

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

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

1. 定义本地方法例子中,创建一个HelloWorld类。

代码语言:java复制public class HelloWorld {

public native void helloWorld();

}本地方法声明,表示该方法的具体实现不在 Java 代码中,而是在通过 System.loadLibrary 加载的本地库中实现。2. 生成JNI头文件javac 命令用于编译 Java 源文件并生成 JNI 头文件

代码语言:shell复制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的文件,可以看出生成的这个头文件的名称就是类的全类名(通过下划线划分)

代码语言:c代码运行次数:0运行复制/* DO NOT EDIT THIS FILE - it is machine generated */

#include

/* 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注释:提示这是一个自动生成的文件,不应手动编辑。代码语言:c代码运行次数:0运行复制/* DO NOT EDIT THIS FILE - it is machine generated */包含头文件:包含了 JNI 的标准头文件 jni.h,这是所有 JNI 程序必须包含的头文件,提供了与 Java 交互所需的所有宏、类型和函数声明。2.2 小节 VS Code已经配置该头文件的位置,因此编写代码的时候是能自动代码提示的。代码语言:c代码运行次数:0运行复制#include 防止重复包含:使用预处理器指令防止头文件被多次包含,确保编译时不会出现重复定义的问题。代码语言:c代码运行次数:0运行复制#ifndef _Included_ltd_dujiabao_jni_tests_HelloWorld

#define _Included_ltd_dujiabao_jni_tests_HelloWorld

//...

#endifC++ 兼容性处理:如果编译环境是 C++,则使用 extern "C" 来确保 C 链接方式,避免名称修饰问题。代码语言:c代码运行次数:0运行复制#ifdef __cplusplus

extern "C" {

#endif

//...

#ifdef __cplusplus

}

#endifJNI 方法声明:这是关键部分,声明了一个名为 Java_ltd_dujiabao_jni_1tests_HelloWorld_helloWorld 的 C 函数。JNIEXPORT 和 JNICALL 是 JNI 宏,分别用于导出符号和指定调用约定。JNIEnv * 是指向 JNI 环境的指针,提供了与 JVM 交互的功能。jobject 是对调用此方法的 Java 对象的引用。Signature: ()V 表示该方法没有参数且返回 void。代码语言:c代码运行次数:0运行复制/*

* 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

代码语言:c代码运行次数:0运行复制#include "ltd_dujiabao_jni_tests_HelloWorld.h"

#include

JNIEXPORT void JNICALL Java_ltd_dujiabao_jni_1tests_HelloWorld_helloWorld(JNIEnv *env, jobject obj)

{

// 打印一条消息到控制台

printf("Hello from JNI!\n");

}包含头文件:#include "ltd_dujiabao_jni_tests_HelloWorld.h": 包含自动生成的头文件,其中定义了JNI函数的声明。#include :标准输入输出库实现函数 Java_ltd_dujiabao_jni_1tests_HelloWorld_helloWorld:printf("Hello from JNI!\n");: 使用标准C库的printf函数在控制台打印一条消息4. 编译生成动态链接库此步骤,将上述编写的C语言代码编译成动态链接库,以供Java程序调用。不同的操作系统的命令有少许差异。

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

代码语言:shell复制gcc -I${JAVA_HOME}/include -I${JAVA_HOME}/include/linux -shared -o libHelloWorld.so ltd_dujiabao_jni_tests_HelloWorld.c在Windows上,动态链接库通常以.dll文件的形式存在。可以使用gcc(例如通过MinGW)来编译生成动态链接库。

代码语言:shell复制gcc -I"%JAVA_HOME%\include" -I"%JAVA_HOME%\include\win32" -shared -o HelloWorld.dll ltd_dujiabao_jni_tests_HelloWorld.c在macOS上,动态链接库通常以.dylib文件的形式存在。你可以使用gcc或clang来编译生成动态链接库。

代码语言:shell复制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,用于指定动态链接库的目录地址

代码语言:shell复制-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数据类型。

代码语言:c代码运行次数:0运行复制/* DO NOT EDIT THIS FILE - it is machine generated */

#include

/* 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. 实现函数代码语言:c代码运行次数:0运行复制#include

#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时,抛异常这段代码。以下是这段代码的详细解释:

查找异常类jclass: 这是一个指向Java类的引用。FindClass: 这是一个JNI函数,用于查找并返回指定名称的Java类的引用。"java/lang/IllegalArgumentException": 这是Java中IllegalArgumentException类的全限定名。illegalArgumentException: 这是一个指向IllegalArgumentException类的指针,用于后续操作。代码语言:c代码运行次数:0运行复制jclass illegalArgumentException = (*env)->FindClass(env, "java/lang/IllegalArgumentException");检查类是否成功找到if (illegalArgumentException != NULL): 检查FindClass函数是否成功找到了IllegalArgumentException类。如果找到了,则illegalArgumentException不为NULL。代码语言:c代码运行次数:0运行复制if (illegalArgumentException != NULL)抛出异常ThrowNew: 这是一个JNI函数,用于抛出一个新的Java异常。illegalArgumentException: 这是要抛出的异常类的引用。"Division by zero": 这是异常的详细消息,描述了异常的原因。代码语言:c代码运行次数:0运行复制(*env)->ThrowNew(env, illegalArgumentException, "Division by zero");返回值return -1;: 在抛出异常后,函数返回一个值。在这个例子中,返回-1。可能这里有人会有疑问,为什么抛异常之后,还要return。这是因为对于C语言代码来说,上述抛异常只是调用了一个函数,并不是异常,如果不return的话,调用完抛异常的函数之后,还会继续往下执行。代码语言:c代码运行次数:0运行复制return -1;从上述代码中,也可以看出JNI是一个非常重要的结构体。可以简单浏览一下,其实就是包含各种JNI函数指针的结构体,可以类比成Java中的接口。

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

代码语言:c代码运行次数:0运行复制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。

代码语言:c代码运行次数:0运行复制/* DO NOT EDIT THIS FILE - it is machine generated */

#include

/* 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对象

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

代码语言:c代码运行次数:0运行复制#include "ltd_dujiabao_jni_tests_StrUtils.h"

#include

#include

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. 输入字符串将Java字符串转换为C字符串GetStringUTFChars: 这是一个JNI函数,用于将Java字符串(jstring)转换为UTF-8编码的C字符串(const char *)。env: JNI环境指针。str: 要转换的Java字符串。0: 这是一个指向jboolean的指针,用于指示是否复制字符串。传递0表示不复制字符串。nativeString: 转换后的C字符串。代码语言:c代码运行次数:0运行复制const char *nativeString = (*env)->GetStringUTFChars(env, str, 0);检查内存分配是否成功if (nativeString == NULL): 检查GetStringUTFChars是否成功返回C字符串。如果返回NULL,表示内存不足或转换失败。return 0: 如果内存不足,返回0作为错误码。代码语言:c代码运行次数:0运行复制if (nativeString == NULL)

{

return 0; // 如果内存不足,返回 0

}计算字符串长度strlen: 这是C标准库中的函数,用于计算C字符串的长度。nativeString: 要计算长度的C字符串。jint: 将strlen的返回值(size_t类型)转换为jint类型。代码语言:c代码运行次数:0运行复制jint length = (jint)strlen(nativeString);释放C字符串ReleaseStringUTFChars: 这是一个JNI函数,用于释放之前通过GetStringUTFChars获取的C字符串。必须释放内存,不然会出现内存泄漏!!env: JNI环境指针。str: 原始的Java字符串。nativeString: 要释放的C字符串。代码语言:c代码运行次数:0运行复制(*env)->ReleaseStringUTFChars(env, str, nativeString);返回字符串长度return length: 返回计算得到的字符串长度。代码语言:c代码运行次数:0运行复制return length;2.3.2. 输出字符串分配内存malloc: 这是C标准库中的函数,用于动态分配内存。(length + 1) * sizeof(char): 分配的内存大小为 length + 1 字节,以容纳字符串的终止符 \0。nativeString: 指向分配的内存的指针。if (nativeString == NULL): 检查内存分配是否成功。如果分配失败,返回 NULL。代码语言:c代码运行次数:0运行复制char *nativeString = (char *)malloc((length + 1) * sizeof(char));

if (nativeString == NULL)

{

return NULL; // 如果内存不足,返回 NULL

}填充字符串for 循环: 遍历从 0 到 length - 1 的索引,将每个位置设置为 value。nativeString[length] = '\0': 在字符串的末尾添加终止符 \0,表示字符串的结束。代码语言:c代码运行次数:0运行复制for (jint i = 0; i < length; i++)

{

nativeString[i] = (char)value;

}

nativeString[length] = '\0'; // 添加终止符将C字符串转换为Java字符串NewStringUTF: 这是一个JNI函数,用于将UTF-8编码的C字符串转换为Java字符串。Java字符串是被JVM管理的,因此不需要考虑内存泄漏的问题。env: JNI环境指针。nativeString: 要转换的C字符串。result: 转换后的Java字符串。代码语言:c代码运行次数:0运行复制jstring result = (*env)->NewStringUTF(env, nativeString);释放C字符串free: 这是C标准库中的函数,用于释放之前分配的内存。nativeString: 要释放的C字符串。代码语言:c代码运行次数:0运行复制free(nativeString);返回Java字符串return result: 返回转换后的Java字符串。代码语言:c代码运行次数:0运行复制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在上一小节已简单介绍,在此不再赘述。

代码语言:c代码运行次数:0运行复制/* DO NOT EDIT THIS FILE - it is machine generated */

#include

/* 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

#endif3.3. 实现函数代码语言:c代码运行次数:0运行复制#include "ltd_dujiabao_jni_tests_PointUtils.h"

#include

#include

#include

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, "", "(DD)V");

// 创建Point对象

jobject pointObj = (*env)->NewObject(env, pointClass, constructorID, x, y);

return pointObj;

}3.3.1. 结构体Point结构体定义typedef: 这是一个关键字,用于为类型创建一个新的名称。struct: 这是一个关键字,用于定义结构体。{ ... }: 结构体的主体部分,包含结构体的成员。double x;: 结构体的第一个成员,表示 x 坐标,类型为 double。double y;: 结构体的第二个成员,表示 y 坐标,类型为 double。Point: 结构体的新名称,用于后续引用该结构体。代码语言:c代码运行次数:0运行复制typedef struct

{

double x;

double y;

} Point;3.3.2. Java对象转换为结构体 getPoint分配内存malloc: 这是C标准库中的函数,用于动态分配内存。sizeof(Point): 分配的内存大小为 Point 结构体的大小。p: 指向分配的内存的指针。if (p == NULL): 检查内存分配是否成功。如果分配失败,返回 NULL。代码语言:c代码运行次数:0运行复制Point *p = (Point *)malloc(sizeof(Point));

if (p == NULL)

{

return NULL; // 如果内存不足,返回 NULL

}获取Java类代码语言:c代码运行次数:0运行复制jclass pointClazz = (*env)->GetObjectClass(env, point);GetObjectClass: 这是一个JNI函数,用于获取指定Java对象的类。env: JNI环境指针。point: Java对象实例。pointClazz: 获取到的Java类的引用。获取方法IDGetMethodID: 这是一个JNI函数,用于获取指定类的方法ID。env: JNI环境指针。pointClazz: Java类的引用。"getX" 和 "getY": 方法名称。"()D": 方法签名,表示方法没有参数且返回一个 double 类型的值。getX_method_id 和 getY_method_id: 获取到的方法ID。代码语言:c代码运行次数:0运行复制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

调用Java方法CallDoubleMethod: 这是一个JNI函数,用于调用Java对象的 double 类型的方法。env: JNI环境指针。point: Java对象实例。getX_method_id 和 getY_method_id: 方法ID。p->x 和 p->y: 将调用方法返回的值存储到 Point 结构体的相应字段中。代码语言:c代码运行次数:0运行复制p->x = (*env)->CallDoubleMethod(env, point, getX_method_id);

p->y = (*env)->CallDoubleMethod(env, point, getY_method_id);返回 Point 结构体指针return p: 返回指向 Point 结构体的指针。代码语言:c代码运行次数:0运行复制return p;JNI调用Java方法的方式,可以总结为几步:获取类,再根据类、方法名获取方法id,最终传入对象、方法名调用方法。和Java反射有那么一点相似。

3.3.3. 创建对象 Java_ltd_dujiabao_jni_1tests_PointUtils_newPoint获取Java类FindClass: 这是一个JNI函数,用于查找并返回指定名称的Java类的引用。env: JNI环境指针。"ltd/dujiabao/jni_tests/Point": 这是Java中Point类的全限定名,使用斜杠/分隔包名和类名。pointClass: 获取到的Point类的引用。代码语言:c代码运行次数:0运行复制jclass pointClass = (*env)->FindClass(env, "ltd/dujiabao/jni_tests/Point");获取构造方法IDGetMethodID: 这是一个JNI函数,用于获取指定类的方法ID。env: JNI环境指针。pointClass: Java类的引用。"": 这是构造方法的名称,构造方法的名称固定为。"(DD)V": 这是构造方法的签名,表示构造方法有两个double类型的参数且没有返回值。constructorID: 获取到的构造方法ID。代码语言:c代码运行次数:0运行复制jmethodID constructorID = (*env)->GetMethodID(env, pointClass, "", "(DD)V");创建Java对象NewObject: 这是一个JNI函数,用于创建一个新的Java对象。env: JNI环境指针。pointClass: Java类的引用。constructorID: 构造方法ID。x 和 y: 传递给构造方法的参数。pointObj: 创建的Java对象。代码语言:c代码运行次数:0运行复制jobject pointObj = (*env)->NewObject(env, pointClass, constructorID, x, y);返回Java对象return pointObj: 返回创建的Java对象。代码语言:c代码运行次数:0运行复制return pointObj;4. 调用已有C/C++代码库对于已有代码库,有几种方式可以调用:

JNI代码作为桥接程序,和已有的本地代码的源码一起编译成一个动态链接库JNI代码作为桥接程序编译成一个动态链接库,已有本地代码提供另外的动态链接库第一种方式实际上和上面代码示例差别不大,在此不再赘述。本小节仅介绍第二种方式。

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

代码语言:c代码运行次数:0运行复制#ifndef FOUR_OPERATIONS_H

#define FOUR_OPERATIONS_H

#include

// 加法

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定义了本地方法

代码语言:Java复制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头文件为:

代码语言:c代码运行次数:0运行复制/* DO NOT EDIT THIS FILE - it is machine generated */

#include

/* 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的头文件将其实现,引入现有本地方法库的头文件,调用其函数。

代码语言:c代码运行次数:0运行复制#include

#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);

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

代码语言:shell复制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的头文件。

相关文章