JNI

前提

关于 JNI 的文章很多,但大多都是写怎么使用,本文主要讲述两件事:

  1. 数据类型到底是怎么在 jvm 与 native 之间互传的
  2. jvm 与 native 的函数调用具体是如何执行的

OpenJDK

扯到 JNI,绕不过 jdk(源码就是最好的文档),而开源版本中使用最广泛的就是 OpenJDK。

使用

JNI 参数类型

JNI支持多种Java类型和其对应的原生(Native)类型,以便在Java和C/C++之间传递数据。以下是JNI支持的参数类型,以及它们的映射关系。

1. 基本数据类型

JNI直接支持的基本数据类型:

Java类型 JNI类型 C/C++ 类型
boolean jboolean unsigned char(0或1)
byte jbyte signed char
char jchar unsigned short
short jshort short
int jint int
long jlong long long
float jfloat float
double jdouble double
void void void

这些类型是Java和C之间的基本数据类型对应关系,可以直接用于参数传递和返回。

2. 对象类型

JNI支持Java对象类型传递给C/C++代码。常见的对象类型及其对应关系如下:

Java类型 JNI类型 描述
Object jobject Java的任意对象
String jstring Java字符串
Class jclass Java类
Throwable jthrowable 异常类
Boolean jobject Java包装类型
Integer jobject Java包装类型
Double jobject Java包装类型
数组类型 各种j*Array 包括jbooleanArray

3. 数组类型

JNI支持Java数组,可以传递给C/C++。不同的基本数据类型数组都有对应的JNI类型:

Java数组类型 JNI类型 描述
boolean[] jbooleanArray 布尔数组
byte[] jbyteArray 字节数组
char[] jcharArray 字符数组
short[] jshortArray 短整型数组
int[] jintArray 整型数组
long[] jlongArray 长整型数组
float[] jfloatArray 浮点型数组
double[] jdoubleArray 双精度浮点型数组
Object[] jobjectArray 对象数组

数组操作的常用函数

在JNI中,可以通过一些函数操作数组,如GetArrayLengthGetIntArrayElementsSetIntArrayRegion等来读写Java数组。

4. 特殊类型

JNI还定义了一些特殊类型来帮助跨语言调用:

JNI 类型 描述
JNIEnv* JNI环境指针,每个线程都有独立的JNIEnv
JavaVM* Java虚拟机指针,用于全局获取Java虚拟机实例
jfieldID Java类字段的引用ID
jmethodID Java类方法的引用ID
jvalue JNI函数调用中使用的参数联合类型

示例代码

下面是一个带有数组和字符串参数的JNI方法示例:

public class ExampleJNI {
    // 声明native方法,传入一个整型数组和一个字符串
    public native int processArrayAndString(int[] numbers, String text);
    
    static {
        System.loadLibrary("example");
    }
}

对应的C代码:

#include <jni.h>
#include <stdio.h>
#include "ExampleJNI.h"

JNIEXPORT jint JNICALL Java_ExampleJNI_processArrayAndString(JNIEnv *env, jobject obj, jintArray numbers, jstring text) {
    // 处理整型数组
    jint *numArray = (*env)->GetIntArrayElements(env, numbers, NULL);
    jsize length = (*env)->GetArrayLength(env, numbers);
    
    // 处理字符串
    const char *nativeString = (*env)->GetStringUTFChars(env, text, NULL);
    
    // 示例处理:将数组所有元素相加
    int sum = 0;
    for (int i = 0; i < length; i++) {
        sum += numArray[i];
    }
    
    printf("Received string: %s\n", nativeString);
    printf("Sum of array: %d\n", sum);
    
    // 释放内存
    (*env)->ReleaseIntArrayElements(env, numbers, numArray, 0);
    (*env)->ReleaseStringUTFChars(env, text, nativeString);
    
    return sum;
}

在这个示例中:

JavaVM

jdk 中关于 JavaVM 的定义代码如下(JDK - JNIInvokeInterface_):

struct JNIInvokeInterface_;

struct JavaVM_;

#ifdef __cplusplus
typedef JavaVM_ JavaVM;
#else
typedef const struct JNIInvokeInterface_ *JavaVM;
#endif
struct JNIInvokeInterface_ {
    void *reserved0;
    void *reserved1;
    void *reserved2;

    jint (JNICALL *DestroyJavaVM)(JavaVM *vm);

    jint (JNICALL *AttachCurrentThread)(JavaVM *vm, void **penv, void *args);

    jint (JNICALL *DetachCurrentThread)(JavaVM *vm);

    jint (JNICALL *GetEnv)(JavaVM *vm, void **penv, jint version);

    jint (JNICALL *AttachCurrentThreadAsDaemon)(JavaVM *vm, void **penv, void *args);
};

struct JavaVM_ {
    const struct JNIInvokeInterface_ *functions;
#ifdef __cplusplus

    jint DestroyJavaVM() {
        return functions->DestroyJavaVM(this);
    }
    jint AttachCurrentThread(void **penv, void *args) {
        return functions->AttachCurrentThread(this, penv, args);
    }
    jint DetachCurrentThread() {
        return functions->DetachCurrentThread(this);
    }

    jint GetEnv(void **penv, jint version) {
        return functions->GetEnv(this, penv, version);
    }
    jint AttachCurrentThreadAsDaemon(void **penv, void *args) {
        return functions->AttachCurrentThreadAsDaemon(this, penv, args);
    }
#endif
};

在上述的头文件中可以看出 JavaVM 是用于获取 JNIEnv、附加/分离线程

