JNIEnv API

2022-06-05  本文已影响0人  程序员札记

详细探讨了JNI调用如何使用,JNI的库文件是如何加载的,下面来详细探讨下JNI API,这API是做什么的,有啥注意事项,这是后续JNI开发的基础。


image.png

数据类型

Java数据类型

Java的数据类型分为基本类型(primitive type,又称原生类型或者原始类型)和引用类型(reference type),其中基本类型又分为数值类型,boolean类型和returnAddress类型三类。returnAddress类型在Java语言中没有对应的数据类型,由JVM使用表示指向某个字节码的指针。JVM定义了boolean类型,但是对boolean类型的支持非常有限,boolean类型没有任何专供boolean值使用的字节码指令,java语言表达式操作boolean值,都是使用int类型对应的字节码指令完成的,boolean数组的访问修改共用byte数组的baload和bstore指令;JVM规范中明确了1表示true,0表示false,但是未明确boolean类型的长度,Hotspot使用C++中无符号的char类型表示boolean类型,即boolean类型占8位。数值类型分为整数类型和浮点数类型,如下:

整数类型包含:

浮点类型包括:

引用类型

引用类型在JVM中有三种,类类型(class type),数组类型(array type)和接口类型(interface type),数组类型最外面的一维元素的类型称为该数组的组件类型,组件类型也可以是数组类型,如果组件类型不是元素类型则称为该数组的元素类型,引用类型其实就是C++中的指针。
JVM规范中并没有强引用,软引用,弱引用和虚引用的概念,JVM定义的引用就是强引用,软引用,弱引用和虚引用是JDK结合垃圾回收机制提供的功能支持而已。
参考:Java 的强引用、弱引用、软引用、虚引用

JNI数据类型

JNI数据类型其实就是Java数据类型在Hotspot中的具体表示或者对应的C/C++类型,类型的定义参考OpenJDK hotspot/src/share/prims/jni.h中,如下图:

image.png

部分类型跟CPU架构相关的,通过宏定义jni_md.h引入,如下图:

image.png

通常的服务器都是x86_64架构的,其定义的类型如下:

image.png

从上面的定义可以得出,JVM中除基本数据类型外,所有的引用类型都是指针,JVM这里只是定义了空白的类来区分不同的引用类型,具体处理指针时会将指针强转成合适的数据类型,如jobject指针会强转成Oop指针,详情可以参考JNIEnv API的实现。

Java同JNI数据类型的对应关系

怎么去验证Java数据类型和JNI数据类型的对应关系了?可以通过javah生成的本地方法头文件,比对Java方法和对应的本地方法的参数可以看出两者的对应关系,如下示例:

package jni;
 
import java.util.List;
 
public class JniTest{
 
    static
    {
        System.loadLibrary("HelloWorld");
    }
 
    public native static void say(boolean a, byte b, char c, short d, int e, long f, float g, double h, String s, List l,Throwable t,Class cl,
                                  boolean[] a2, byte[] b2, char[] c2, short[] d2, int[] e2, long[] f2, float[] g2, double[] h2,String[] s2);
 
}

生成的jni_JniTest.h头文件如下:

/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class jni_JniTest */
 
#ifndef _Included_jni_JniTest
#define _Included_jni_JniTest
#ifdef __cplusplus
extern "C" {
#endif
/*
 * Class:     jni_JniTest
 * Method:    say
 * Signature: (ZBCSIJFDLjava/lang/String;Ljava/util/List;Ljava/lang/Throwable;Ljava/lang/Class;[Z[B[C[S[I[J[F[D[Ljava/lang/String;)V
 */
JNIEXPORT void JNICALL Java_jni_JniTest_say
  (JNIEnv *, jclass, jboolean, jbyte, jchar, jshort, jint, jlong, jfloat, jdouble, jstring, jobject, jthrowable, jclass, jbooleanArray, jbyteArray, jcharArray, jshortArray, jintArray, jlongArray, jfloatArray, jdoubleArray, jobjectArray);
 
#ifdef __cplusplus
}
#endif
#endif

