7

Java安全详谈-JNI 底层分析

 2 years ago
source link: https://qftm.github.io/2022/05/29/Java-JVM-JNI/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

Java安全详谈-JNI 底层分析

JNI (Java Native Interface,JAVA 本地接口) 允许 Java 代码和其它编程语言编写的代码进行交互,主要为Java和Native层(C/C++)相互调用的接口规范,但是并不妨碍扩展其他语言。 JNI 在 Java1.1 中正式推出,在 Java1.2 中加入了JNI_OnLoadJNI_OnUnload方法。

JNI 设计交互 Native层 C/C++ 代码的主要优点为:特定功能场景下,提升应用性能;代码层保护,增加反编译难度;Native库文件,可重复使用及不同项目间扩展移植。说完优点那么唯一的一个缺点就是C/C++代码编译生成的动态链接库不可跨平台使用。

正常情况下编写的Java代码是不能直接调用C/C++代码,需要通过JNI标准接口进行间接调用访问。Java代码要想使用JNI调用Native层C/C++代码就需要先向JVM注册Native函数,其注册主要分为静态注册和动态注册两大类,通过注册可实现 Java Native 方法与 C/C++ 方法的对应关系。静态和动态注册主要区别在于查找效率:静态注册,首次调用Java Native方法时,会按照JNI命名规则去寻找;而动态注册,由于存在一张映射表JNINativeMethod,因此查找效率高。另外,静态注册多用于NDK开发,而动态注册多用于Framework开发。最终,无论静态注册方法,还是动态注册方法,都需要将相应的C/C++文件编译成平台所需要的动态库。

JNI静态注册在Java代码首次调用 Java Native 函数时完成

通过编写Java程序,调用native层的整数求和函数来了解JNI静态注册实现过程。Java代码调用静态Native函数,需要在类中加载相应动态链接库文件,并声明要调用的Java Native函数

package org.qftm.learn.jni.demo1;

/**
 * Created by IntelliJ IDEA.
 * User: Qftm
 * Date: 2022/5/11
 * Time: 17:36
 */

public class IntSum {
    // 声明 native 本地函数
    public native int sums(int num1, int num2);
    // 静态代码块,加载动态链接库
    static {
        System.loadLibrary("IntSum");
    }

    public static void main(String[] args) {
        // 实例化调用 native 本地函数
        System.out.println((new IntSum()).sums(10, 5));
    }
}

编译Java程序

λ Qftm >>>: javac org\qftm\learn\jni\demo1\IntSum.java

利用 javah 生成编译native库所需的相应 .h 头文件,javah 会自动识别 class 文件里声明的 java native 本地方法并进行解析处理生成相应 .h 头文件 (也可以根据JNI静态注册规则手动编写头文件)

λ Qftm >>>: javah -o IntSum.h org.qftm.learn.jni.demo1.IntSum

查看 javah 处理生成的 IntSum.h 头文件并进行分析,其中定义了一个JNI Native调用的 C/C++方法Java_org_qftm_learn_jni_demo1_IntSum_sums,可以看到方法结构与Java方法类似,同样包含方法名、参数、返回类型、修饰符

/* DO NOT EDIT THIS FILE - it is machine generated */
#include 
/* Header for class org_qftm_learn_jni_demo1_IntSum */

#ifndef _Included_org_qftm_learn_jni_demo1_IntSum
#define _Included_org_qftm_learn_jni_demo1_IntSum
#ifdef __cplusplus
extern "C" {
#endif
/*
 * Class:     org_qftm_learn_jni_demo1_IntSum
 * Method:    sums
 * Signature: (II)D
 */
JNIEXPORT jdouble JNICALL Java_org_qftm_learn_jni_demo1_IntSum_sums (JNIEnv *, jobject, jint, jint);

#ifdef __cplusplus
}
#endif
#endif

默认javah解析生成的JNI头文件中,C/C++ 函数都为JNI Native对应的静态注册函数,其通过 JNIEXPORT 和 JNICALL 两个宏定义声明。除了两个宏定义外,函数还存在返回类型和方法名及函数参数,其中静态注册函数名遵循特定JNI命名规范,即为Java_<PackageName>_<ClassName>_<MethodName>,这里需要注意下,如果Java代码声明的本地方法名称中本来就包含下划线,那么该部分将使用下划线加数字替换。另外会发现函数多了两个类型参数JNIEnv *jobject,这两个参数由javah自动添加,并且其参数值由虚拟机自动传入。

  • jni.h

