Java使用JNI调用动态链接库

项目中有个需求,需要根据用户上传的图片分析出用户的一些面部特征,血压,血氧等用户体征数据,Java层接收用户上传图片,保存,然后将图片地址传递给C++算法去处理,算法层计算出结果并返回给Java层一个对象。这里做个简单的demo模拟这个需求,大概需要的技术有C++,JNI Java等。

模拟C++算法层并编写动态链接库

环境搭建

先下载VC_redist.x64.exe
我们这里使用Clion进行开发,下载并安装Clion,MinGW交叉编译工具并配置。我这里直接配置的CodeBlock的MinGW地址。如下图所示

有了MinGW地址后,为了方便后面使用命令行,可以将MinGW添加到系统环境变量中。然后在Clion中配置交叉编译环境。


配置完成后就可以开始开发了。

动态库开发

在Clion中新建项目,库类型选择共享库,如下所示

创建完项目会生成一个头文件,一个源文件,还有一个cmakelist脚本文件。
这里我们编写一个对象供JNI调用即可,代码如下

  • 头文件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#ifndef PERSON_H
#define PERSON_H
#include <iostream>


class Person
{
public:
Person();
Person(std::string name,int age, bool sex);
virtual ~Person();
void show();
std::string getName();
int getAge();
bool getSex();
private:
std::string name;
int age;
bool sex;
};

#endif // PERSON_H
  • 源文件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
#include "Person.h"
#include <iostream>
Person::Person()
{
this->name = "张三";
this->age = 20;
this->sex = true;
std::cout << "user name is "<<this->name<<" age is "<<this->age<<" sex is "<<this->sex<<std::endl;
}

Person::~Person()
{
//dtor
}

Person::Person(std::string name,int age, bool sex){
this->name = name;
this->age = age;
this->sex = sex;
std::cout << "user name is "<<this->name<<" age is "<<this->age<<" sex is "<<this->sex<<std::endl;
}

std::string Person::getName() {
return name;
}

int Person::getAge() {
return age;
}

bool Person::getSex() {
return sex;
}

void Person::show() {
std::cout << "user name is "<<name<<" age is "<<age<<" sex is "<<sex<<std::endl;
}

void hello() {
std::cout << "Hello, World!" << std::endl;
}
  • 编译脚本
1
2
3
4
cmake_minimum_required(VERSION 3.18)
project(person)
set(CMAKE_CXX_STANDARD 11)
add_library(person SHARED Person.cpp)

点击build既可生成文件libperson.dll,这个文件是windows下的动态链接库文件

如果需要打Linux下使用的SO文件,可以将代码放到linux下执行,Clion支持远程调试功能

1
2
3
4
5
sudo apt install g++ gcc gdb cmake
mkdir build
cd build
cmake ../
make

Java JNI代码生成

Java调用C++可以使用动态和静态注册,我们这里只提供静态注册的使用方式,关于动态注册可以在网上查询。首先我们需要编写一个Java类,并通过javac将这个Java类生成JNI需要的头文件。

1
2
3
4
5
6
7
8
9
10
11
12
13
public class SharedJNI {
// 根据传入的参数返回我们需要的一个Java对象Person
public native Person init(String name,Integer age,Boolean sex);
}

@Data
@NoArgsConstructor
@AllArgsConstructor
public class Person {
private String name;
private Integer age;
private Boolean sex;
}

在java包目录下调用如下命令

1
javah -encoding utf8 -classpath  . com.mayahx.pexm.app.SharedJNI

调用命令后会生成如下文件,我们可以对头文件进行重命名,搞得短一些

文件内容如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class com_mayahx_pexm_app_SharedJNI */

#ifndef _Included_com_mayahx_pexm_app_SharedJNI
#define _Included_com_mayahx_pexm_app_SharedJNI
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class: com_mayahx_pexm_app_SharedJNI
* Method: init
* Signature: (Ljava/lang/String;Ljava/lang/Integer;Ljava/lang/Boolean;)Lcom/mayahx/pexm/app/Person;
*/
JNIEXPORT jobject JNICALL Java_com_mayahx_pexm_app_SharedJNI_init
(JNIEnv *, jobject, jstring, jobject, jobject);

#ifdef __cplusplus
}
#endif
#endif