本地方法的第一个入参都是JNIEnv指针,第二个入参根据本地方法是否是静态方法区别处理,如果是静态方法,第二个入参是该类的Class即jclass,如果是普通方式则是当前类实例的引用即jobject,其他的入参跟Java方法的入参一样,将Java的数据类型映射到JNI数据类型即可。在Java代码调用本地方法同调用Java一样都是值传递,基本类型参数传递的是参数值,对象类型参数传递的是对象引用,JVM必须跟踪所有传递到本地方法的对象引用,确保所引用的对象没有被垃圾回收掉,本地代码也需要及时通知JVM回收某个对象,垃圾回收器需要能够回收本地代码不再引用的对象。

两者的对应关系具体如下表:

Java数据类型 JNI数据类型 x86 C++类型 长度 备注
boolean jboolean unsignedchar 8
byte jbyte signed char 8
char jchar unsigned short 16
short jshort short 16
int jint int 16
long jlong long 64
float jfloat float 32
double jdouble double 64
String jstring _jstring * 32/64 类指针,在64位机器上默认开启指针压缩,指针长度是32位,否则是64位,不过被压缩的指针仅限于指向堆对象的指针
Class jclass _jclass *
Throwable jthrowable _jthrowable *
boolean[] jbooleanArray _jbooleanArray *
byte[] jbyteArray _jbyteArray *
char[] jcharArray _jcharArray *
short[] jshortArray _jshortArray *
int[] jintArray _jintArray *
long[] jlongArray _jlongArray *
float[] jfloatArray _jfloatArray *
double[] jdoubleArray _jdoubleArray *
Object[] jobjectArray _jobjectArray *

API定义

JNI标准API的发展

早期不同厂商的JVM实现提供的JNI API接口有比较大的差异,这些差异导致开发者必须编写不同的代码适配不同的平台,简单的介绍下这些API接口:

这些API经过各厂商充分讨论,因为各种问题最终没有成为标准API,详情参考Java Native Interface Specification Contents Chapter 1: Introduction

JNIEnv和JavaVM定义

本地代码调用JNI API的入口只有两个JNIEnv和JavaVM类,这两个都在jni.h中定义,如下:


image.png

部署后端应用程序的服务器都具备C++运行时,所以只关注C++下的代码即可,即#ifdef __cplusplus下的代码。

JNIENV_和JavaVM_结构体的定义如下:

image.png

两者的实现其实是对结构体JNINativeInterface_和JNIInvokeInterface_的简单包装而已,两者定义如下:

image.png

从定义上可以看出两者的结构类似于C++中的虚函数表,结构体中没有定义方法而是方法指针,这样一方面实现了C++下对C的兼容,C的结构体中不能定义方法但是可以定义方法指针,C++的结构体基本被扩展成class,可以定义方法和继承;另一方面这种做法实现了接口与实现分离的效果,调用API的本地代码与JVM中具体的实现类解耦,虚拟机可以基于此结构轻松的提供两种实现版本的JNI API,比如其中一个对入参严格校验,另一个只做关键参数最少的校验,让方法指针指向不同的实现即可,API调用方完全无感知。官方文档中提供了一张图描述这种结构,如下图:

image.png

其中的JNI interface pointer就是传入本地方法的参数JNIEnv指针, 该指针指向的JNIEnv对象本身包含了一个指向JNINativeInterface_结构体的指针,即图中的Pointer,JNINativeInterface_结构体在内存中相当于一个指针数组,即图中的Array of pointers to JNI functions,指针数组中的每个指针都是具体的方法实现的指针。注意JVM规范要求同一个线程内多次JNI调用接收的JNIEnv或者JavaVM指针都是同一个指针,且该指针只在该线程内有效,因此本地代码不能讲该指针从当前线程传递到另一个线程中。

JNINativeInterface_和JNIInvokeInterface_两者的赋值如下:

image.png

前面的三个NULL都是为未来兼容COM对象保留的,JNINativeInterface_中第四个NULL是为未来的一个类相关的JNI操作保留的。结构体JNINativeInterface_和JNIInvokeInterface_包含的方法实现在跟jni.h同目录的jni.cpp中,JNIEnv和JavaVM类的初始化可以参考《Hotspot启动和初始化源码解析》。

