【JNI编程】调用API

640 阅读11分钟

Invocation API允许软件供应商将JVM加载到任意本地应用程序中。供应商可以提供支持Java的应用程序,而无需链接JVM源代码。

本章首先概述了Invocation API。接下来是所有Invocation API函数的参考页面。

一、概述

以下代码示例说明了如何在Invocation API中使用函数。在此示例中,C++代码创建JVM并调用静态方法,称为Main.test。为清楚起见,我们省略了错误检查。

    #include <jni.h>       /* where everything is defined */
    ...
    JavaVM *jvm;       /* denotes a Java VM */
    JNIEnv *env;       /* pointer to native method interface */
    JavaVMInitArgs vm_args; /* JDK/JRE 6 VM initialization arguments */
    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 */
    JNI_CreateJavaVM(&jvm, (void**)&env, &vm_args);
    delete options;
    /* invoke the Main.test method using the JNI */
    jclass cls = env->FindClass("Main");
    jmethodID mid = env->GetStaticMethodID(cls, "test", "(I)V");
    env->CallStaticVoidMethod(cls, mid, 100);
    /* We are done. */
    jvm->DestroyJavaVM();

此示例在API中使用三个函数。 Invocation API允许本机应用程序使用JNI接口指针来访问VM功能。该设计类似于Netscape的JRI嵌入接口。

1.1 创建VM

JNI_CreateJavaVM()函数加载并初始化Java VM并返回指向JNI接口指针的指针。调用JNI_CreateJavaVM()的线程被认为是主线程。

1.2 连接到VM

JNI接口指针(JNIEnv)仅在当前线程中有效。 如果另一个线程需要访问Java VM,它必须首先调用AttachCurrentThread()以将自身连接到VM并获取JNI接口指针。一旦连接到VM,本地线程就像在本地方法中运行的普通Java线程一样工作。本地线程保持连接到VM,直到它调用DetachCurrentThread()来分离自身。

连接的线程应该有足够的堆栈空间来执行合理的工作量。 每个线程的堆栈空间分配是特定于操作系统的。例如,使用pthreads,可以在pthread_create的pthread_attr_t参数中指定堆栈大小。

1.3 从VM中分离

连接到VM的本地线程必须在退出之前调用DetachCurrentThread()以分离自身。如果调用堆栈上有Java方法,则线程无法分离。

1.4 卸载VM

JNI_DestroyJavaVM()函数卸载Java VM。从JDK/JRE 1.1开始,只有主线程可以通过调用DestroyJavaVM来卸载VM。从JDK/JRE 1.2开始,删除了限制,任何线程都可以调用DestroyJavaVM来卸载VM。

VM将等待,直到当前线程是惟一的非守护用户线程,然后才实际卸载。用户线程包括Java线程和连接的本机线程。之所以存在这种限制,是因为Java线程或连接的本机线程可能持有系统资源,如锁、窗口等,VM不能自动释放这些资源。通过将当前线程限制为VM卸载时唯一运行的线程,程序员将承担释放由任意线程持有的系统资源的负担。

二、库和版本管理

从JDK/JRE 1.1开始,一旦加载了本地库,就可以从所有类加载器中看到它。因此,不同类加载器中的两个类可以使用相同的本地方法链接。这导致两个问题:

  • 类可能会错误地链接到由不同类加载器中具有相同名称的类加载的本地库。
  • 本地方法可以轻松地混合来自不同类加载器的类。这打破了类加载器提供的名称空间分离,并导致类型安全问题。

从JDK/JRE 1.2开始,每个类加载器都管理自己的一组本地库。无法将相同的JNI本地库加载到多个类加载器中。这样做会导致抛出UnsatisfiedLinkError。例如,System.loadLibrary在用于将本地库加载到两个类加载器时抛出UnsatisfiedLinkError。 新方法的好处是:

  • 基于类加载器的名称空间分隔保留在本地库中。本地库无法轻松混合来自不同类加载器的类。
  • 此外,当对应的类加载器被垃圾回收时,可以卸载本地库。