生成JNI头文件后,我们就可以根据这个头文件编写Java所需要的动态链接库了。

编写JNI层并生成动态链接库

我们再次创建一个动态链接库项目,命名为shared。然后将我们需要的Person.h,还有上一步生成的jni头文件加入到项目中,然后还需要将C++算法层所生成的so文件放在项目中。项目结构如下

其中jni和jni_md头文件在JDK的安装目录下的include文件夹和include/system32文件夹中,复制到项目即可。

现在我们就可以编写Java所需的动态链接库代码了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177

#include "SharedJNI.h"
#include "Person.h"
#include<iostream>
/*
* Class: com_mayahx_pexm_app_SharedJNI
* Method: init
* Signature: (Ljava/lang/String;Ljava/lang/Integer;Ljava/lang/Boolean;)Lcom/mayahx/pexm/app/Person;
*/
jobject intToInteger(JNIEnv *env, int i);

extern "C"
JNIEXPORT jobject JNICALL Java_com_mayahx_pexm_app_SharedJNI_init
(JNIEnv *env, jobject instance, jstring name_, jobject age_, jobject sex_) {
//-----------------根据c++处理得到一个对象-----------------------------------//
int ageV = 0;
bool sexV = false;
const char *name_char;
name_char = env->GetStringUTFChars(name_, nullptr);
std::string name = name_char;
jclass age = env->GetObjectClass(age_);
jfieldID ifd;
if (age) {
ifd = env->GetFieldID(age, "value", "I");
ageV = (int) env->GetIntField(age_, ifd);
}
jclass sex = env->GetObjectClass(sex_);
if (sex) {
jfieldID sfd = env->GetFieldID(sex, "value", "Z");
sexV = (bool) env->GetBooleanField(sex_, sfd);
}
Person *person = new Person(name, ageV, sexV);

//-----------------将这个对象映射为Java对象-----------------------------------//

jclass clz = env->FindClass("com/mayahx/pexm/app/Person");
if (clz == nullptr) {
std::cout << "clz == null" << std::endl;
return nullptr;
}
jmethodID mid = env->GetMethodID(clz, "<init>", "()V");
if (mid == nullptr) {
std::cout << "mid == null" << std::endl;
return nullptr;
}
jobject obj = env->NewObject(clz, mid);
if (obj == nullptr) {
std::cout << "obj == null" << std::endl;
return nullptr;
}

jfieldID ageFd = env->GetFieldID(clz, "age", "Ljava/lang/Integer;");
if (ageFd == nullptr) {
std::cout << "ageFd == null" << std::endl;
return nullptr;
}
env->SetObjectField(obj, ageFd, intToInteger(env, person->getAge()));

jfieldID sexFd = env->GetFieldID(clz, "sex", "Ljava/lang/Boolean;");
if (sexFd == nullptr){
std::cout << "sexFd == null" << std::endl;
return nullptr;
}
env->SetObjectField(obj, sexFd, sex_);

jfieldID nameFd = env->GetFieldID(clz, "name", "Ljava/lang/String;");
if (nameFd == nullptr){
std::cout << "nameFd == null" << std::endl;
return nullptr;
}
env->SetObjectField(obj, nameFd, env->NewStringUTF(name_char));

// 删除局部引用
env->DeleteLocalRef(age);
env->DeleteLocalRef(sex);
delete person;
return obj;
}

// c++ int to Integer
jobject intToInteger(JNIEnv *env, int v){
jclass clz = env->FindClass("java/lang/Integer");
if (clz == nullptr) {
std::cout << "clz == null" << std::endl;
return nullptr;
}
jmethodID mid = env->GetMethodID(clz, "<init>", "(I)V");
if (mid == nullptr) {
std::cout << "mid == null" << std::endl;
return nullptr;
}
jobject obj = env->NewObject(clz, mid,v);
if (obj == nullptr) {
std::cout << "obj == null" << std::endl;
return nullptr;
}
return obj;
}

// c++ int to java Integer
jobject longToLong(JNIEnv *env,long v){
jclass clz = env->FindClass("java/lang/Long");
if (clz == nullptr) {
std::cout << "clz == null" << std::endl;
return nullptr;
}
jmethodID mid = env->GetMethodID(clz, "<init>", "(J)V");
if (mid == nullptr) {
std::cout << "mid == null" << std::endl;
return nullptr;
}
jobject obj = env->NewObject(clz, mid,v);
if (obj == nullptr) {
std::cout << "obj == null" << std::endl;
return nullptr;
}
return obj;
}
// c++ float to java Float
jobject floatToFloat(JNIEnv *env, float v){
jclass clz = env->FindClass("java/lang/Float");
if (clz == nullptr) {
std::cout << "clz == null" << std::endl;
return nullptr;
}
jmethodID mid = env->GetMethodID(clz, "<init>", "(F)V");
if (mid == nullptr) {
std::cout << "mid == null" << std::endl;
return nullptr;
}
jobject obj = env->NewObject(clz, mid,v);
if (obj == nullptr) {
std::cout << "obj == null" << std::endl;
return nullptr;
}
return obj;
}

// c++ bool to java Boolean
jobject boolToBoolean(JNIEnv *env,bool v){
jclass clz = env->FindClass("java/lang/Boolean");
if (clz == nullptr) {
std::cout << "clz == null" << std::endl;
return nullptr;
}
jmethodID mid = env->GetMethodID(clz, "<init>", "(Z)V");
if (mid == nullptr) {
std::cout << "mid == null" << std::endl;
return nullptr;
}
jobject obj = env->NewObject(clz, mid,v);
if (obj == nullptr) {
std::cout << "obj == null" << std::endl;
return nullptr;
}
return obj;
}

// c++ double to java Double
jobject doubleToDouble(JNIEnv *env,int v){
jclass clz = env->FindClass("java/lang/Double");
if (clz == nullptr) {
std::cout << "clz == null" << std::endl;
return nullptr;
}
jmethodID mid = env->GetMethodID(clz, "<init>", "(D)V");
if (mid == nullptr) {
std::cout << "mid == null" << std::endl;
return nullptr;
}
jobject obj = env->NewObject(clz, mid,v);
if (obj == nullptr) {
std::cout << "obj == null" << std::endl;
return nullptr;
}
return obj;
}

上面的代码需要一些知识,比如JNI一些数据类型与C++对应,还有方法签名,字段签名等等。可以参考
JNI类型签名和方法签名,这里就不说明了。
除了编写需要的功能代码,我们还需要配置cmake编译脚本,如下所示,具体的指令都添加了注释

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
cmake_minimum_required(VERSION 3.18)
project(shared)

set(CMAKE_CXX_STANDARD 11)
#设置头文件路径
set(INC_DIR ./include)
#设置链接库路径
set(LINK_DIR ./lib)
#引入头文件
include_directories(${INC_DIR})
#引入库文件
link_directories(${LINK_DIR})

add_library(shared SHARED src/SharedJNI.cpp)
# linux
#target_link_libraries(shared libperson.so)
# windows
target_link_libraries(shared libperson.dll)

点击build即可生成libshared.dll动态链接库。

Java调用动态链接库

我们将上面生成的libperson.dll和libshared.dll拷贝到项目中,如下图所示

然后编写代码加载这两个文件

1
2
3
4
5
6
7
public class SharedJNI {
static {
System.load("D:\\Java\\pexm-server\\pexm-app\\libperson.dll");
System.load("D:\\Java\\pexm-server\\pexm-app\\libshared.dll");
}
public native Person init(String name,Integer age,Boolean sex);
}

编写Controller以用户调用返回

1
2
3
4
5
6
7
8
9
10
11
@GetMapping(value = "/jni")
public Result<Person> getPerson(){
SharedJNI jni = new SharedJNI();
Person person = jni.init("逗比",25,false);
try {
new ObjectMapper().writeValueAsString(person);
} catch (JsonProcessingException e) {
e.printStackTrace();
}
return Result.ok(person);
}

调用结果如下

总结

本篇文章主要模拟Java调用C++并返回一个对象是代码流程,包括动态链接库编写,打包。JNI的生成,调用等等。实际过程可能比这复杂,不过我们跑通了流程也就简单多了,后面只需要根据需求拓展即可。如果后面还有JNI新的内容再进行更新。

作者

Labradors

发布于

2022-02-20

更新于

2022-03-10

许可协议

评论