三、异常处理
所有的JNI方法同大多数的C库函数一样不检查传入的参数的正确性,这点由调用方负责检查,如果参数错误可能导致JVM直接宕机。大多数情况下,JNI方法通过返回一个特定的错误码或者抛出一个Java异常的方式报错,调用方可以通过ExceptionOccurred()方法判断是否发生了异常,如本地方法调用Java方法,判断Java方法执行期间是否发生了异常,并通过该方法获取异常的详细信息。

JNI允许本地方法抛出或者捕获Java异常,未被本地方法捕获的异常会向上传递给方法的调用方。本地方法有两种方式处理异常,一种是直接返回,导致异常在调用本地方法的Java方法中被抛出;一种是调用ExceptionClear()方法清除这个异常,然后继续执行本地方法的逻辑。当异常产生,本地方法必须先清除该异常才能调用其他的JNI方法,当异常尚未处理时,只有下列方法可以被安全调用:

ExceptionOccurred()
ExceptionDescribe()
ExceptionClear()
ExceptionCheck()
ReleaseStringChars()
ReleaseStringUTFChars()
ReleaseStringCritical()
Release<Type>ArrayElements()
ReleasePrimitiveArrayCritical()
DeleteLocalRef()
DeleteGlobalRef()
DeleteWeakGlobalRef()
MonitorExit()
PushLocalFrame()
PopLocalFrame()

异常处理相关API如下:

测试用例如下:

package jni;
 
public class ThrowTest {
 
    static {
        System.load("/home/openjdk/cppTest/ThrowTest.so");
    }
 
 
    public static native void rethrowException(Exception e);
 
    public static native void handlerException();
 
    public static void main(String[] args) {
        handlerException();
        rethrowException(new UnsupportedOperationException("Unsurpported ThrowTest"));
    }
 
 
}
#include "ThrowTest.h"
#include <stdio.h>
 
JNIEXPORT void JNICALL Java_jni_ThrowTest_rethrowException(JNIEnv * env,
        jclass cls, jthrowable e) {
    printf("Java_jni_ThrowTest_rethrowException\n");
    env->Throw(e);
}
 
void throwNewException(JNIEnv * env) {
    printf("throwNewException\n");
    jclass unsupportedExceptionCls = env->FindClass(
            "java/lang/UnsupportedOperationException");
    env->ThrowNew(unsupportedExceptionCls, "throwNewException Test\n");
}
 
JNIEXPORT void JNICALL Java_jni_ThrowTest_handlerException(JNIEnv * env,
        jclass cls) {
    throwNewException(env);
    jboolean result = env->ExceptionCheck();
    printf("ExceptionCheck result->%d\n", result);
    env->ExceptionDescribe();
    result = env->ExceptionCheck();
    printf("ExceptionCheck for ExceptionDescribe result->%d\n", result);
    throwNewException(env);
    jthrowable e = env->ExceptionOccurred();
    if (e) {
        printf("ExceptionOccurred not null\n");
    } else {
        printf("ExceptionOccurred null\n");
    }
    env->ExceptionClear();
    printf("ExceptionClear\n");
    e = env->ExceptionOccurred();
    if (e) {
        printf("ExceptionOccurred not null\n");
    } else {
        printf("ExceptionOccurred null\n");
    }
}

引用操作

引用的定义

jni.h中关于引用只定义了一个枚举,如下:

image.png

jobjectRefType表示引用类型,仅限于本地方法使用,具体如下:

综上,这里的引用其实就是Java中new一个对象返回的引用,本地引用相当于Java方法中的局部变量对Java对象实例的引用,全局引用相当于Java类的静态变量对Java对象实例的引用,其本质跟C++智能指针模板一样,是对象指针的二次包装,通过包装避免了该指针指向的对象被垃圾回收器回收掉,因此JNI中通过隐晦的引用访问Java对象的消耗比通过指针直接访问要高点,但是这是JVM对象和内存管理所必须的。

引用API

相关API如下:

