为了更好的获得一些比较独立的模块的性能,比如视频模块,寻路模块,通过对C++ 接口的封装,通过JNI技术对它进行跨语言调用。
那什么是JNI呢?JNI (Java Native Interface,Java本地接口)是一种编程框架,使得Java虚拟机中的Java程序可以调用本地应用/或库,也可以被其他程序调用。 本地程序一般是用其它语言(C、C++或汇编语言等)编写的,并且被编译为基于本机硬件和操作系统的程序。
生成头文件
首先要编辑头文件对应的JAVA文件,就是暴露出native接口的JAVA类。
public class GamiooJNI { static { try { NativeUtils.loadLibrary("recast"); } catch (IOException e) { LOGGER.error(e.getMessage(), e); } } /** * 获取寻路API版本号 * * @return 获取寻路API版本号 */ public native String getVersion(); }
java ->*.h 工具
点击 File > Settings > Tools > External Tools,添加一个先的External Tools:
Name:Generate Header File Description: 生成C++类的头文件 Program: $JDKPath$/bin/javah Arguments: -jni -classpath $OutputPath$ -d ./jni $FileClass$ Working directory: $ProjectFileDir$
做好后,在某个java文件导出
在GamiooJNI.java文件中点击右键> External Tools > Generate Header File
会在项目根目录下的jni目录生成文件,如果生成不出来的话,编译下该Java文件,并把*.h文件给删了先。
实际上,就是执行了类似如下指令:
"D:\Program Files\Java\TencentKona-8.0.4/bin/javah" -jni -classpath F:\gamioo\out\production\classes -d ./jni com.gamioo.ooxx.GamiooJNI
生成的文件:com_gamioo_ooxx_GamiooJNI.h
/* DO NOT EDIT THIS FILE - it is machine generated */ #include <jni.h> /* Header for class com.gamioo.ooxx.GamiooJNI */ #ifndef _Included_com_gamioo_ooxx_GamiooJNI #define _Included_com_gamioo_ooxx_GamiooJNI #ifdef __cplusplus extern "C" { #endif /* * Class: com_gamioo_ooxx_GamiooJNI * Method: getVersion * Signature: ()Ljava/lang/String; */ JNIEXPORT jstring JNICALL Java_com_gamioo_ooxx_GamiooJNI_getVersion (JNIEnv *, jobject); #ifdef __cplusplus } #endif #endif
编辑C++ 文件
用CLion去创建dll工程,引入头文件com_gamioo_ooxx_GamiooJNI.h
创建cpp文件
#include "jni.h" #include <iostream> #include <exception> #include <string> #include <cstdint> #include <map> #include "com_gamioo_ooxx_GamiooJNI.h" using namespace std; static const int NAVMESHSET_VERSION = 1; const char* NavMesh::Version() { return ""+ NAVMESHSET_VERSION; } /** * 获取寻路API版本号 * * @return 获取寻路API版本号 */ JNIEXPORT jstring JNICALL Java_com_gamioo_ooxx_GamiooJNI_getVersion (JNIEnv* env, jobject jobj) { const char* version = NavMesh::GetInstace()->Version(); return env->NewStringUTF(version); }
这里需要注意,一开始第二行的#include <jni.h>报错了,这时因为MinGW编译器没有jni.h这个头文件,打开JDK的home目录,在include目录中可以找到jni.h头文件,除此之外,我们还需要include/win32目录下的jni_md.h头文件,一共两个,把这两个头文件都复制到MinGW安装目录(CLion 2021.3.3\bin\mingw\x86_64-w64-mingw32\include目录中,注意这两个头文件是一起放在MinGW的这个目录的,jni_md.h不需要另外创建一个win32目录来存放。完成后发现com_example_jni_JNIObject.h的报错消失了。
如果是在VS 2019环境下,则要在属性–>VC++目录–>包含目录里加上JDK目录:
类型互转
有很多类型的互转需要注意,类型互转问题转不好,还有内存泄漏的问题,这里会陆续总结:
/**JByteaArray -> char* */ static char* ConvertJByteaArrayToChars(JNIEnv* env, jbyteArray bytearray) { char* chars = NULL; jbyte* bytes; bytes = env->GetByteArrayElements(bytearray, 0); size_t chars_len = env->GetArrayLength(bytearray); chars = new char[chars_len + 1]; memset(chars, 0, chars_len + 1); memcpy(chars, bytes, chars_len); chars[chars_len] = 0; env->ReleaseByteArrayElements(bytearray, bytes, 0); return chars; } /** float* -> jfloatArray */ static jfloatArray ConvertFloatStarToJfloatArray(JNIEnv* env, float* array, int length) { jfloatArray ret = env->NewFloatArray(length); env->SetFloatArrayRegion(ret, 0, length, array); return ret; } jstring stringTojstring(JNIEnv* env, const char* pat) { jclass strClass = (env)->FindClass("java/lang/String"); jmethodID ctorID = (env)->GetMethodID(strClass, "<init>", "([BLjava/lang/String;)V"); jbyteArray bytes = (env)->NewByteArray(strlen(pat)); (env)->SetByteArrayRegion(bytes, 0, strlen(pat), (jbyte*)pat); jstring encoding = (env)->NewStringUTF("GB2312"); return (jstring)(env)->NewObject(strClass, ctorID, bytes, encoding); } std::string jstringTostring(JNIEnv* env, jstring jstr) { char* rtn = NULL; jclass clsstring = env->FindClass("java/lang/String"); jstring strencode = env->NewStringUTF("GB2312"); jmethodID mid = env->GetMethodID(clsstring, "getBytes", "(Ljava/lang/String;)[B"); jbyteArray barr = (jbyteArray)env->CallObjectMethod(jstr, mid, strencode); jsize alen = env->GetArrayLength(barr); jbyte* ba = env->GetByteArrayElements(barr, JNI_FALSE); if (alen > 0) { rtn = (char*)malloc(alen + 1); memcpy(rtn, ba, alen); rtn[alen] = 0; } env->ReleaseByteArrayElements(barr, ba, 0); std::string stemp(rtn); free(rtn); return stemp; } std::string toStr(JNIEnv* env, jstring jstr) { return toStr(env, env->GetStringUTFChars(jstr, 0)); } std::string toStr(JNIEnv* env, const char* chs) { std::string s(chs); return s; } jstring toJstring(JNIEnv* env, std::string str) { return toJstring(env, str.c_str()); } jstring toJstring(JNIEnv* env, char* chs) { return env->NewStringUTF(chs); }
生成 DLL,SO 文件
CMAKE 文件
CMakeLists.txt 的内容如下,依照葫芦画瓢就行。
cmake_minimum_required(VERSION 3.22) find_package(JNI REQUIRED) # Use C++11 set(CMAKE_CXX_STANDARD 11) # Require (at least) it set(CMAKE_CXX_STANDARD_REQUIRED ON) # Don't use e.g. GNU extension (like -std=gnu++11) for portability set(CMAKE_CXX_EXTENSIONS OFF) include_directories(${JNI_INCLUDE_DIRS}) if ( WIN32 AND NOT CYGWIN AND NOT ( CMAKE_SYSTEM_NAME STREQUAL "WindowsStore" ) ) set(CMAKE_C_FLAGS_RELEASE "${CMAKE_C_FLAGS_RELEASE} /MT" CACHE STRING "") set(CMAKE_C_FLAGS_DEBUG "${CMAKE_C_FLAGS_DEBUG} /MTd" CACHE STRING "") set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE} /MT" CACHE STRING "") set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} /MTd" CACHE STRING "") endif () project(RecastDll) find_path(RecastDll_PROJECT_DIR NAMES SConstruct PATHS ${CMAKE_SOURCE_DIR} NO_DEFAULT_PATH ) MARK_AS_ADVANCED(RecastDll_PROJECT_DIR) # 配置cpp文件 file(GLOB RECASTDLL_SOURCES Source/*.cpp ../Detour/Source/*.cpp ../DetourCrowd/Source/*.cpp ../DetourTileCache/Source/*.cpp ../Recast/Source/*.cpp ) # 配置头文件 include_directories( Include ../DebugUtils/Include ../Detour/Include ../DetourCrowd/Include ../DetourTileCache/Include ../Recast/Include ) macro(source_group_by_dir proj_dir source_files) if(MSVC) get_filename_component(sgbd_cur_dir ${proj_dir} ABSOLUTE) foreach(sgbd_file ${${source_files}}) get_filename_component(sgbd_abs_file ${sgbd_file} ABSOLUTE) file(RELATIVE_PATH sgbd_fpath ${sgbd_cur_dir} ${sgbd_abs_file}) string(REGEX REPLACE "\(.*\)/.*" \\1 sgbd_group_name ${sgbd_fpath}) string(COMPARE EQUAL ${sgbd_fpath} ${sgbd_group_name} sgbd_nogroup) string(REPLACE "/" "\\" sgbd_group_name ${sgbd_group_name}) if(sgbd_nogroup) set(sgbd_group_name "\\") endif(sgbd_nogroup) source_group(${sgbd_group_name} FILES ${sgbd_file}) endforeach(sgbd_file) endif(MSVC) endmacro(source_group_by_dir) source_group_by_dir(${CMAKE_CURRENT_SOURCE_DIR} RECASTDLL_SOURCES) add_library(RecastDll SHARED ${RECASTDLL_SOURCES}) if ( WIN32 AND NOT CYGWIN ) target_compile_definitions (RecastDll PRIVATE DLL_EXPORTS) endif ( )
make_win64.bat & make_linux64.sh 文件
然后在写好windows下的bat,和linux 下的sh脚本:
make_win64.bat
mkdir build64 & pushd build64 cmake -G "Visual Studio 16 2019" -A x64 .. popd cmake --build build64 --config Release md Plugins\x86_64 copy /Y build64\Release\RecastDll.dll Plugins\x86_64\recast.dll rmdir /S /Q build64 pause
mkdir -p build_linux64 && cd build_linux64 cmake ../ cd .. cmake --build build_linux64 --config Release cp build_linux64/libRecastDll.so Plugins/x86_64/recast.so rm -rf build_linux64
这里会遇到cmake 的版本问题,版本要求
CMake 3.22 or higher is required. You are running version 2.8.12.2
先卸载本地的cmake
yum erase cmake
安装高版本的
yum install -y epel-release yum install -y cmake3
验证版本:
cmake3 --version
可以做个软链接生效cmake 指令
ln -s /usr/bin/cmake3 /usr/bin/cmake
如果遇到异常 :Could NOT find OpenGL (missing: OPENGL_opengl_LIBRARY OPENGL_glx_LIBRARY
OPENGL_INCLUDE_DIR), 缺少OpenGL库, libgl1-mesa-dev是有关OpenGL的库
yum install mesa-libGL-devel mesa-libGLU-devel -y
如果遇到异常: Could NOT find SDL2 (missing: SDL2_LIBRARY SDL2_INCLUDE_DIR)
yum install SDL2-devel -y
如果版本不够的话手工下载安装:
因为通过yum 安装的版本都相对很低:
[root@VM-16-13-centos ~]# yum --showduplicates list cmake3 | expand Available Packages cmake3.x86_64 3.17.5-1.el7 epel
[root@VM-16-13-centos ~]# yum --showduplicates list cmake | expand Available Packages cmake.x86_64 2.8.12.2-2.el7 base
wget https://cmake.org/files/v3.22/cmake-3.22.6.tar.gz cd cmake-3.22.6 yum install build-essential 返回cmake-3.22.6的上层目录cd .. 执行chmod -R 777 cmake-3.22.6 到目录cmake-3.22.6执行./bootstrap ,再执行make,再执行make install 验证 cmake --version ln -s /usr/local/bin/cmake /usr/bin
如果遇到:Could NOT find JNI (missing: JAVA_AWT_LIBRARY JAVA_INCLUDE_PATH
JAVA_INCLUDE_PATH2 JAVA_AWT_INCLUDE_PATH),看看环境变量里JAVA_HOME有没有。
顺利的话应该会导出结果如下:
[root@VM-16-13-centos RecastDll]# sh make_linux64.sh -- Found JNI: /usr/local/j2sdk-image/jre/lib/amd64/libjawt.so -- The C compiler identification is GNU 4.8.5 -- The CXX compiler identification is GNU 4.8.5 -- Detecting C compiler ABI info -- Detecting C compiler ABI info - done -- Check for working C compiler: /usr/bin/cc - skipped -- Detecting C compile features -- Detecting C compile features - done -- Detecting CXX compiler ABI info -- Detecting CXX compiler ABI info - done -- Check for working CXX compiler: /usr/bin/c++ - skipped -- Detecting CXX compile features -- Detecting CXX compile features - done -- Configuring done -- Generating done .... .... [100%] Built target RecastDll
windows 下安装如下文件:
https://cmake.org/files/v3.22/cmake-3.22.6-windows-x86_64.msi,执行make_win64.bat
分别在windows下和linux 下导出:
加载DLL,SO 文件
一般我们会把dll文件和so文件随着对应JNI暴露的JAVA文件所在的jar包一起导出,
–jni.java
*.dll
*.so
但调用的动态链接库文件又必须是独立的,如何做到呢,我们需要把文件复制到临时目录下,然后用System.load()调用。
/** * 用于加载native dll的工具类 * * @author Allen Jiang */ public class NativeUtils { public static void loadLibrary(String name) throws IOException { String suffix = ""; //TODO 暂时只为两种系统服务 if (SystemUtils.IS_OS_LINUX) { suffix += ".so"; } else { suffix += ".dll"; } try (InputStream inputStream = FileUtils.getInputStream(name + suffix); ByteArrayOutputStream out = new ByteArrayOutputStream()) { byte[] buffer = new byte[1024]; int n = 0; while (-1 != (n = inputStream.read(buffer))) { out.write(buffer, 0, n); } File file = File.createTempFile(name, suffix); try (FileOutputStream fileOutputStream = new FileOutputStream(file)) { fileOutputStream.write(out.toByteArray()); } System.load(file.getAbsolutePath()); } } }
然后在JNI接口类里加载进来:
static { try { NativeUtils.loadLibrary("recast"); } catch (IOException e) { LOGGER.error(e.getMessage(), e); } }
调用JNI 接口
调用接口就像调用JAVA 普通的API一样,
GamiooJNI jni=new GamiooJNI (); String version=jni.getVersion();
JNI中接下去需要探索的: 自定义对象的转换
查询内存泄漏
//18 是pid jcmd 18 VM.native_memory detail scale=MB >leak.log
如果返回Native memory tracking is not enabled,那么就是在启动参数里忘记设置了 -XX:NativeMemoryTracking=detail
如果有内存泄漏,那么,你会在log文件里看到你写的JNI接口
NMT必须先通过VM启动参数中打开,不过要注意的是,打开NMT会带来5%-10%的性能损耗。
-XX:NativeMemoryTracking=[off | summary | detail] # off: 默认关闭 # summary: 只统计各个分类的内存使用情况. # detail: Collect memory usage by individual call sites. 例如:-XX:NativeMemoryTracking=detail
其中,reserved表示应用可用的内存大小,committed表示应用正在使用的内存大小
GraphScope analytics in Java:打破大规模图计算的跨语言障碍
JVM NATIVEMEMORYTRACKING ;JCMD PROCESS_ID VM.NATIVE_MEMORY;NATIVE MEMORY TRACKING IS NOT ENABLED