#include <jni.h> 引入了JVM所声明的所有JNI接口规范,jni.h 位于$JAVA_HOME/include目录内,这是JNI中所有的类型、函数、宏等定义的地方。

  • extern

extern "C" 声明是为了避免编绎器按照C++的方式去编绎C函数,也就是告诉编译器这部分代码使用C语言的规则进行编译和链接,这样处理的原因主要是涉及到C和C++的函数编译差异。C不支持函数的重载,编译之后函数名不变;而C++支持函数的重载,无论函数是否同名,编译之后函数名会发生改变,即被编译成函数名+参数类型的特殊修饰格式,Windows下修饰格式为:? + 函数名 + @ + 类名/名称空间 + @ + YG/YA/YI(__stdcall、__cdecl、__fastcall) + 函数返回值类型 + 参数类型表(X-void、D-char、E-unsigned char、F-short、H-int、I-unsigned int、J-long、K-unsigned long、M-float、N-double、_N-bool、PA+指针类型-指针) + @Z;Linux下修饰格式为:函数名 + 参数类型表(d-double、P+指针类型、i-int)

举例在 Windows 下测试代码如下

// define1.h 定义自定义函数
extern "C" {
    int add1(int num1, int num2);
    void info1(char* name, int age);
}

int add2(int num1, int num2);
void info2(char* name, int age);


// dllmain.cpp : 定义 DLL 应用程序的入口点。
#include "pch.h"
#include "stdio.h"
#include "define1.h"

int add1(int num1, int num2) {
    return num1 + num2;
}

void info1(char* name, int age) {
    printf("");
}

int add2(int num1, int num2) {
    return num1 + num2;
}

void info2(char* name, int age) {
    printf("");
}

查看extern "C"使用C编译方式的函数,可以看到函数被编译后,函数名未发生改变

image-20220516231618958
image-20220516231618958

而,使用C++编译方式的函数,可以看到函数被编译后,函数名发生改变

int add2(int num1, int num2)      ->    ?add2@@YAHHH@Z
void info2(char* name, int age)   ->    ?info2@@YAXPADH@Z
image-20220516232740093
image-20220516232740093

那么JNI静态注册调用的C/C++函数必须要使用extern "C"声明,否则JNI静态查找规则调用Native时会抛出Exception in thread "main" java.lang.UnsatisfiedLinkError异常。

/*
 * Class:     org_qftm_learn_jni_demo1_IntSum
 * Method:    sums
 * Signature: (II)D
 */
JNIEXPORT jdouble JNICALL Java_org_qftm_learn_jni_demo1_IntSum_sums (JNIEnv *, jobject, jint, jint);

函数的注释分为三部分,解释如下

Class:     对应的Java层Class完整类
Method:    对应的Java层Native方法
Signature: 对应的Java层Native方法的签名
  • JNIEXPORT、JNICALL

后续的分析,在 Windows 平台下生成特定的 dll 动态链接库,使用 VS2019 创建生成 DLL 动态链接库项目(Linux 下使用 gcc -fPIC -shared xxx.c -o yyy.so 编译生成特定的 so 动态链接库)

image-20220506173143258
image-20220506173143258

DLL项目最初结构如下

image-20220506201020978
image-20220506201020978

点击左侧头文件, 选择添加新建项, 新建头文件命名 intsum.h 进行添加,添加后打开该头文件并复制上面 IntSum.h头文件内容,复制完毕会发现报错找不到 jni.h 头文件

image-20220519100609787
image-20220519100609787

jni.h 头文件位于 JDK 项目 include 目录下,该目录下的所有 .h 文件主要为编译本地代码时使用的 C/C++ 头文件,所以需要引入 <JDK_HOME>/include<JDK_HOME>/include/win32 目录作为附加包含目录(导航栏依次选择 项目 => 属性 => C/C++ => 常规 => 附加包含目录),

image-20220506204135391
image-20220506204135391

附加包含目录引入后可以在项目结构的外部依赖项中查看到所有的 .h 头文件依赖

定位关键字JNIEXPORT、JNICALL定义在 jni_md.h 宏定义中,不同平台下JDK所定义的宏定义内容有所不同,如下

// Windows JDK 1.8
#define JNIEXPORT __declspec(dllexport)
#define JNIIMPORT __declspec(dllimport)
#define JNICALL __stdcall