image.png

WITH_LOCAL_REFS和END_WITH_LOCAL_REFS两个宏的定义如下:

image.png

NewGlobalRef的使用场景通常是初始化C/C++的全局属性,需要通过全局引用确保该属性指向的某个Java对象实例不被垃圾回收器回收掉,如下图:

image.png

NewLocalRef的使用场景不多,通常是用来检测目标对象是否已经被回收掉了,如果被回收了则该方法返回NULL,如下图:


image.png

创建了一个新对象并返回该对象的本地引用通常直接调用JNIHandles::make_local实现,jni_NewLocalRef的实现也是通过该方法完成,所以NewLocalRef被直接使用的不多,如下:

image.png

类和对象操作

类操作API如下:

package jni;
 
class A{
 
}
 
public class ObjTest extends A {
 
    static {
        System.load("/home/openjdk/cppTest/ObjTest.so");
    }
 
    public ObjTest() {
        System.out.println("default");
    }
 
    public ObjTest(int age) {
        System.out.println("param Construtor,age->"+age);
    }
 
    public native static void test(Object a);
 
    public static void main(String[] args) {
        test(new ObjTest());
    }
}
#include "ObjTest.h"
#include <stdio.h>
 
 
JNIEXPORT void JNICALL Java_jni_ObjTest_test
  (JNIEnv * env, jclass jcl,jobject obj){
 
    jcl=env->GetObjectClass(obj);
    jclass objACls=env->FindClass("jni/A");
    jboolean result=env->IsAssignableFrom(jcl,objACls);
    printf("IsAssignableFrom result->%d\n",result);
 
    jobject objTest=env->AllocObject(jcl);
    printf("AllocObject succ \n");
 
    jmethodID defaultConst=env->GetMethodID(jcl,"<init>","()V");
    objTest=env->NewObject(jcl,defaultConst);
    printf("default construct new succ \n");
 
    jmethodID paramConst=env->GetMethodID(jcl,"<init>","(I)V");
    objTest=env->NewObject(jcl,paramConst,12);
    printf("param construct succ new \n");
 
    jclass superCls=env->GetSuperclass(jcl) jobject superObj=env->AllocObject(superCls);
    result=env->IsInstanceOf(superObj,objACls);
    printf("IsInstanceOf result->%d\n",result);
 
    jobject objTest2=env->NewLocalRef(objTest);
    result=env->IsSameObject(objTest2,objTest);
    printf("IsSameObject result->%d\n",result);
 
}

字段和方法操作

jfieldID和jmethodID定义

JNI中使用jfieldID来标识一个某个类的字段,jmethodID来标识一个某个类的方法,jfieldID和jmethodID都是根据他们的字段名(方法名)和描述符确定的,通过jfieldID读写字段或者通过jmethodID调用方法都会避免二次查找,但是这两个ID不能阻止该字段或者方法所属的类被卸载,如果类被卸载了则jfieldID和jmethodID失效了需要重新计算,因此如果希望长期使用jfieldID和jmethodID则需要保持对该类的持续引用,即建立对该类的全局引用,两者的定义在jni.h中,如下:

image.png

这两个并非常规的字符串或者数字形式的ID,而是一个指针,指向实际保存字段信息和方法的该类的Klass。

API说明

相关的API如下:

注意在调用方法或者设置属性传参数时,需要密切关注参数类型,尤其是基本类型,只有字段或者方法声明明确使用了基本类型传参才能使用基本类型,否则必须通过Integer等包装类的构造方法将基本类型转换为对应包装类的对象;另一个需要注意的就是可变参数类型,Java中可以传入数量可变的参数,这些参数最终会被编译器转换成一个数组,即这类参数类型实际是一个数组,因此传参时不能跟Java一样,而需要显示的传入一个数组类型。示例如下:

package jni;
 
import java.util.Arrays;
import java.util.List;
 
class SuperA{
    public void say(){
        System.out.println("say SuperA");
    }
 
    public void add(int a,int b){
        System.out.println("SuperA add a->"+a+",b->"+b+",result->"+(a+b));
    }
}
 
