JNI

JNI

JNI是指Java原生接口,它允许在Java虚拟机中的运行的Java代码与其他编程语言(如C、C++、汇编)编写的程序和库进行互操作。它是一种调用规范,我们的编写的JNI程序可以运行在任何实现了该JNI规范的商业虚拟机上。

JNI原生接口规范

背景

尽快完全可以用Java编写应用程序,但是在某些情况下,仅仅Java无法满足应用程序的需求,这时需要本机方法来处理这些情况。

  • 标准Java类库不支持应用程序所需的平台相关的功能
  • 复用其他语言编写的库
  • 使用较低级别的语言如汇编来实现对运行时间要求较高的代码

通过JNI,可以使用本机方法执行以下操作:

  • 创建、检查和更新Java对象(包括数组和字符串)
  • 调用Java方法
  • 捕获并抛出异常
  • 加载类并获取类信息
  • 执行运行时类型检查

和其他技术一样,JNI也并不是一蹴而就的,在没有统一的JNI接口之前,各个厂商也都推出过自己的本机接口调用规范,如JDK1.0本机接口、Netscape提出的JRI、微软Java VM支持的本机接口。
后来大家认为统一的,经过深思熟虑的标准接口会为每个人带来好处。

  • 每个VM厂商都可以支持更大范围的本机代码
  • 工具构建者将不必维护不同种类的本机方法接口
  • 应用程序程序员将能够编写其本地代码的一个版本,就可以在不同的VM上运行

JNI 定义了两个关键数据结构,即“JavaVM”和“JNIEnv”。两者本质上都是指向函数表的二级指针。

编译、加载、链接本地方法

因为Java虚拟机是支持多线程的,本机库也应该编译并与支持多线程的本机编译器链接。

两种链接方式:

  1. 动态链接 System.loadLibrary(“name”);
package pkg;  

class Cls { 
     native double f(int i, String s); 
     static { 
         System.loadLibrary(“pkg_Cls”); 
     } 
} 

动态链接方式中,依据方法名称来分析调用入口。具体格式如下:

  • 以Java_开头
  • 类名的完整限定符
  • 用下划线_分割
  • 方法名称,如Java_pkg_Cls_f
  • 对于重载的本机方法,后面加两个下划线
  1. 静态链接 RegisterNatives()
public class Object {
    private static native void registerNatives();
    static {
        registerNatives();
    }
}

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

上面registerNatives()是通过动态链接形式调用的,在其本地方法内部又通过RegisterNatives函数静态注册了一些函数methods,methods定义如下:

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

本机方法参数

本地方法的第一个参数固定为JNIEnv类型的指针。
第二参数区分静态方法或非静态方法而有所不同。静态方法的第二参数是其Java类的引用,非静态方法的第二个参数是对对象的引用。
之后就是原Java方法中对应的参数。

下面是非静态方法的示例:

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

全局引用和本地引用

JNI将本机代码使用的对象引用分为两类:本地引用和全局引用。
本地引用在本机方法调用期间有效,并在本机方法返回后自动释放。
全局引用在显示释放之前一直有效。

Java对象指针作为本地引用传递给本机方法,JNI函数返回的所有Java对象都是本地引用。本地引用只在当前线程有效,不能传递到另一个线程使用。

本地方法访问Java对象的方法和字段

JNI允许本地方法访问字段并调用Java对象的方法。JNI通过他们的符号名和类型签名来标识方法和字段。
例如要调用类cls中的方法f,先获取方法ID,如下所示:

jmethodID mid = env->GetMethodID(cls, “f”, "(ILJava/lang/String;)D");

然后本地代码可以重复使用方法ID,从而无需多次花费茶查找方法的时间,调用方式如下:

jdouble result = env->CallDoubleMethod(obj, mid, 10, str);

注意:字段或方法ID不会阻止VM卸载已经获取方法ID的类,卸载类后,方法或字段ID变为无效。
如果打算长时间使用方法或字段ID,需要:

  • 保持对基础类的实时引用
  • 每次都重新计算方法或字段ID

JNI类型和数据结构

下表中描述了Java基本数据类型和与其对应的设备相关的本地数据类型:

Java类型本地类型描述
booleanjboolean无符号8位
bytejbyte有符号8位
charjchar无符号16位
shortjshort有符号16位
intjint有符号32位
longjlong有符号64位
floatjfloat32位
doublejdouble64位
voidvoidN/A

JNI包含了许多引用类型,对应了不同种类的Java对象类型。

jobject所有Java对象
jclassClass对象
jstringString对象
jarray数组对象
jobjectArray对象数组
jbooleanArray布尔数组
jbyteArray字节数组
jcharArray字符数组
jshortArray短整型数组
jintArray整型数组
jlongArray长整型数组
jfloatArray浮点型数组
jdoubleArray双精度浮点型数组
jthrowableThrowable对象