// Linux JDK 1.8
#if (defined(__GNUC__) && ((__GNUC__ > 4) || (__GNUC__ == 4) && (__GNUC_MINOR__ > 2))) || __has_attribute(visibility)
  #define JNIEXPORT     __attribute__((visibility("default")))
  #define JNIIMPORT     __attribute__((visibility("default")))
#else
  #define JNIEXPORT
  #define JNIIMPORT
#endif

#define JNICALL

宏定义代表的是define常量右边定义的内容,JNIEXPORT 用来表示函数是否可导出(即:方法的可见性),Linux下__attribute__((visibility("default")))描述的是”可见性”属性 visibility,取值如下

default:表示外部可见,类似于public修饰符,可以被外部发现调用
hidden :表示隐藏,类似于private修饰符,只能被内部发现调用

如果JNIEXPORT由hidden属性描述,则被JNIEXPORT修饰的C/C++函数 __attribute__((visibility("hidden"))) jdouble JNICALL Java_org_qftm_learn_jni_demo1_IntSum_sums(JNIEnv*, jobject, jint, jint); 将不会出现在导出表里面,那么JNI静态注册的Native函数被JNI静态调用规则调用时就无法在相应动态链接库的导出表内找到对应的C/C++函数,导致JNI静态规则调用失败;另外,如果Java Native函数对应的C/C++函数没有通过正常的JNIEXPORT修饰(即:去掉JNIEXPORT关键字),那么该C/C++函数同样不会出现在导出表里面,也就无法被JNI静态注册调用规则正常调用,但是可以利用JNI动态注册调用。

JNICALL 用来表示C/C++函数的调用规范(如:__stdcall、__cdecl、__fastcall),Linux下JNICALL右边是空的,说明只是个空定义,库文件代码可以直接使用右边的内容(空)替换调JNICALL(即:去掉JNICALL关键字)

  • JNI 静态调用 Native

编辑 dllmain.cpp 实现intsum.h头文件定义的C/C++函数,代码如下

// dllmain.cpp : 定义 DLL 应用程序的入口点。
#include "pch.h"
#include 
#include "intsum.h"

// JNIEnv *env 为JVM环境指针, 用于和JVM交互、jobject obj 表示当前操作的对象
JNIEXPORT jdouble JNICALL Java_org_qftm_learn_jni_demo1_IntSum_sums(JNIEnv* env, jobject obj, jint num1, jint num2) {
   return num1+num2;
}

点击导航栏的生成 => 生成解决方案,即可生成所需的 dll 动态链接库,将 Windows DLL 项目生成的 dll 拷贝到 Java JNI 项目下,并测试通过 Java JNI 调用静态 Native 函数

image-20220527103833860
image-20220527103833860

通过上面可以发现,java代码native函数和dll代码里面c/c++函数声明的差异

// java
public native double sums(int num1, int num2);

// c++
JNIEXPORT jdouble JNICALL Java_org_qftm_learn_jni_demo1_IntSum_sums(JNIEnv*, jobject, jint, jint);

通过对比可以发现java native sums函数名在c++中变得很长,这跟JNI native函数注册方式有关,这里使用的是JNI Native静态函数注册,而JNI Native静态注册在编写动态链接库C++代码中必须按照JNI接口规范的命名规则注册,即java native函数对应的C/C++函数名为Java_<PackageName>_<ClassName>_<MethodName>。那么,当我们在Java中调用静态Native方法时,JVM 就会根据JNI命名规则来查找相应dll库的导出表,即调用Java静态Native方法所对应的 C/C++ 方法。

  • JNIENV

JNIENV 直译为 JNI 环境,和具体线程相关。函数默认的第一个参数JNIEnv *env由虚拟机自动传入,通过JNIEnv * 指针可以对 Java 层代码进行操作,如:获取Java类/父类Class对象、创建Java对象、调用Java对象的方法等。

定位JNIEnv实现在jni.h中:Windows JDK 1.8

/*
 * JNI Native Method Interface.
 */

struct JNINativeInterface_;

struct JNIEnv_;

#ifdef __cplusplus   // C++
typedef JNIEnv_ JNIEnv;
#else                // C
typedef const struct JNINativeInterface_ *JNIEnv;
#endif

C++JNIEnv宏定义为一个JNIEnv_结构体,JNIEnv_结构体定义如下