public class FiledMethodTest extends SuperA {
 
    static {
        System.load("/home/openjdk/cppTest/FiledMethodTest.so");
    }
 
    private List<Integer> list;
 
    private boolean boolField;
 
    private byte byteField;
 
    private char charField;
 
    private short shortField;
 
    private int intField;
 
    private long longField;
 
    private float floatField;
 
    private double doubleField;
 
    private static int staticFiled;
 
    public FiledMethodTest() {
        list= Arrays.asList(1,2);
        boolField=false;
        byteField=11;
        charField='c';
        shortField=12;
        intField=13;
        longField=14;
        floatField=15.0f;
        doubleField=16.0;
        staticFiled=17;
    }
 
    @Override
    public void say() {
        System.out.println("say FiledMethodTest");
    }
 
 
    public List<Integer> getList() {
        return list;
    }
 
    private static void printList(List list){
        if(list==null){
            System.out.println("list is null");
        }else {
            System.out.println("list->" + list);
        }
    }
 
    private void printObj() {
        System.out.println("FiledMethodTest{" +
                "list=" + list +
                ", boolField=" + boolField +
                ", byteField=" + byteField +
                ", charField=" + charField +
                ", shortField=" + shortField +
                ", intField=" + intField +
                ", longField=" + longField +
                ", floatField=" + floatField +
                ", doubleField=" + doubleField +
                ", staticFiled=" + staticFiled +
                '}');
    }
 
    public native static void test(FiledMethodTest a);
 
    public static void main(String[] args) throws Exception {
        FiledMethodTest a=new FiledMethodTest();
        System.out.println("start test");
        test(a);
 
    }
}
#include "FiledMethodTest.h"
#include <stdio.h>
 
