Java JNI在Windows平台和Linux平台调用本地C/C++代码
前言
Java JNI(Java Native Interface)是Java平台的一部分,它允许Java代码与用其他编程语言(如C或C++)编写的本地代码进行交互。JNI提供了一组API,使Java代码可以调用本地代码,反之亦然。
使用JNI的常见场景包括:
- 性能优化:在某些情况下,使用C或C++编写的本地代码可能比Java代码更高效。通过JNI,可以在性能关键的部分使用本地代码来提高性能。
- 访问操作系统特性:某些操作系统特性或硬件接口只能通过本地代码访问。JNI允许Java程序调用这些特性。
- 重用现有库:许多现有的C/C++库提供了丰富的功能。通过JNI,Java程序可以利用这些库而无需重新实现相同的功能。
JNI的基本工作流程为:
- 声明本地方法,即将某些方法声明为
native
方法 - 生成头文件
- 实现本地方法
- 编译本地代码为动态链接库
- 调用本地代码
下面将在Windows和Linux平台做演示。
Java 代码准备
声明方法
编辑代码,路径为:
C:\Users\Yalexin\IdeaProjects\hello-world\src\top\yalexin\jni\HelloJNI.java
内容为:
package top.yalexin.jni;
public class HelloJNI {
static {
// helloJNI.dll in Windows, libhelloJNI.so in Unixes
System.loadLibrary("helloJNI");
}
private native int pow2(int value);
public static void main(String[] args) {
HelloJNI helloJNI = new HelloJNI();
System.out.println("pow2(5) -> " + helloJNI.pow2(5));
System.out.println("pow2(15) -> " + helloJNI.pow2(15));
}
}
生成头文件
生成对应的头文件,期间使用到JDK提供的javah
,使用方式为:
javah -classpath <CLASS_PATH> -d <OUT_PATH> CLASS_NAME
对于上述例子,我们可以在路径C:\Users\Yalexin\IdeaProjects\hello-world
下执行
javah -classpath C:\Users\Yalexin\IdeaProjects\hello-world\src -d . top.yalexin.jni.HelloJNI
注意项目所在的路径是
C:\Users\Yalexin\IdeaProjects\hello-world\src
然后我们就可以在路径C:\Users\Yalexin\IdeaProjects\hello-world
下生成一个头文件top_yalexin_jni_HelloJNI.h
:
/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class top_yalexin_jni_HelloJNI */
#ifndef _Included_top_yalexin_jni_HelloJNI
#define _Included_top_yalexin_jni_HelloJNI
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class: top_yalexin_jni_HelloJNI
* Method: pow2
* Signature: (I)I
*/
JNIEXPORT jint JNICALL Java_top_yalexin_jni_HelloJNI_pow2
(JNIEnv *, jobject, jint);
#ifdef __cplusplus
}
#endif
#endif
该头文件名字实际上就是以包命作为前缀,将.
替换为下划线,同时接上类名.
而之前的native
方法则变成了JAVA_${package}_${className}_${method}
.
生成字节码
我们先生成对应的字节码:
cd C:\Users\Yalexin\IdeaProjects\hello-world\src\top\yalexin\jni>
javac HelloJNI.java
Windows实现
这里借助VS2022完成:
新建一个空白项目:
然后将刚刚生成的头文件拷贝至项目文件夹中:
然后右击 【头文件】, 添加现有项,添加刚刚复制的头文件
然后我们右键源文件,添加一个Pow2DLL.cpp
文件,文件内容为:
#include "top_yalexin_jni_HelloJNI.h"
JNIEXPORT jint JNICALL Java_top_yalexin_jni_HelloJNI_pow2
(JNIEnv* env, jobject thisObj, jint value) {
return value * value;
}
其实主要是把头文件引入,以及将对应的方法给实现了.
为了能够引入jni.h
文件,我们需要包含以及引用JDK目录.
右键我们的项目,点击【属性】
然后将配置类型修改为【动态链接库】,即项目最终生成一个dll
文件.
然后我们直接构建:
我们就可以得到以下输出:
生成开始于 23:49...
1>------ 已启动生成: 项目: Pow2DLL, 配置: Debug x64 ------
1>Pow2DLL.cpp
1> 正在创建库 C:\Users\Yalexin\source\repos\Pow2DLL\x64\Debug\Pow2DLL.lib 和对象 C:\Users\Yalexin\source\repos\Pow2DLL\x64\Debug\Pow2DLL.exp
1>Pow2DLL.vcxproj -> C:\Users\Yalexin\source\repos\Pow2DLL\x64\Debug\Pow2DLL.dll
========== 生成: 1 成功,0 失败,0 最新,0 已跳过 ==========
========== 生成 于 23:49 完成,耗时 00.542 秒 ==========
我们把生成的dll
文件拷贝至字节码所在路径,并重命名为helloJNI.dll
.
确保文件如下:
$ tree
.
|-- HelloJNI.class
|-- HelloJNI.java
`-- helloJNI.dll
0 directories, 3 files
然后就可以验证了:
java -Djava.library.path=top\yalexin\jni top.yalexin.jni.HelloJNI
第一个参数是设定动态链接库所在的地址。
Linux实现
将之前生成头文件和要实现的C++代码放到统一个文件夹:
$ tree
.
├── helloJNI.cpp
└── top_yalexin_jni_HelloJNI.h
0 directories, 2 files
编辑helloJNI.cpp
#include "top_yalexin_jni_HelloJNI.h"
JNIEXPORT jint JNICALL Java_top_yalexin_jni_HelloJNI_pow2
(JNIEnv* env, jobject thisObj, jint value) {
return value * value;
}
编辑Makefile
文件,帮助我们快速构建.so
文件
libhelloJNI.so: helloJNI.cpp
g++ -o $@ $+ -fPIC -shared -I$(JAVA_HOME)/include -I$(JAVA_HOME)/include/linux
.PHONY : clean
clean :
-rm libhelloJNI.so
上述要求我们配置好JAVA_HOME
变量。
echo $JAVA_HOME
/usr/java/jdk1.8.0_411
其外,由于在Linux下,库文件都是以lib
开头,以.so
结尾,因此对于:
System.loadLibrary("helloJNI");
系统会尝试寻找libhelloJNI.so
文件。
我们运行make
,生成库文件:
pwd
/root/jni
tree
.
├── helloJNI.cpp
├── libhelloJNI.so
├── Makefile
├── top
│ └── yalexin
│ └── jni
│ ├── HelloJNI.class
│ └── HelloJNI.java
└── top_yalexin_jni_HelloJNI.h
验证:
java -Djava.library.path='/root/jni' top.yalexin.jni.HelloJNI
pow2(5) -> 25
pow2(15) -> 225
总结
当然,Java中调用其他语言就难免存在许多不可控因素,有利有弊。
优点:
- 性能提升:在性能关键的部分使用本地代码可以提高效率。
- 访问特定功能:可以访问Java中无法直接访问的操作系统功能和硬件接口。
- 重用现有库:可以利用已有的C/C++库,避免重新实现复杂功能。
缺点:
- 复杂性增加:需要编写和维护C/C++代码,增加了开发复杂度。
- 平台依赖:本地代码通常是平台特定的,需要为不同平台编译和测试。
- 安全性风险:不当使用JNI可能会导致安全漏洞和内存泄漏。
总之,JNI是Java与本地代码交互的重要工具,但在使用时需要慎重考虑其带来的复杂性和潜在问题。
本文由「黄阿信」创作,创作不易,请多支持。
如果您觉得本文写得不错,那就点一下「赞赏」请我喝杯咖啡~
商业转载请联系作者获得授权,非商业转载请附上原文出处及本链接。
关注公众号,获取最新动态!