为了便于版本控制和资源管理,从JDK/JRE 1.2开始的JNI库可选择导出以下两个函数:

2.1 JNI_OnLoad

jint JNI_OnLoad(JavaVM *vm, void *reserved);

当加载本地库时,VM调用JNI_OnLoad(例如,通过System.loadLibrary)。JNI_OnLoad必须返回本地库所需的JNI版本。

为了使用任何新的JNI函数,本地库必须导出返回JNI_VERSION_1_2的JNI_OnLoad函数。如果本地库不导出JNI_OnLoad函数,VM假设该库只需要JNI版本JNI_VERSION_1_1。如果VM不识别JNI_OnLoad返回的版本号,则无法加载本地库。

LINKAGE:

从包含本地方法实现的本地库导出。

SINCE:

JDK/JRE 1.4

为了使用J2SE版本1.2中引入的JNI函数,除了JDK/JRE 1.1中提供的那些函数外,本地库必须导出一个JNI_OnLoad函数,该函数返回JNI_VERSION_1_2。

为了使用J2SE发行版1.4中引入的JNI函数,除了发行版1.2中提供的函数外,本地库还必须导出返回JNI_VERSION_1_4的JNI_OnLoad函数。

如果本地库不导出JNI_OnLoad函数,VM假设该库只需要JNI版本JNI_VERSION_1_1。如果VM不识别JNI_OnLoad返回的版本号,则无法加载本地库。

2.2 JNI_OnUnload

void JNI_OnUnload(JavaVM *vm, void *reserved);

当包含本地库的类加载器被垃圾回收时,VM调用JNI_OnUnload。此功能可用于执行清理操作。因为在未知的上下文中调用此函数(例如来自终结器),程序员应该保守使用Java VM服务,并避免任意Java回调。

请注意,JNI_OnLoad和JNI_OnUnload是JNI库可选提供的两个函数,不是从VM导出的。

LINKAGE:

从包含本地方法实现的本地库导出。

三、调用API函数

JavaVM类型是指向Invocation API函数表的指针。 以下代码示例显示了此函数表。

typedef const struct JNIInvokeInterface *JavaVM;


const struct JNIInvokeInterface ... = {
    NULL,
    NULL,
    NULL,

    DestroyJavaVM,
    AttachCurrentThread,
    DetachCurrentThread,

    GetEnv,

    AttachCurrentThreadAsDaemon
};

注意,三个调用API函数JNI_GetDefaultJavaVMInitArgs()、JNI_GetCreatedJavaVMs()和JNI_CreateJavaVM()不是JavaVM函数表的一部分。可以在不使用现有JavaVM结构的情况下使用这些函数。

3.1 JNI_GetDefaultJavaVMInitArgs

jint JNI_GetDefaultJavaVMInitArgs(void *vm_args);

返回Java VM的默认配置。在调用此函数之前,本地代码必须将vm_args->version字段设置为它期望VM支持的JNI版本。此函数返回后,vm_args->version将设置为VM支持的实际JNI版本。

LINKAGE:

从实现Java虚拟机的本地库导出。

PARAMETERS:

vm_args: 一个指向JavaVMInitArgs结构的指针,其中填充了默认参数。

RETURNS:

如果支持所请求的版本,则返回JNI_OK; 如果不支持请求的版本,则返回JNI错误代码(负数)。

3.2 JNI_GetCreatedJavaVMs

jint JNI_GetCreatedJavaVMs(JavaVM **vmBuf, jsize bufLen, jsize *nVMs);

返回已创建的所有Java VM。指向VM的指针按创建顺序写入缓冲区vmBuf。最多将写入bufLen条目数。在*nVM中返回创建的VM的总数。

从JDK/JRE 1.2开始,不支持在单个进程中创建多个VM。

LINKAGE:

从实现Java虚拟机的本地库导出。

PARAMETERS:

vmBuf:指向将放置VM结构的缓冲区的指针。

bufLen:缓冲区的长度。

nVMs:指向整数的指针。

