要么改变世界,要么适应世界

Java JNI在Windows平台和Linux平台调用本地C/C++代码

2024-06-02 19:58:16
0
目录

前言

Java JNI(Java Native Interface)是Java平台的一部分,它允许Java代码与用其他编程语言(如C或C++)编写的本地代码进行交互。JNI提供了一组API,使Java代码可以调用本地代码,反之亦然。

使用JNI的常见场景包括:

  1. 性能优化:在某些情况下,使用C或C++编写的本地代码可能比Java代码更高效。通过JNI,可以在性能关键的部分使用本地代码来提高性能。
  2. 访问操作系统特性:某些操作系统特性或硬件接口只能通过本地代码访问。JNI允许Java程序调用这些特性。
  3. 重用现有库:许多现有的C/C++库提供了丰富的功能。通过JNI,Java程序可以利用这些库而无需重新实现相同的功能。

JNI的基本工作流程为:

  1. 声明本地方法,即将某些方法声明为native方法
  2. 生成头文件
  3. 实现本地方法
  4. 编译本地代码为动态链接库
  5. 调用本地代码

下面将在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完成:

新建一个空白项目:

image-20240601232417062

image-20240601232445510

然后将刚刚生成的头文件拷贝至项目文件夹中:

image-20240601232647371

然后右击 【头文件】, 添加现有项,添加刚刚复制的头文件

image-20240601232808055

然后我们右键源文件,添加一个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目录.

右键我们的项目,点击【属性】

image-20240601234227295

image-20240601234354861

然后将配置类型修改为【动态链接库】,即项目最终生成一个dll文件.

image-20240601234743726

然后我们直接构建:

image-20240601235058407

我们就可以得到以下输出:

生成开始于 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

第一个参数是设定动态链接库所在的地址。

image-20240602000427085

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

image-20240602200617640

总结

当然,Java中调用其他语言就难免存在许多不可控因素,有利有弊。

优点

  • 性能提升:在性能关键的部分使用本地代码可以提高效率。
  • 访问特定功能:可以访问Java中无法直接访问的操作系统功能和硬件接口。
  • 重用现有库:可以利用已有的C/C++库,避免重新实现复杂功能。

缺点

  • 复杂性增加:需要编写和维护C/C++代码,增加了开发复杂度。
  • 平台依赖:本地代码通常是平台特定的,需要为不同平台编译和测试。
  • 安全性风险:不当使用JNI可能会导致安全漏洞和内存泄漏。

总之,JNI是Java与本地代码交互的重要工具,但在使用时需要慎重考虑其带来的复杂性和潜在问题。

历史评论
开始评论