一、引言
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();
}
- 本地方法声明,表示该方法的具体实现不在 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
- 注释:提示这是一个自动生成的文件,不应手动编辑。
/* DO NOT EDIT THIS FILE - it is machine generated */
- 包含头文件:包含了 JNI 的标准头文件
jni.h
,这是所有 JNI 程序必须包含的头文件,提供了与 Java 交互所需的所有宏、类型和函数声明。2.2 小节 VS Code已经配置该头文件的位置,因此编写代码的时候是能自动代码提示的。
#include <jni.h>
- 防止重复包含:使用预处理器指令防止头文件被多次包含,确保编译时不会出现重复定义的问题。
#ifndef _Included_ltd_dujiabao_jni_tests_HelloWorld
#define _Included_ltd_dujiabao_jni_tests_HelloWorld
//...
#endif
- C++ 兼容性处理:如果编译环境是 C++,则使用
extern "C"
来确保 C 链接方式,避免名称修饰问题。
#ifdef __cplusplus
extern "C" {
#endif
//...
#ifdef __cplusplus
}
#endif
- JNI 方法声明:
- 这是关键部分,声明了一个名为
Java_ltd_dujiabao_jni_1tests_HelloWorld_helloWorld
的 C 函数。 JNIEXPORT
和JNICALL
是 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");
}
- 包含头文件:
#include "ltd_dujiabao_jni_tests_HelloWorld.h"
: 包含自动生成的头文件,其中定义了JNI函数的声明。#include <stdio.h>
:标准输入输出库
- 实现函数
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时,抛异常这段代码。以下是这段代码的详细解释:
- 查找异常类
jclass
: 这是一个指向Java类的引用。FindClass
: 这是一个JNI函数,用于查找并返回指定名称的Java类的引用。"java/lang/IllegalArgumentException"
: 这是Java中IllegalArgumentException
类的全限定名。illegalArgumentException
: 这是一个指向IllegalArgumentException
类的指针,用于后续操作。
jclass illegalArgumentException = (*env)->FindClass(env, "java/lang/IllegalArgumentException");
- 检查类是否成功找到
if (illegalArgumentException != NULL)
: 检查FindClass
函数是否成功找到了IllegalArgumentException
类。如果找到了,则illegalArgumentException
不为NULL
。
if (illegalArgumentException != NULL)
- 抛出异常
ThrowNew
: 这是一个JNI函数,用于抛出一个新的Java异常。illegalArgumentException
: 这是要抛出的异常类的引用。"Division by zero"
: 这是异常的详细消息,描述了异常的原因。
(*env)->ThrowNew(env, illegalArgumentException, "Division by zero");
- 返回值
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对象
struct _jobject
:这是一个不完整的结构体声明,表示Java对象的内部结构。_jobject
是一个不透明的结构体,JNI用户不需要知道其内部细节。typedef struct _jobject *jobject;
:这是一个指向_jobject
结构的指针,表示一个通用的Java对象。jobject
是JNI中所有对象类型的基类型。- 类型别名:为了更好地表示不同的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. 输入字符串
- 将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);
- 检查内存分配是否成功
if (nativeString == NULL)
: 检查GetStringUTFChars
是否成功返回C字符串。如果返回NULL
,表示内存不足或转换失败。return 0
: 如果内存不足,返回0作为错误码。
if (nativeString == NULL)
{return 0; // 如果内存不足,返回 0
}
- 计算字符串长度
strlen
: 这是C标准库中的函数,用于计算C字符串的长度。nativeString
: 要计算长度的C字符串。jint
: 将strlen
的返回值(size_t
类型)转换为jint
类型。
jint length = (jint)strlen(nativeString);
- 释放C字符串
ReleaseStringUTFChars
: 这是一个JNI函数,用于释放之前通过GetStringUTFChars
获取的C字符串。必须释放内存,不然会出现内存泄漏!!env
: JNI环境指针。str
: 原始的Java字符串。nativeString
: 要释放的C字符串。
(*env)->ReleaseStringUTFChars(env, str, nativeString);
- 返回字符串长度
return length
: 返回计算得到的字符串长度。
return length;
2.3.2. 输出字符串
- 分配内存
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
}
- 填充字符串
for
循环: 遍历从0
到length - 1
的索引,将每个位置设置为value
。nativeString[length] = '\0'
: 在字符串的末尾添加终止符\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字符串。
jstring result = (*env)->NewStringUTF(env, nativeString);
- 释放C字符串
free
: 这是C标准库中的函数,用于释放之前分配的内存。nativeString
: 要释放的C字符串。
free(nativeString);
- 返回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
- 结构体定义
typedef
: 这是一个关键字,用于为类型创建一个新的名称。struct
: 这是一个关键字,用于定义结构体。{ ... }
: 结构体的主体部分,包含结构体的成员。double x;
: 结构体的第一个成员,表示x
坐标,类型为double
。double y;
: 结构体的第二个成员,表示y
坐标,类型为double
。Point
: 结构体的新名称,用于后续引用该结构体。
typedef struct
{double x;double y;
} Point;
3.3.2. Java对象转换为结构体 getPoint
- 分配内存
malloc
: 这是C标准库中的函数,用于动态分配内存。sizeof(Point)
: 分配的内存大小为Point
结构体的大小。p
: 指向分配的内存的指针。if (p == NULL)
: 检查内存分配是否成功。如果分配失败,返回NULL
。
Point *p = (Point *)malloc(sizeof(Point));
if (p == NULL)
{return NULL; // 如果内存不足,返回 NULL
}
- 获取Java类
jclass pointClazz = (*env)->GetObjectClass(env, point);
GetObjectClass
: 这是一个JNI函数,用于获取指定Java对象的类。env
: JNI环境指针。point
: Java对象实例。pointClazz
: 获取到的Java类的引用。
- 获取方法ID
GetMethodID
: 这是一个JNI函数,用于获取指定类的方法ID。env
: JNI环境指针。pointClazz
: Java类的引用。"getX"
和"getY"
: 方法名称。"()D"
: 方法签名,表示方法没有参数且返回一个double
类型的值。getX_method_id
和getY_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
- 调用Java方法
CallDoubleMethod
: 这是一个JNI函数,用于调用Java对象的double
类型的方法。env
: JNI环境指针。point
: Java对象实例。getX_method_id
和getY_method_id
: 方法ID。p->x
和p->y
: 将调用方法返回的值存储到Point
结构体的相应字段中。
p->x = (*env)->CallDoubleMethod(env, point, getX_method_id);
p->y = (*env)->CallDoubleMethod(env, point, getY_method_id);
- 返回
Point
结构体指针
return p
: 返回指向Point
结构体的指针。
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
类的引用。
jclass pointClass = (*env)->FindClass(env, "ltd/dujiabao/jni_tests/Point");
- 获取构造方法ID
GetMethodID
: 这是一个JNI函数,用于获取指定类的方法ID。env
: JNI环境指针。pointClass
: Java类的引用。"<init>"
: 这是构造方法的名称,构造方法的名称固定为<init>
。"(DD)V"
: 这是构造方法的签名,表示构造方法有两个double
类型的参数且没有返回值。constructorID
: 获取到的构造方法ID。
jmethodID constructorID = (*env)->GetMethodID(env, pointClass, "<init>", "(DD)V");
- 创建Java对象
NewObject
: 这是一个JNI函数,用于创建一个新的Java对象。env
: JNI环境指针。pointClass
: Java类的引用。constructorID
: 构造方法ID。x
和y
: 传递给构造方法的参数。pointObj
: 创建的Java对象。
jobject pointObj = (*env)->NewObject(env, pointClass, constructorID, x, y);
- 返回Java对象
return pointObj
: 返回创建的Java对象。
return pointObj;
4. 调用已有C/C++代码库
对于已有代码库,有几种方式可以调用:
- JNI代码作为桥接程序,和已有的本地代码的源码一起编译成一个动态链接库
- 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的头文件。