/*
 * We use inlined functions for C++ so that programmers can write:
 *
 *    env->FindClass("java/lang/String")
 *
 * in C++ rather than:
 *
 *    (*env)->FindClass(env, "java/lang/String")
 *
 * in C.
 */

struct JNIEnv_ {
    const struct JNINativeInterface_ *functions;
#ifdef __cplusplus

    jint GetVersion() {
        return functions->GetVersion(this);
    }
    jclass DefineClass(const char *name, jobject loader, const jbyte *buf,
                       jsize len) {
        return functions->DefineClass(this, name, loader, buf, len);
    }
    jclass FindClass(const char *name) {
        return functions->FindClass(this, name);
    }
    jmethodID FromReflectedMethod(jobject method) {
        return functions->FromReflectedMethod(this,method);
    }
    jfieldID FromReflectedField(jobject field) {
        return functions->FromReflectedField(this,field);
    }

    jobject ToReflectedMethod(jclass cls, jmethodID methodID, jboolean isStatic) {
        return functions->ToReflectedMethod(this, cls, methodID, isStatic);
    }
    .......

从上述声明可以看出,函数由结构体指针JNINativeInterface定义,其定义了JNI函数调用的接口

/*
 * JNI Invocation Interface.
 */

struct JNIInvokeInterface_;

struct JavaVM_;

#ifdef __cplusplus
typedef JavaVM_ JavaVM;
#else
typedef const struct JNIInvokeInterface_ *JavaVM;
#endif

struct JNINativeInterface_ {
    void *reserved0;
    void *reserved1;
    void *reserved2;

    void *reserved3;
    jint (JNICALL *GetVersion)(JNIEnv *env);

    jclass (JNICALL *DefineClass)
      (JNIEnv *env, const char *name, jobject loader, const jbyte *buf,
       jsize len);
    jclass (JNICALL *FindClass)
      (JNIEnv *env, const char *name);

    jmethodID (JNICALL *FromReflectedMethod)
      (JNIEnv *env, jobject method);
    jfieldID (JNICALL *FromReflectedField)
      (JNIEnv *env, jobject field);

    jobject (JNICALL *ToReflectedMethod)
      (JNIEnv *env, jclass cls, jmethodID methodID, jboolean isStatic);
     .....

从定义的函数接口可以看出结构体JNINativeInterface_是操作 Java 层的入口,那么相应C/C++函数代码通过JNIEnv 指针就可对Java层进行操作

  • jclass、jobject

jclass表示Java层静态native函数对应Java类的Class对象

jobject 表示Java层非静态native函数对应Java类的实例对象,相当于Java中的this