类型签名

JNI使用Java虚拟机的类型签名方式来标识一个类型和方法。

类型签名Java类型
Zboolean
Bbyte
Cchar
SShort
Iint
Jlong
Ffloat
Ddoubble
L full-qualified-class;类型的全限定符
[ type某种类型type的数组
(参数类型)返回值类型方法类型

下面为一个示例:

long f(int n, String s, int[] arr);

上述方法的类型签名为:

(ILjava/lang/String;[I)J

接口函数表

所有JNI定义的函数都被包含到了JNINativeInterface_结构体中,它就像一个表,每个函数都可以通过固定的偏移量来访问。它的定义和实现如下所示:


//接口函数表定义
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);
    
    ...
}

//接口函数表实现
struct JNINativeInterface_ jni_NativeInterface = {
    NULL,
    NULL,
    NULL,

    NULL,

    jni_GetVersion,

    jni_DefineClass,
    jni_FindClass,
    
    ...
}

在接口函数表中的前三个函数作为保留项,是为了将来适配COM。第四个函数作为保留项是为了将来再新定义类相关的JNI操作时可以被添加到FindClass函数后面,而不是添加到函数表的末尾。

下面是jni_GetVersion函数的实现:

JNI_LEAF(jint, jni_GetVersion(JNIEnv *env))
  JNIWrapper("GetVersion");
#ifndef USDT2
  DTRACE_PROBE1(hotspot_jni, GetVersion__entry, env);
#else /* USDT2 */
  HOTSPOT_JNI_GETVERSION_ENTRY(
                               env);
#endif /* USDT2 */
#ifndef USDT2
  DTRACE_PROBE1(hotspot_jni, GetVersion__return, CurrentVersion);
#else /* USDT2 */
  HOTSPOT_JNI_GETVERSION_RETURN(
                                CurrentVersion);
#endif /* USDT2 */
  return CurrentVersion;
JNI_END

我们前面说到的本地方法的第一个参数,固定为JNIEnv类型的指针,这个JNIEnv类型定义如下:

struct JNIEnv_;

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

如果是C语言环境,JNIEnv指针就是JniNativeInterface类型。
如果是C++语言环境,JNIEnv对象是JNIEnv_类型的实例,JNIEnv_类型定义如下:

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

    jint GetVersion() {
        return functions->GetVersion(this);
    }
    ...
}

可以看出,其内部就是对JNINativeInterface_结构体的封装。
综上所述,JNIEnv就是一系列JNI函数的集合。

The Invocation API

Invocation API允许在在本地方法中创建一个Java虚拟机实例。如下所示:

 #include <jni.h>       /* where everything is defined */
    ...
    JavaVM *jvm;       /* JavaVM代表一个虚拟机实例 */
    JNIEnv *env;       /* JNI接口函数表 */
    JavaVMInitArgs vm_args; /* JDK/JRE 6 虚拟机初始化参数 */
    JavaVMOption* options = new JavaVMOption[1];
    options[0].optionString = "-Djava.class.path=/usr/lib/java";
    vm_args.version = JNI_VERSION_1_6;
    vm_args.nOptions = 1;
    vm_args.options = options;
    vm_args.ignoreUnrecognized = false;
    /* load and initialize a Java VM, return a JNI interface
     * pointer in env */
    /* 加载并初始化一个Java虚拟机,env是一个返回参数*/
    JNI_CreateJavaVM(&jvm, (void**)&env, &vm_args);
    delete options;
    /* 使用JNIEnv调用Main类的test方法 */
    jclass cls = env->FindClass("Main");
    jmethodID mid = env->GetStaticMethodID(cls, "test", "(I)V");
    env->CallStaticVoidMethod(cls, mid, 100);
    jvm->DestroyJavaVM();

其中JavaVM定义如下:

struct JavaVM_;

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

//JNIInvokeInterface_定义
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);
};

//JNIInvokeInterface_实现
const struct JNIInvokeInterface_ jni_InvokeInterface = {
    NULL,
    NULL,
    NULL,

    jni_DestroyJavaVM,
    jni_AttachCurrentThread,
    jni_DetachCurrentThread,
    jni_GetEnv,
    jni_AttachCurrentThreadAsDaemon
};

从上面可以看出,JavaVM类型就是一个指向Invocation API函数表的指针。大家可能注意到了,有三个Invocation API:JNI_GetDefaultJavaVMInitArgs()、JNI_GetCreateJavaVMs()和JNI_CreateJavaVM()不在JavaVM函数表中。是因为这三个函数可以在没有JavaVM结构的情况下使用。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值