需要注意的是 jni 的整体设计是基于 C 的,而不是 C++,所以并没有面向对象的概念。JavaVM 可以理解成一个抽象接口,表示 java 虚拟机实例,但是实现并非是以面向对象的接口形式来实现。

JavaVM 是全局的,一个进程(通常)只有一个 JavaVM 示例

关键“成员变量”的挂在点:

成员变量 挂载位置 文件路径 描述
线程管理 (Threads) Threads::_main_vm_thread src/hotspot/share/runtime/thread.cpp 管理所有 JVM 线程,包括主线程和附加线程。
JNI 环境 (JNIEnv) JavaThread::_jni_environment src/hotspot/share/runtime/thread.hpp 每个线程的 JNI 环境,用于与 Java 交互。
类加载器 (ClassLoaderData) ClassLoaderData::_class_loader src/hotspot/share/classfile/classLoaderData.hpp 管理类加载器的数据,包括方法区的类定义。
内存管理 (Heap) Universe::_collectedHeap src/hotspot/share/memory/universe.cpp 管理 JVM 的堆内存,包括垃圾回收。
全局配置 (Arguments) Arguments 单例 src/hotspot/share/runtime/arguments.cpp JVM 启动时解析的全局参数,控制 JVM 行为。

JNIEnv

jdk 中关于 JNIEnv 的定义代码简要如下(JDK - JNINativeInterface_):

struct JNINativeInterface_;

struct JNIEnv_;

#ifdef __cplusplus
typedef JNIEnv_ JNIEnv;
#else
typedef const struct JNINativeInterface_ *JNIEnv;
#endif
struct JNINativeInterface_ {
    //  仅列出使用频率高的
    ...

    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 *GetMethodID) (JNIEnv *env, jclass clazz, const char *name, const char *sig);

    jobject (JNICALL *GetObjectField) (JNIEnv *env, jobject obj, jfieldID fieldID);
    jboolean (JNICALL *GetBooleanField) (JNIEnv *env, jobject obj, jfieldID fieldID);
    jbyte (JNICALL *GetByteField) (JNIEnv *env, jobject obj, jfieldID fieldID);

    jstring (JNICALL *NewString) (JNIEnv *env, const jchar *unicode, jsize len);
    jsize (JNICALL *GetStringLength) (JNIEnv *env, jstring str);
    const jchar *(JNICALL *GetStringChars) (JNIEnv *env, jstring str, jboolean *isCopy);
    void (JNICALL *ReleaseStringChars) (JNIEnv *env, jstring str, const jchar *chars);

    jstring (JNICALL *NewStringUTF) (JNIEnv *env, const char *utf);
    jsize (JNICALL *GetStringUTFLength) (JNIEnv *env, jstring str);
    const char* (JNICALL *GetStringUTFChars) (JNIEnv *env, jstring str, jboolean *isCopy);
    void (JNICALL *ReleaseStringUTFChars) (JNIEnv *env, jstring str, const char* chars);

    jsize (JNICALL *GetArrayLength) (JNIEnv *env, jarray array);
    jobjectArray (JNICALL *NewObjectArray) (JNIEnv *env, jsize len, jclass clazz, jobject init);
    jobject (JNICALL *GetObjectArrayElement) (JNIEnv *env, jobjectArray array, jsize index);
    jbooleanArray (JNICALL *NewBooleanArray) (JNIEnv *env, jsize len);
    jbyteArray (JNICALL *NewByteArray) (JNIEnv *env, jsize len);

    jobject (JNICALL *GetStaticObjectField) (JNIEnv *env, jclass clazz, jfieldID fieldID);
    jboolean (JNICALL *GetStaticBooleanField) (JNIEnv *env, jclass clazz, jfieldID fieldID);

    ...
};

同 JavaVM,JNIEnv 是 JNINativeInterface_ 结构体的指针,而 JNINativeInterface_ 则是一个函数指针表

native 是怎么拿到 Java 变量的

示例:

public class MyObject {
    private String name;

    public MyObject(String name) {
        this.name = name;
    }

    public native void printName();
}
// 实现 native 方法,为了展示逻辑,无各种边界判断
JNIEXPORT void JNICALL Java_MyObject_printName(JNIEnv *env, jobject obj) {
    jclass clazz = (*env)->GetObjectClass(env, obj);  // 获取类对象
    jfieldID fid = (*env)->GetFieldID(env, clazz, "name", "Ljava/lang/String;"); // 获取字段的 Field ID
    jstring name = (jstring)(*env)->GetObjectField(env, obj, fid);  // 获取字段值(jstring 类型)
    const char *c_name = (*env)->GetStringUTFChars(env, name, NULL);  // 将 jstring 转换为 C 字符串
    printf("Name: %s\n", c_name);  // 打印字段值
    (*env)->ReleaseStringUTFChars(env, name, c_name); // 释放资源
}

上述示例中,看起来好像挺像回事,但是 native 具体是怎么夸 runtime 拿到具体变量值的又没展示出来。(个人认为)核心是 GetObjectField,这里有两个关键点:

  1. jstring 到底是什么
  2. GetObjectField 具体怎么实现

jstring 到底是什么

typedef jobject jstring;

class _jobject {};
class _jclass : public _jobject {};
class _jstring : public _jobject {};

typedef _jobject *jobject;
typedef _jclass *jclass;
typedef _jthrowable *jthrowable;
typedef _jstring *jstring;

JVM 是怎么获取 native 变量的

Java 变量与 native 具体怎么传递

AttachCurrentThread

限制

核心原理

JVM、native 交互