  • Java、JNI、C/C++ 数据类型映射关系

javah生成的头文件里使用的类型都是jni.h所定义规范的,目的是为了做到与平台无关,比如:在所有平台上都能保证jint是32位的有符号整型。

通过定位查看jni.h等源码,可以得出 Java、JNI、C/C++ 中基本数据类型和引用数据类型的映射关系表,核心源码如下位于jni.h

// Windows JDK 1.8
/*
 * JNI Types
 */

#ifndef JNI_TYPES_ALREADY_DEFINED_IN_JNI_MD_H

typedef unsigned char   jboolean;
typedef unsigned short  jchar;
typedef short           jshort;
typedef float           jfloat;
typedef double          jdouble;

typedef jint            jsize;

#ifdef __cplusplus

class _jobject {};
class _jclass : public _jobject {};
class _jthrowable : public _jobject {};
class _jstring : public _jobject {};
class _jarray : public _jobject {};
class _jbooleanArray : public _jarray {};
class _jbyteArray : public _jarray {};
class _jcharArray : public _jarray {};
class _jshortArray : public _jarray {};
class _jintArray : public _jarray {};
class _jlongArray : public _jarray {};
class _jfloatArray : public _jarray {};
class _jdoubleArray : public _jarray {};
class _jobjectArray : public _jarray {};

typedef _jobject *jobject;
typedef _jclass *jclass;
typedef _jthrowable *jthrowable;
typedef _jstring *jstring;
typedef _jarray *jarray;
typedef _jbooleanArray *jbooleanArray;
typedef _jbyteArray *jbyteArray;
typedef _jcharArray *jcharArray;
typedef _jshortArray *jshortArray;
typedef _jintArray *jintArray;
typedef _jlongArray *jlongArray;
typedef _jfloatArray *jfloatArray;
typedef _jdoubleArray *jdoubleArray;
typedef _jobjectArray *jobjectArray;

#else

struct _jobject;

typedef struct _jobject *jobject;
typedef jobject jclass;
typedef jobject jthrowable;
typedef jobject jstring;
typedef jobject jarray;
typedef jarray jbooleanArray;
typedef jarray jbyteArray;
typedef jarray jcharArray;
typedef jarray jshortArray;
typedef jarray jintArray;
typedef jarray jlongArray;
typedef jarray jfloatArray;
typedef jarray jdoubleArray;
typedef jarray jobjectArray;

#endif

typedef jobject jweak;

typedef union jvalue {
    jboolean z;
    jbyte    b;
    jchar    c;
    jshort   s;
    jint     i;
    jlong    j;
    jfloat   f;
    jdouble  d;
    jobject  l;
} jvalue;

struct _jfieldID;
typedef struct _jfieldID *jfieldID;

struct _jmethodID;
typedef struct _jmethodID *jmethodID;

(1)基本数据类型关系表

JNI 类型 Java 类型 C/C++ 类型 本地类型
jsize/jint int int int32_t
jshort short short int16_t
jlong long long/__int64 int64_t
jbyte byte signed char char
jboolean boolean unsigned char uint8_t
jchar char unsigned short uint16_t
jfloat float float float
jdouble double double double

(2)引用数据类型关系表

JNI 类型 Java 类型 C 类型 C++ 类型
jobject 类实例化对象 _jobject* _jobject*
jclass _jobject* _jclass*
jstring java.lang.String _jobject* _jstring*
jthrowable java.lang.Throwable _jobject* _jthrowable*
jintArray int[] _jobject* _jintArray*

注意,除了上面定义的StringClassThrowable数组,其它的类都是以jobject的形式出现。

在面向对象的程序语言中一切皆对象,如果JNI对应的C/C++函数使用C编译器进行编译是没有对象继承的概念,则此时jstringjclass数组等皆为jobject