RETURNS:

成功时返回JNI_OK; 失败时返回合适的JNI错误代码(负数)。

3.3 JNI_CreateJavaVM

jint JNI_CreateJavaVM(JavaVM **p_vm, void **p_env, void *vm_args);

加载并初始化Java VM。当前线程成为主线程。将env参数设置为主线程的JNI接口指针。

从JDK/JRE 1.2开始,不支持在单个进程中创建多个VM。

JNI_CreateJavaVM的第二个参数始终是指向JNIEnv*的指针,而第三个参数是指向JavaVMInitArgs结构的指针,该结构使用选项字符串来编码任意VM启动选项:

typedef struct JavaVMInitArgs {
    jint version;

    jint nOptions;
    JavaVMOption *options;
    jboolean ignoreUnrecognized;
} JavaVMInitArgs;

version字段必须至少设置为JNI_VERSION_1_2。 options字段是以下类型的数组:

typedef struct JavaVMOption {
    char *optionString;  /* 该选项作为默认平台编码中的字符串 */
    void *extraInfo;
} JavaVMOption;

数组的大小由JavaVMInitArgs中的nOptions字段表示。如果ignoreUnrecognized是JNI_TRUE,则JNI_CreateJavaVM将忽略以“-X”或“_”开头的所有无法识别的选项字符串。如果ignoreUnrecognized是JNI_FALSE,则JNI_CreateJavaVM会在遇到任何无法识别的选项字符串时立即返回JNI_ERR。 所有Java VM都必须识别以下一组标准选项:

选项字符串含义
-D=设置系统属性
-verbose[:class|gc|jni]启用详细输出。选项后面可以跟一个逗号分隔的名称列表,表明VM将打印哪种消息。 例如,“-verbose:gc,class”指示VM打印GC和类加载相关消息。标准名称包括:gc,class和jni。 所有非标准(特定于VM)名称必须以“X”开头。
vfprintfextraInfo是指向vfprintf回调的指针。
exitextraInfo是指向exit回调的指针。
abortextraInfo是指向abort回调的指针。

此外,每个VM实现可以支持其自己的一组非标准选项字符串。非标准选项名称必须以“-X”或下划线(“_”)开头。例如,JDK/JRE支持-Xms和-Xmx选项,以允许程序员指定初始和最大堆大小。以“-X”开头的选项可从“java”命令行访问。

以下是在JDK/JRE中创建Java VM的示例代码:

JavaVMInitArgs vm_args;
JavaVMOption options[4];

options[0].optionString = "-Djava.compiler=NONE";           /* 禁用JIT */
options[1].optionString = "-Djava.class.path=c:\myclasses"; /* 用户类 */
options[2].optionString = "-Djava.library.path=c:\mylibs";  /* 设置本地库路径 */
options[3].optionString = "-verbose:jni";                   /* 打印与JNI相关的消息 */

vm_args.version = JNI_VERSION_1_2;
vm_args.options = options;
vm_args.nOptions = 4;
vm_args.ignoreUnrecognized = TRUE;

/*
 * 请注意,在JDK/JRE中,不再需要调用JNI_GetDefaultJavaVMInitArgs。
 */
res = JNI_CreateJavaVM(&vm, (void **)&env, &vm_args);
if (res < 0) ...

LINKAGE:

从实现Java虚拟机的本地库导出。

PARAMETERS:

p_vm:指向生成的VM结构所在位置的指针。

p_env:指向将放置主线程的JNI接口指针的位置的指针。

vm_args:Java VM初始化参数。

RETURNS:

成功时返回JNI_OK; 失败时返回合适的JNI错误代码(负数)。

3.4 DestroyJavaVM

卸载Java VM并回收其资源。

在JDK/JRE 1.1中,对DestroyJavaVM的支持尚未完成。 从JDK/JRE 1.1开始,只有主线程可以调用DestroyJavaVM。从JDK/JRE 1.2开始,任何线程(无论是否连接)都可以调用此函数。如果连接了当前线程,则VM将等待,直到当前线程是唯一的非守护程序用户级Java线程。如果未连接当前线程,则VM会连接当前线程,然后等待,直到当前线程是唯一的非守护程序用户级线程。 但是,JDK/JRE仍然不支持VM卸载。

