jni动态注册是性能最优的方案。写这个的原因是jni使用的过程中坑比较多,方便此前没有接触过的朋友快速构架开发编译环境。
如果使用jna,只需要通用的c type函数导出的dll就可以了。如果是android使用的话Android studio自带cmake,这里不多讲。
工具/环境
win10 vs2019 cmake-3.19 idea jdk-8(https://adoptopenjdk.net)
java/kotlin工程
任意的gradle/maven工程。
在源码目录下新建包com,包下新建CppNative.kt:
package com
object CppNative {
external fun hello()
external fun read():ByteArray
external fun print(content:ByteArray)
}
同样的,在com包下新建Native.java:
package com;
import java.util.function.Function;
public class Native {
public static native void setCallback(Function f);
}
我们可以编译此项目,得到class文件,在class目录下:
.
├── java
│ └── main
│ └── com
│ └── Native.class
└── kotlin
└── main
└── com
└── CppNative.class
获取java函数签名
方法一 javah命名
在java/main下运行javah -jni com.Native
得到com_Native.h:
/* DO NOT EDIT THIS FILE - it is machine generated */
#include
/* Header for class com_Native */
#ifndef _Included_com_Native
#define _Included_com_Native
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class: com_Native
* Method: setCallback
* Signature: (Ljava/util/function/Function;)V
*/
JNIEXPORT void JNICALL Java_com_Native_setCallback
(JNIEnv *, jclass, jobject);
#ifdef __cplusplus
}
#endif
#endif
在kotlin/main下运行javah -jni com.CppNative
得到com_CppNative.h
/* DO NOT EDIT THIS FILE - it is machine generated */
#include
/* Header for class com_CppNative */
#ifndef _Included_com_CppNative
#define _Included_com_CppNative
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class: com_CppNative
* Method: hello
* Signature: ()V
*/
JNIEXPORT void JNICALL Java_com_CppNative_hello
(JNIEnv *, jobject);
/*
* Class: com_CppNative
* Method: read
* Signature: ()[B
*/
JNIEXPORT jbyteArray JNICALL Java_com_CppNative_read
(JNIEnv *, jobject);
/*
* Class: com_CppNative
* Method: print
* Signature: ([B)V
*/
JNIEXPORT void JNICALL Java_com_CppNative_print
(JNIEnv *, jobject, jbyteArray);
#ifdef __cplusplus
}
#endif
#endif
这是jni静态映射需要的头文件。但是动态映射不需要这个头文件,只需要获取一些拿不准的方法签名和参数类型。由于kotlin的实现机制,可以看到kotlin object类实际上是一个单例实现,jni的c函数映射的第二个参数是jobject,而不是jclass类型。如果kotlin的object内的方法想要真正的静态实现只需加入注解:@JvmStatic external fun hello()
方法二 javap命令
在java/main下运行javap -s com.Native
:
Compiled from "Native.java"
public class com.Native {
public com.Native();
descriptor: ()V
static native void setCallback(java.util.function.Function);
descriptor: (Ljava/util/function/Function;)V
}
在kotlin/main下运行javap -s com.CppNative
:
Compiled from "CppNative.kt"
public final class com.CppNative {
public static final com.CppNative INSTANCE;
descriptor: Lcom/CppNative;
public final native void hello();
descriptor: ()V
public final native byte[] read();
descriptor: ()[B
public final native void print(byte[]);
descriptor: ([B)V
static {};
descriptor: ()V
}
javap参数 | 含义 |
---|---|
-s | Sign,打印签名 |
-p | 打印全部方法和属性签名,否则默认打印public修饰的方法属性 |
我们关注我们想要映射方法的descriptor,在cmake工程中使用。
cmake工程
使用camke的原因是便于跨平台,一般说来我们在linux、windows上都有需求。
快速方便的vs2019 cmake项目
使用vs2019(如有需要在visual studio installer上勾选“使用c++的linux开发”,调试linux cpp项目比较方便),新建cmake项目“native-do”:
.
├── CMakeLists.txt
└── native-do
├── CMakeLists.txt
├── native-do.cpp
└── native-do.h
vs2019默认的cmake项目是一个父CMake项目下包含一个子项目的形式,其实没有必要改,但是为了在windows:msvc和linux:gcc下能够编译同一个项目,简化项目结构,最终项目结构如下:
.
├── CMakeLists.txt
├── jni.cpp
├── linux-x64
│ ├── jni.h
│ └── jni_md.h
├── native_do.cpp
├── native_do.h
├── tmp.zip
└── windows-x64
├── jni.h
└── jni_md.h
这里省略了vs调试方面的工作。
其中的jni.h和jni_md.h由对应的jdk文件复制而来,做动态映射的主要工作由jni.cpp完成,而native_do.cpp实现具体函数。所有文件都是utf8 LF格式。
CMakeLists.txt的编写
cmake_minimum_required (VERSION 3.17)
# 源字符集utf-8
add_compile_options("$<$:/source-charset:utf-8>")
# 执行字符集utf-8
add_compile_options("$<$:/execution-charset:utf-8>")
project(jni-native)
set(CMAKE_CXX_STANDARD 11)
add_library(jni SHARED "jni.cpp" "native_do.cpp" "native_do.h")
if (WIN32)
message(STATUS "this is windows")
target_include_directories(jni PRIVATE "windows-x64")
elseif (APPLE)
message(STATUS "this is apple")
message(STATUS "sorry this can do nothing")
elseif (UNIX)
message(STATUS "this is unix")
target_include_directories(jni PRIVATE "linux-x64")
endif ()
jni.cpp做动态注册
参考https://android.googlesource.com/platform/development/+/master/samples/SimpleJNI/jni/native.cpp
#include "native_do.h"
#include
static const char *classPathName = "com/CppNative";
static JNINativeMethod methods[] = {
{(char *) "hello", (char *) "()V", (void *) hello},
};
static int registerNativeMethods(
JNIEnv *env, const char *className,
JNINativeMethod *gMethods, int numMethods) {
jclass clazz;
clazz = env->FindClass(className);
if (clazz == nullptr) {
return JNI_FALSE;
}
if (env->RegisterNatives(clazz, gMethods, numMethods) < 0) {
return JNI_FALSE;
}
return JNI_TRUE;
}
static int registerNatives(JNIEnv *env) {
if (!registerNativeMethods(env, classPathName, methods,
sizeof(methods) / sizeof(methods[0]))) {
return JNI_FALSE;
}
return JNI_TRUE;
}
jint JNI_OnLoad(JavaVM *vm, void *reserved) {
void *p = nullptr;
jint result = -1;
JNIEnv *env = nullptr;
if (vm->GetEnv(&p, JNI_VERSION_1_8) != JNI_OK) {
goto bail;
}
env = (JNIEnv *) p;
if (registerNatives(env) != JNI_TRUE) {
goto bail;
}
result = JNI_VERSION_1_8;
bail:
return result;
}
主要注意classPathName
和methods
两个变量,通过classPathName
指定完整类名(包名+类名),methods
指定方法的映射关系:
typedef struct {
char *name;//java方法名
char *signature;//java方法签名
void *fnPtr;//cpp函数指针
} JNINativeMethod;
native_do.h:
#ifndef NATIVE_DO_H
#define NATIVE_DO_H
void hello();
#endif //NATIVE_DO_H
native_do.cpp:
#include "native_do.h"
#include
void hello(){
//中文输出是否乱码取决于源字符码和命令行终端的显示字符编码
std::cout << "native library version 1.0.0" << std::endl;
}
编译
在我们的CMakeLists.txt所在的目录执行;
windows/msvc
cmake -B build -S .
-- Building for: Visual Studio 16 2019
-- Selecting Windows SDK version 10.0.18362.0 to target Windows 10.0.19042.
-- The C compiler identification is MSVC 19.28.29914.0
-- The CXX compiler identification is MSVC 19.28.29914.0
-- Detecting C compiler ABI info
-- Detecting C compiler ABI info - done
-- Check for working C compiler: C:/Program Files (x86)/Microsoft Visual Studio/2019/Community/VC/Tools/MSVC/14.28.29910/bin/Hostx64/x64/cl.exe - 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: C:/Program Files (x86)/Microsoft Visual Studio/2019/Community/VC/Tools/MSVC/14.28.29910/bin/Hostx64/x64/cl.exe - skipped
-- Detecting CXX compile features
-- Detecting CXX compile features - done
-- Configuring done
-- Generating done
-- Build files have been written to: C:/Users/Public/tmp/build
cmake --build build --config Release
用于 .NET Framework 的 Microsoft (R) 生成引擎版本 16.9.0+5e4b48a27
版权所有(C) Microsoft Corporation。保留所有权利。
Checking Build System
Building Custom Rule C:/Users/Public/tmp/CMakeLists.txt
jni.cpp
native_do.cpp
正在生成代码...
正在创建库 C:/Users/Public/tmp/build/Debug/jni.lib 和对象 C:/Users/Public/tmp/build/Debug/jni.exp
jni.vcxproj -> C:\Users\latla\Desktop\test-file\tmp\build\Debug\jni.dll
Building Custom Rule C:/Users/Public/tmp/CMakeLists.txt
linux/gcc
cmake -B build -S . -DCMAKE_BUILD_TYPE=Release
-- The C compiler identification is GNU 9.3.0
-- The CXX compiler identification is GNU 9.3.0
-- 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
-- Build files have been written to: /root/tmp/build
cmake --build build
Scanning dependencies of target jni
[ 33%] Building CXX object CMakeFiles/jni.dir/jni.cpp.o
[ 66%] Building CXX object CMakeFiles/jni.dir/native_do.cpp.o
[100%] Linking CXX shared library libjni.so
[100%] Built target jni
这里只是简单的映射了打印字符串方法,关于jni引用参数、回调等映射方式放在下章。