c++的jni动态注册简单示例(上)

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;
}

主要注意classPathNamemethods两个变量,通过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引用参数、回调等映射方式放在下章。

你可能感兴趣的:(c++的jni动态注册简单示例(上))