LINKAGE:

JavaVM接口函数表中的索引3。

PARAMETERS:

vm:将被销毁的Java VM。

RETURNS:

成功时返回JNI_OK; 失败时返回合适的JNI错误代码(负数)。

从JDK/JRE 1.1.2开始,不支持卸载VM。

3.5 AttachCurrentThread

jint AttachCurrentThread(JavaVM *vm, void **p_env, void *thr_args);

将当前线程attach到Java VM。 返回JNIEnv参数中的JNI接口指针。

试图attach已经attach的线程是一个无操作。

本地线程无法同时连接到两个Java VM。

当线程attach到VM时,上下文类加载器是引导加载程序。

LINKAGE:

JavaVM接口函数表中的索引4。

PARAMETERS:

vm:当前线程将attach到的VM。

p_env:指向当前线程的JNI接口指针所在位置的指针。

thr_args:可以是NULL或指向JavaVMAttachArgs结构的指针,以指定其他信息:

从JDK/JRE 1.1开始,AttachCurrentThread的第二个参数总是一个指向JNIEnv的指针。AttachCurrentThread的第三个参数是保留的,应该设置为NULL。

从JDK/JRE 1.2开始,将NULL作为第三个参数传递和1.1保持一致,或者传递一个指向以下结构的指针来指定其他信息:

typedef struct JavaVMAttachArgs {
    jint version;  /* 必须至少为JNI_VERSION_1_2 */
    char *name;    /* 线程的名称使用修改后的UTF-8字符串,或NULL */
    jobject group; /* ThreadGroup对象的全局引用,或NULL */
} JavaVMAttachArgs

RETURNS:

成功时返回JNI_OK; 失败时返回合适的JNI错误代码(负数)。

3.6 AttachCurrentThreadAsDaemon

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

与AttachCurrentThread相同的语义,但新创建的java.lang.Thread实例是一个守护进程。

如果线程已通过AttachCurrentThread或AttachCurrentThreadAsDaemon连接,则此例程只是将penv指向的值设置为当前线程的JNIEnv。 在这种情况下,AttachCurrentThread和此例程都不会对线程的守护程序状态产生任何影响。

LINKAGE:

JavaVM接口函数表中的索引7。

PARAMETERS:

vm:当前线程将attach到的虚拟机实例。

penv:指向当前线程的JNIEnv接口指针所在位置的指针。

args:指向JavaVMAttachArgs结构的指针。

RETURNS:

成功时返回JNI_OK; 失败时返回合适的JNI错误代码(负数)。

EXCEPTIONS:

SINCE:

JDK/JRE 1.4

3.7 DetachCurrentThread

jint DetachCurrentThread(JavaVM *vm);

从Java VM分离当前线程。该线程持有的所有Java监视器都将被释放。等待此线程死亡的所有Java线程都会收到通知。

从JDK/JRE 1.2开始,主线程可以从VM分离。

LINKAGE:

JavaVM接口函数表中的索引5。

PARAMETERS:

vm: 当前线程将从其中分离的vm。

RETURNS:

成功时返回JNI_OK; 失败时返回合适的JNI错误代码(负数)。

3.8 GetEnv

jint GetEnv(JavaVM *vm, void **env, jint version);

LINKAGE:

JavaVM接口函数表中的索引6。

PARAMETERS:

vm:将从中检索接口的虚拟机实例。

env:指向当前线程的JNI接口指针所在位置的指针。

version:请求的JNI版本。

RETURNS:

如果当前线程未attach到VM,则将*env设置为NULL,并返回JNI_EDETACHED。如果不支持指定的版本,请将*env设置为NULL,并返回JNI_EVERSION。 否则,将*env设置为适当的接口,并返回JNI_OK。

SINCE:

JDK/JRE 1.2