JNIEXPORT void JNICALL Java_jni_FiledMethodTest_test(JNIEnv * env, jclass jcl,
        jobject obj) {
 
    //注意字段和方法描述符中如果是其他的类,必须带上后面的分号
    jfieldID listId = env->GetFieldID(jcl, "list", "Ljava/util/List;");
    jfieldID boolFieldId = env->GetFieldID(jcl, "boolField", "Z");
    jfieldID byteFieldId = env->GetFieldID(jcl, "byteField", "B");
    jfieldID charFieldId = env->GetFieldID(jcl, "charField", "C");
    jfieldID shortFieldId = env->GetFieldID(jcl, "shortField", "S");
    jfieldID intFieldId = env->GetFieldID(jcl, "intField", "I");
    jfieldID longFieldId = env->GetFieldID(jcl, "longField", "J");
    jfieldID floatFieldId = env->GetFieldID(jcl, "floatField", "F");
    jfieldID doubleFieldId = env->GetFieldID(jcl, "doubleField", "D");
    jfieldID staticFiledId = env->GetStaticFieldID(jcl, "staticFiled", "I");
 
    jmethodID printListId = env->GetStaticMethodID(jcl, "printList",
            "(Ljava/util/List;)V");
    jmethodID printObjId = env->GetMethodID(jcl, "printObj", "()V");
    jmethodID getListId = env->GetMethodID(jcl, "getList",
            "()Ljava/util/List;");
    jmethodID sayId=env->GetMethodID(jcl,"say","()V");
    jmethodID addId=env->GetMethodID(jcl,"add","(II)V");
 
    jclass arrayListCls = env->FindClass("java/util/ArrayList");
    jmethodID list_costruct = env->GetMethodID(arrayListCls, "<init>", "()V");
    jmethodID listAddId = env->GetMethodID(arrayListCls, "add",
            "(Ljava/lang/Object;)Z");
 
    jclass arraysCls=env->FindClass("java/util/Arrays");
    jmethodID asListId=env->GetStaticMethodID(arraysCls,"asList","([Ljava/lang/Object;)Ljava/util/List;");
 
    jclass intergerCls = env->FindClass("java/lang/Integer");
    jmethodID interger_costruct = env->GetMethodID(intergerCls, "<init>",
            "(I)V");
 
    //如果找不到方法或者字段不会直接报错,需要手动执行异常检查
    if (env->ExceptionCheck()) {
        jthrowable err = env->ExceptionOccurred();
        env->Throw(err);
    }
 
    jobject listObj = env->GetObjectField(obj, listId);
    env->CallStaticVoidMethod(jcl, printListId, listObj);
 
    jobject listObj2 = env->CallObjectMethod(obj, getListId);
    jboolean issame = env->IsSameObject(listObj, listObj2);
    printf("issame->%d\n", issame);
 
    jboolean boolField = env->GetBooleanField(obj, boolFieldId);
    printf("boolField->%d\n", boolField);
 
    jbyte byteField = env->GetByteField(obj, byteFieldId);
    printf("byteField->%d\n", byteField);
 
    jchar charField = env->GetCharField(obj, charFieldId);
    printf("charField->%d\n", charField);
 
    jshort shortField = env->GetShortField(obj, shortFieldId);
    printf("shortField->%d\n", shortField);
 
    jint intField = env->GetIntField(obj, intFieldId);
    printf("intField->%d\n", intField);
 
    jlong longField = env->GetLongField(obj, longFieldId);
    printf("longField->%d\n", longField);
 
    jfloat floatField = env->GetFloatField(obj, floatFieldId);
    printf("floatField->%f\n", floatField);
 
    jdouble doubleField = env->GetDoubleField(obj, doubleFieldId);
    printf("doubleField->%f\n", doubleField);
 
    jint staticFiled = env->GetStaticIntField(jcl, staticFiledId);
    printf("staticFiled->%d\n", staticFiled);
 
    //JNI中没有对基本类型的自动装箱拆箱机制,必要时需要手动包装
    jobject intObj = env->NewObject(intergerCls, interger_costruct, 3);
    jobject intObj2 = env->NewObject(intergerCls, interger_costruct, 4);
 
//  jobject newList = env->NewObject(arrayListCls, list_costruct);
    //add方法接受的参数实际是一个对象,因此需要手动包装
//  env->CallBooleanMethod(newList, listAddId, intObj);
//  env->CallBooleanMethod(newList, listAddId, intObj2);
 
    //Arrays.asList方法在Java中是不可变参数,实际多个参数最终会被转变成数组,因此这里的入参必须是数组
    jobjectArray objArray=env->NewObjectArray(2,intergerCls,intObj);
    env->SetObjectArrayElement(objArray,1,intObj2);
    jobject newList=env->CallStaticObjectMethod(arraysCls,asListId,objArray);
 
    env->SetObjectField(obj, listId, newList);
    env->SetBooleanField(obj, boolFieldId, 1);
    env->SetByteField(obj, byteFieldId, 21);
    env->SetCharField(obj, charFieldId, 'd');
    env->SetShortField(obj, shortFieldId, 22);
    env->SetIntField(obj, intFieldId, 23);
    env->SetLongField(obj, longFieldId, 24);
    env->SetFloatField(obj, floatFieldId, 25.0);
    env->SetDoubleField(obj, doubleFieldId, 26.0);
    env->SetStaticIntField(jcl, staticFiledId, 27);
 
    env->CallVoidMethod(obj, printObjId);
 
    jclass superCls=env->GetSuperclass(jcl);
    jmethodID superSayId=env->GetMethodID(superCls,"say","()V");
    //如果子类没有覆写则使用父类的实现,否则使用子类覆写的实现
    env->CallVoidMethod(obj,sayId);
    env->CallVoidMethod(obj,addId,3,4);
    //使用jclass的方法实现,可以是子类的也可以是父类的,取决于后面的methodId
    env->CallNonvirtualVoidMethod(obj,jcl,sayId);
    env->CallNonvirtualVoidMethod(obj,jcl,superSayId);
    env->CallNonvirtualVoidMethod(obj,superCls,sayId);
    env->CallNonvirtualVoidMethod(obj,superCls,superSayId);
 
}

注意上述示例中printObj和printList方法都是私有方法,但是通过JNI接口一样可以正常调用,说明JNI无视Java的访问权限控制,可以访问任何方法和字段。

上一篇 下一篇

猜你喜欢

热点阅读