  • JNI 签名(描述符)

signature 译为签名,用于Java里面成员的描述符,其概念如下

(1) 如果是字段,表示字段类型的描述符
(2) 如果是函数,表示函数结构的描述符,即:(每个参数类型描述符) + 返回值类型描述符

(1)Java 数据类型描述符关系表

Java 类型 字段描述符(签名) 备注
int I int 首字母大写
float F float 首字母大写
double D double 首字母大写
short S short 首字母大写
long L long 首字母大写
char C char 首字母大写
byte B byte 首字母大写
boolean Z Z (因为 B 已被 byte 使用)
object L + /分隔完整类名 java.lang.String 如: Ljava/lang/String
array [ + 类型描述符 int[] 如:[I

(2)Java 函数结构描述符关系表

Java 函数 函数描述符(签名) 备注
void V 无返回值类型
Method (每个参数字段描述符)函数返回值字段描述符 double sums(int num1, int num2) => (II)D

举例 Java Native 函数sums的签名解读

// Signature: (II)D
public native double sums(int num1, int num2);

// int 类型描述符:I
// double 类型描述符:D
// 参数字段类型描述符:II
// 函数结构描述符:(II)D

动态注册相比静态注册查找效率高、函数名任意。

通过JNI下接口RegisterNatives()可动态实现JNI中Native函数的注册,即动态将JVM中某个类调用的Native方法与动态链接库中的特定方法做绑定和映射,这样JNI可直接利用JNINativeMethod映射表来搜索调用Java Native函数对应的C/C++函数。

针对动态注册的核心RegisterNatives()接口,可通过Java JNI 化静为动Native层 JNI_OnLoad()两种方式进行调用实现

Java JNI 化静为动

Windows JDK 1.8源码为例,通过java.lang.Object类来了解Java静态块中自定义的JNI静态方法registerNatives()如何调用JNI下RegisterNatives()接口动态注册特定Java Native函数。

Object类中首先声明了JNI静态注册函数registerNatives(),然后通过静态块进行调用

public class Object {

    private static native void registerNatives();
    static {
        registerNatives();
    }
    .......

查看Java Native函数registerNatives()对应的JNI静态注册规则下的C/C++函数:Java_java_lang_Object_registerNatives(JNIEnv *env, jclass cls)

// openjdk\jdk\src\share\native\java\lang\Object.c

static JNINativeMethod methods[] = {
    {"hashCode",    "()I",                    (void *)&JVM_IHashCode},
    {"wait",        "(J)V",                   (void *)&JVM_MonitorWait},
    {"notify",      "()V",                    (void *)&JVM_MonitorNotify},
    {"notifyAll",   "()V",                    (void *)&JVM_MonitorNotifyAll},
    {"clone",       "()Ljava/lang/Object;",   (void *)&JVM_Clone},
};

JNIEXPORT void JNICALL
Java_java_lang_Object_registerNatives(JNIEnv *env, jclass cls)
{
    (*env)->RegisterNatives(env, cls,
                            methods, sizeof(methods)/sizeof(methods[0]));
}

JNIEXPORT jclass JNICALL
Java_java_lang_Object_getClass(JNIEnv *env, jobject this)
{
    if (this == NULL) {
        JNU_ThrowNullPointerException(env, NULL);
        return 0;
    } else {
        return (*env)->GetObjectClass(env, this);
    }
}

Java_java_lang_Object_registerNatives函数内通过JNIEnv调用JNI接口下的RegisterNatives()函数进行Java Native函数和C/C++函数的动态注册绑定。

其中methods结构体数组定义了某个类下Java Native方法与动态链接库中C/C++特定函数的映射关系,而JNINativeMethod结构体代码定义如下

/*
 * used in RegisterNatives to describe native method name, signature,
 * and function pointer.
 */

typedef struct {
    char *name;
    char *signature;
    void *fnPtr;
} JNINativeMethod;

JNINativeMethod结构体成员字段如下表

字段 含义
char *name Java Native 方法名
char *signature Java Native 方法签名
void *fnPtr C/C++ 方法的函数指针

通过上面Object类JNI动态注册流程的简单分析,可总结出如下流程:核心在于先注册一个JNI静态命名规则方法,然后再利用该方法去调用JNI接口RegisterNatives()实现JNI动态注册

(1) Java 类中声明一个 registerNatives() 静态Java Native方法
(2) 动态链接库中定义一个JNI动态注册的映射表,JNINativeMethod 类型的结构体数组
(3) 动态链接库中定义并实现一个符合JNI静态注册规则的 Java_<PackageName>_<ClassName>_registerNatives C/C++方法
(4) Java 类中通过静态块加载相应动态链接库
(5) Java 类中通过静态块调用 registerNatives() 静态Java Native方法
(6) JNI 动态注册开始,Java_<PackageName>_<ClassName>_registerNatives C/C++方法调用JNI动态注册接口 RegisterNatives()
(7) JNI 动态注册完成,Java 类中可调用相应JNI动态注册的Native方法

有了JNI静态注册间接实现JNI动态注册的流程,那么我们就可以很方便的编写属于自己的JNI动态注册项目,这里编写一个乘法运算的JNI动态注册调用案例

Multi.java文件,Java Native方法的声明与调用

package org.qftm.learn.jni.demo2;

/**
 * Created by IntelliJ IDEA.
 * User: Qftm
 * Date: 2022/6/6
 * Time: 21:34
 */

public class Multi {
    // 声明 registerNatives() 静态Java Native方法
    private static native void registerNatives();
    // 声明 multi 静态Java Native方法
    public static native double multi(int num1, double num2);

    static {
        // 静态块加载动态链接库
        System.loadLibrary("demo2");
        // 通过JNI静态命名规则进行查找调用
        registerNatives();
    }

    public static void main(String[] args) {
        // 通过JNI动态注册映射表进行查找调用
        System.out.println(multi(2, 3.3));
    }
}

multi.h头文件,声明JNI静态命名规则C/C++函数

/* DO NOT EDIT THIS FILE - it is machine generated */
#include 
/* Header for class org_qftm_learn_jni_demo2_Multi */

#ifndef _Included_org_qftm_learn_jni_demo2_Multi
#define _Included_org_qftm_learn_jni_demo2_Multi
#ifdef __cplusplus
extern "C" {
#endif
    /*
     * Class:     org_qftm_learn_jni_demo2_Multi
     * Method:    registerNatives
     * Signature: ()V
     */
    JNIEXPORT void JNICALL Java_org_qftm_learn_jni_demo2_Multi_registerNatives (JNIEnv*, jclass);

#ifdef __cplusplus
}
#endif
#endif

dllmain.cpp文件,具体C/C++函数的实现

// dllmain.cpp : 定义 DLL 应用程序的入口点。
#include "pch.h"
#include 
#include "multi.h"

jdouble MultiNumber(JNIEnv* env, jclass cls, jint num1, jdouble num2) {
    return num1 * num2;
}

// typedef struct { char *name; char *signature; void *fnPtr;} JNINativeMethod;
static JNINativeMethod methods[] = {
    {(char*)"multi", (char*)"(ID)D", (void*)MultiNumber},
};

// C: env->FindClass("java/lang/String")、C++: (*env)->FindClass(env, "java/lang/String")
// jint RegisterNatives(jclass clazz, const JNINativeMethod *methods, jint nMethods)
// jclass clazz: Java Native 层对应的类
// const JNINativeMethod *methods: 结构体数组,数组的元素是JNI定义的一个结构体 JNINativeMethod
// jint nMethods: methods 结构体数组的大小
JNIEXPORT void JNICALL Java_org_qftm_learn_jni_demo2_Multi_registerNatives(JNIEnv* env, jclass cls) {
    env->RegisterNatives(cls, methods, sizeof(methods) / sizeof(methods[0]));
}

Native JNI_OnLoad()

JVM在执行System.loadLibrary()加载动态链接库时,底层会调用到JNI_OnLoad(),其主要有两个作用:指定JNI版本、资源初始化。

由于JNI_OnLoad()执行了初始化资源的操作,所以可以在该方法中调用JNI接口下RegisterNatives()方法动态注册Java Native函数,则无需在Java类中声明registerNatives()静态Java Native方法进行操作。

与上面Java JNI 化静为动的实现流程相比,Native层JNI_OnLoad()实现JNI动态注册流程,如下

(1) 动态链接库中定义一个JNI动态注册的映射表,JNINativeMethod 类型的结构体数组
(2) 动态链接库中重写 JNI_OnLoad() 方法
(3) Java 类中通过静态块加载相应动态链接库
(4) JNI 动态注册开始,JNI_OnLoad() 方法调用JNI动态注册接口 RegisterNatives()
(5) JNI 动态注册完成,Java 类中可调用相应JNI动态注册的Native方法

下面以编写一个减法运算的JNI动态注册调用案例来了解JNI_OnLoad(),具体实现代码如下所示

Sub.java,Java Native方法的声明与调用

package org.qftm.learn.jni.demo3;

/**
 * Created by IntelliJ IDEA.
 * User: Qftm
 * Date: 2022/6/7
 * Time: 7:54
 */

public class Sub {
    // 声明 sub 静态Java Native方法
    public static native double sub(int num1, double num2);

    static {
        // 静态块加载动态链接库
        System.loadLibrary("demo3");
    }

    public static void main(String[] args) {
        // 通过JNI动态注册映射表进行查找调用
        System.out.println(sub(8, 3.4));
    }
}

dllmain.cpp文件,具体C/C++函数的实现

// dllmain.cpp : 定义 DLL 应用程序的入口点。
#include "pch.h"
#include 

jdouble SubNumber(JNIEnv* env, jclass cls, jint num1, jdouble num2) {
    return num1 - num2;
}

// typedef struct { char *name; char *signature; void *fnPtr;} JNINativeMethod;
static JNINativeMethod methods[] = {
    {(char*)"sub", (char*)"(ID)D", (void*)SubNumber},
};

JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void* reserved) {
    JNIEnv* env = NULL;

    // 获取 JNIENV
    if (vm->GetEnv(reinterpret_cast(&env), JNI_VERSION_1_6) != JNI_OK)
    {
        return -1;
    }

    // 获取 Java Native 对应的 Class
    jclass jClassName = env->FindClass("org/qftm/learn/jni/demo3/Sub");

    // C: env->FindClass("java/lang/String")、C++: (*env)->FindClass(env, "java/lang/String")
    // jint RegisterNatives(jclass clazz, const JNINativeMethod *methods, jint nMethods)
    // jclass clazz: Java Native 层对应的类
    // const JNINativeMethod *methods: 结构体数组,数组的元素是JNI定义的一个结构体 JNINativeMethod
    // jint nMethods: methods 结构体数组的大小
    jint ret = env->RegisterNatives(jClassName, methods, sizeof(methods)/sizeof(methods[0]));

    return JNI_VERSION_1_6;
}**>
文章首发于跳跳糖:https://tttang.com/archive/1622/

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK