基于JNI实现调用C++ SDK
- 背景
- 分析
- 解决
- 实践
背景
上篇文章总结了几种Java项目调用C/C++ SDK项目方法,在逐一实践、踩坑后,最终还是敲定采用 JNI 方式进行实现。在文章开始的过程,会先大概讲讲笔者遇到的情况,因为封装方式需要根据实际项目而定,可能不太适合于任何人。
分析
如图,这里列举了一个C++项目的头部文件,我们可以看到第一个方法,它返回的是一个GopHandle
对象,而这个对象里有什么属性呢?笔者也不得而知,它的具体定义在一个被打包好的动态库里面,而且这里模板函数方式的格式,也让笔者无从下手。
这里总结了笔者遇到过的一些问题及解决方式:
-
C++模板函数请求的映射:目前通过各种渠道资源的搜索(chatgpt、google等社区)及外部求助,暂时还未找出好的解决方式。
-
关于
std::string &data_root
类型映射 :这个在用JNA实践过程中,在C++实际操作字符串时(如简单的读取值)会出现 invalid memory access 问题。猜测&data_root
这种方式的传递,JNA传入的时string的地址,而C++ 对于这个地址的访问属于跨内存访问,去掉&
,C++可正常访问 -
对象指针的映射:虽然JNA提供了Pointer 指针类,但在实践过程中,这个 Pointer 对 基础类型(int、short、byte)的指针生效。对于对象类类型的指针,需要我们在Java层面,设计一个包含一摸一样的属性的 class 类去对应 C++ 的 结构体或者class 类。
以上图为例子,我们需要设计一个Java版本的 GopHandle类去对应 C++ 的 GopHandle,但GopHandle 封装的一个结构体或类,其中信息不得而知。
解决
这里GitHub找了很Java调用C++ SDK项目作参考,最终还是敲定 JNI 去实现,对于上面遇到的一些问题,也想出了解决思路:
- 如果C++ 方法返回的指针,可以将其放到中间层,统一转为整形表示。(指针通常被表示为一个64位整数,可以用来存储内存地址。通过将指针转换为整数类型,可以在需要时保存或传递指针的值,而不需要直接操作指针。这点来自GPT,理性看待)
- 传参时,对于指针传参,统一放到中间层,用C去实现,Java还是采用对应的数据类型传输。
- 关于模板函数的传参,同样统一放到中间层,用C去实现调用。
思路理清了,接下来就是撸代码了,以下是一些可能写得不太好C代码(现学现卖),但是能用。(手动狗头)
实践
- Java项目里,根据头部文件定义本地方法:SentScore.java
- 生成头部文件
javac -h . SentScore.java
- 用C 实现头部文件接口,在这里才去真正调用C++接口
- 注意!!! 在这里对于内存的操作,不属于JVM的管理范畴,需要调用ReleaseStringUTFChars 手动释放内存,才不会有内存泄漏的风险。
- 如下,是几个方法的例子:
#include "SentScore.h"
#include "SentScore.h"
#include "SentScoreSDK.h"
#include <vector>
#undef __request
#define __request EnglishSentPronsRequest
JNIEXPORT jlong JNICALL Java_SentScore_HandleCreate(JNIEnv *env, jclass obj, jstring data, jint num_threads){
GopJniHandle *jni = new GopJniHandle(num_threads);
if (jni){
jboolean is_copy;
// 实现Java里String -> char *
const char *data_root = env->GetStringUTFChars(data, &is_copy);
// 调用SDK
jni->handle = GopHandleCreate<__request>(data_root, num_threads);
if (is_copy){
// 释放内存
env->ReleaseStringUTFChars(data, data_root);
}
}
// 指针转整形
return reinterpret_cast<jlong>(jni);
}
JNIEXPORT jint JNICALL Java_SentScore_HandleRelease(JNIEnv *env, jclass obj, jlong jni_gop_handle){
// 整形转指针
auto *jni = reinterpret_cast<GopJniHandle*>(jni_gop_handle);
jint ans = 0;
if (jni){
if (jni->handle){
ans = GopHandleRelease<__request>(jni->handle);
jni->handle = 0;
}
delete jni;
}
return ans;
}
JNIEXPORT jint JNICALL Java_SentScore_ThreadHandleStarts(JNIEnv *env, jclass obj, jlong jni_gop_thread_handle, jstring jni_ref, jstring jni_utt, jfloat gop_adjust){
auto *thread = reinterpret_cast<GopJniThreadHandle*>(jni_gop_thread_handle);
__request request;
request.result.gop_adjust = gop_adjust;
if (thread && thread->thread){
// 把传入的字符串jni_ref 复制到到text_buff
CopyString(env, thread->text_buff, jni_ref);
request.ref_text = thread->text_buff.data();
CopyString(env, thread->text_buff, jni_utt);
request.audio_id = thread->text_buff.data();
return GopThreadHandleStarts<__request>(thread->thread, request);
}
else{
jboolean a1, b1;
const char *a = env->GetStringUTFChars(jni_ref, &a1);
const char *b = env->GetStringUTFChars(jni_utt, &b1);
request.audio_id = b;
request.ref_text = a;
if (a1) env->ReleaseStringUTFChars(jni_ref, a);
if (b1) env->ReleaseStringUTFChars(jni_utt, b);
return GopThreadHandleStarts<__request>(NULL, request);
}
}
- 最后一步,将上面的C代码跟着SDK 一起打包成动态库(.so),然后.so文件放到库路径下,在项目里引用调用。这里采用的框架是SpringBoot,直接在启动类里加加载库路径。
- 关于库路径,可以打印如下代码进行获取,或者在程序里指定库路径:
System.getProperty("java.library.path")
- 如果是Linux上,一般默认会库路径会有 /usr/bin,那么我们也可以通过创建软链接方式,把库文件放入库路径里。