七、JNI-调用接口

  • 概述
  • 库和版本管理
  • Invocation API
  • 示例

#1. 概述

Invocation API的作用在于如何把JVM嵌入到本地程序中。

下述代码展示了如何使用Invocation API,在C++程序中创建JVM然后执行Java程序。

#include        /* where everything is defined */
...
JavaVM *jvm;       /* denotes a Java VM */
JNIEnv *env;       /* pointer to native method interface */
JavaVMInitArgs vm_args; /* JDK/JRE 10 VM initialization arguments */
JavaVMOption* options = new JavaVMOption[1];
options[0].optionString = "-Djava.class.path=/usr/lib/java";
vm_args.version = JNI_VERSION_10;
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();
1.1 Creating the VM

JNI_CreateJavaVM函数负责加载和初始化Java VM,参数pvm返回JavaVM接口指针,参数penv返回JNI接口指针。

JNI_CreateJavaVM
_JNI_IMPORT_OR_EXPORT_ jint JNICALL
JNI_CreateJavaVM(JavaVM **pvm, void **penv, void *args);
1.2 Attaching to the VM

JNI接口指针只在当前线程有效。其他线程只有调用了AttachCurrentThread获取属于当前线程的JNI接口指针才能够访问Java VM。一旦attach成功,native线程就相当于是跑在本地方法中的Java线程。Native线程会一直处理attached状态除非调用DetachCurrentThread。

AttachCurrentThread
jint AttachCurrentThread(void **penv, void *args) {
    return functions->AttachCurrentThread(this, penv, args);
}
1.3 Detaching from the VM

处于Attached状态的本地线程在退出前必须要调用DetachCurrentThread。当前线程在其调用栈上的函数尚未执行完毕前不能进行detach操作。

DetachCurrentThread
jint DetachCurrentThread() {
    return functions->DetachCurrentThread(this);
}
1.4 Unloading the VM

JNI_DestroyJavaVM函数负责卸载创建好的Java VM。

DestroyJavaVM
jint DestroyJavaVM() {
    return functions->DestroyJavaVM(this);
}

#2. 库和版本管理

VM所能加载的Native库分为两类:动态链接库(.so或者.dll),静态链接库(.a或者.lib)。

Native库一旦加载成功,对所有的类加载器都是可见的。因此存在同时两个类加载器去加载同一个Native库的情况,这种case会带来两个问题:

  • 类可能会错误的链接上已经被其他类加载器所加载的库。
  • Native的方法中会混淆来自不同类加载器的类,这会打破由类加载器隔绝的命名中间限制,带来类型安全问题。

每一个类加载器负责管理自己加载的本地库,同一个Native库不能同时被两个类加载所加载。如果存在不同的类加载器加载同一个Native库会触发UnsatisfiedLinkError。

2.1 JNI_OnLoad

当动态链接库被Java VM加载后(例如,通过System.loadLibrary),VM回调JNI_OnLoad函数。JNI_OnLoad接受JNI_VERSION作为返回值以决定使用哪个版本的JNI_API。

譬如Native库中需要使用AttachCurrentThreadAsDaemon,那么JNI版本必须是1.4以上,即必须返回JNI_VERSION_1_4或以上版本。如果Native库中没有导出JNI_OnLoad函数,VM会选择默认的JNI函数版本JNI_VERSION_1_1。如果返回的JNI版本号是未定义的,VM会自动卸载掉所加载的Native库。

jint JNI_OnLoad(JavaVM *vm, void *reserved);
JNI Version
#define JNI_VERSION_1_1 0x00010001
#define JNI_VERSION_1_2 0x00010002
#define JNI_VERSION_1_4 0x00010004
#define JNI_VERSION_1_6 0x00010006
示例
JNIEXPORT jint JNI_OnLoad(JavaVM *vm, void *reserved) {
    JNIEnv *env;
    if (vm->GetEnv((void **) &env, JNI_VERSION_1_6) != JNI_OK) {
        return -1;
    }
#ifdef ANDROID_ENV_FIPS_MODE
    // Init FIPS mode.
    if (FIPS_mode() != 0) {
        LOGD("Already in FIPS mode\n");
    } else {
        int ret = FIPS_mode_set(1);
        if (ret == 0) {
            LOGE("FIPS_mode_set() returns 0\n");
            unsigned long err;
            while ((err = ERR_get_error()) != 0) {
                LOGE("\tmsg = %lu, %s\n", err, ERR_error_string(err, nullptr));
            }
        }
    }
#endif
    return JNI_VERSION_1_6;
}
2.2 JNI_OnUnload

Java VM会在加载动态链接库的class loader被虚拟机回收后回调JNI_OnUnload。

JNI_OnUnload函数可以进行一些清理工作。

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

静态链接库中必须要定义的函数。L为静态链接库的名字。JNI_OnLoad_L的作用和JNI_OnLoad是一样的,JNI_OnLoad_L是在JNI_VERSION_1_8以后支持的。

jint JNI_Onload_(JavaVM *vm, void *reserved);
2.4 JNI_OnUnload_L

Java VM会在加载静态链接库的class loader被虚拟机回收后回调JNI_OnUnload_L。

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

#3. Invocation API

JNI函数中提供了一系列的Invocation API,包括JNI_CreateJavaVM,
JNI_GetDefaultJavaVMInitArgs,JNI_GetCreatedJavaVMs等。

3.1 JavaVM

JavaVM是Invocation API函数指针。

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
};
3.2 JNI_GetDefaultJavaVMInitArgs

获取VM的默认配置。在调用该函数前,必须指定vm_args->version(告诉VM需要支持的JNI版本号)。

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

获取已经创建的JavaVM集合。

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

加载并且初始化Java VM。调用JNI_CreateJavaVM的线程变为主线程。p_env参数返回当前线程的JNI接口指针。vm_args参数用来设置VM的启动参数。

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

JavaVM启动参数。

typedef struct JavaVMInitArgs {
    jint version;

    jint nOptions;
    JavaVMOption *options;
    jboolean ignoreUnrecognized;
} JavaVMInitArgs;
JavaVMOption
typedef struct JavaVMOption {
    char *optionString;  /* the option as a string in the default platform encoding */
    void *extraInfo;
} JavaVMOption;
optionString meaning
-D= Set a system property
-verbose[:class|gc|jni] Enable verbose output. The options can be followed by a comma-separated list of names indicating what kind of messages will be printed by the VM. For example, "-verbose:gc,class" instructs the VM to print GC and class loading related messages. Standard names include: gc, class, and jni. All nonstandard (VM-specific) names must begin with "X".
vfprintf extraInfo is a pointer to the vfprintf hook.
exit extraInfo is a pointer to the exit hook.
abort extraInfo is a pointer to the abort hook.
示例
JavaVMInitArgs vm_args;
JavaVMOption options[4];

options[0].optionString = "-Djava.compiler=NONE";           /* disable JIT */
options[1].optionString = "-Djava.class.path=c:\myclasses"; /* user classes */
options[2].optionString = "-Djava.library.path=c:\mylibs";  /* set native library path */
options[3].optionString = "-verbose:jni";                   /* print JNI-related messages */

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

/* Note that in the JDK/JRE, there is no longer any need to call
 * JNI_GetDefaultJavaVMInitArgs.
 */
res = JNI_CreateJavaVM(&vm, (void **)&env, &vm_args);
if (res < 0) ...
3.5 DestroyJavaVM

卸载Java VM。

jint DestroyJavaVM(JavaVM *vm);

#4. 示例

以下代码展示了如何在Native程序中嵌入JVM虚拟机,C++程序调用Java程序的过程。

4.1 Prog.java
public class Prog {
    public static void main(String[] args) {
        System.out.println("Hello World " + args[0]);
    }
}
4.2 Invoke.cpp
#include 
#include 
#include 
#include 

#define PATH_SEPARATOR ';'
#define USER_CLASSPATH "."

JavaVM *jvm; /*The virtual machine instance*/

void *JNU_FindCreateJavaVM(char *vmlibpath)
{
    HMODULE hVM = LoadLibrary(vmlibpath);
    if (hVM == NULL) {
        return NULL;
    }
    return GetProcAddress(hVM, "JNI_CreateJavaVM");
}

void thread_fun(void *arg) {
    jint res;
    jclass cls;
    jmethodID mid;
    jstring jstr;
    jclass stringClass;
    jobjectArray args;
    JNIEnv *env;
    char buf[100];
    int threadNum = (int)arg;
    
#ifdef JNI_VERSION_1_6
    res = jvm->AttachCurrentThread((void **)&env, NULL);
#else
    res = jvm->AttachCurrentThread((void **)&env, NULL);
#endif // JNI_VERSION_1_6
    if (res < 0) {
        fprintf(stderr, "Attach failed\n");
        return;
    }

    cls = env->FindClass("Prog");
    if (cls == NULL) {
        goto detach;
    }
    mid = env->GetStaticMethodID(cls, "main", "([Ljava/lang/String;)V");
    if (mid == NULL) {
        goto detach;
    }
    sprintf_s(buf, sizeof(buf), "from Thread %d", threadNum);
    jstr = env->NewStringUTF(buf);
    if (jstr == NULL) {
        goto detach;
    }
    stringClass = env->FindClass("java/lang/String");
    args = env->NewObjectArray(1, stringClass, jstr);
    if (args == NULL) {
        goto detach;
    }
    env->CallStaticVoidMethod(cls, mid, args);
detach:
    if (env->ExceptionOccurred()) {
        env->ExceptionDescribe();
    }
    jvm->DetachCurrentThread();
}

void main() {
    JNIEnv *env;
    jint res;

    const char *msg = " from C!";
    const char *path = "C:\\Program Files\\Java\\jdk1.8.0_91\\jre\\bin\\server\\jvm.dll";
    char classPath[1024];
    char vmLibPath[1024];
    typedef jint(*JNI_CreateJavaVM_Fn)(JavaVM**, void**, void*);
#ifdef JNI_VERSION_1_6
    JavaVMInitArgs vm_args;
    JavaVMOption options[1];

    sprintf_s(classPath, sizeof(classPath), "%s%s", "-Djava.class.path=", USER_CLASSPATH);
    options[0].optionString = classPath;
    options[0].extraInfo = classPath;

    vm_args.version = JNI_VERSION_1_6;
    vm_args.options = options;
    vm_args.nOptions = 1;
    vm_args.ignoreUnrecognized = JNI_TRUE;
    
    /*Create the Java VM*/
    sprintf_s(vmLibPath, sizeof(vmLibPath), "%s", path);
    void *addr = JNU_FindCreateJavaVM(vmLibPath);
    if (addr == NULL) {
        return;
    }
    JNI_CreateJavaVM_Fn jni_create_vm = reinterpret_cast(addr);
    res = (*jni_create_vm)(&jvm, (void**)&env, &vm_args);
#else
    JDK1_1InitArgs vm_args;
    char classpath[1024];
    vm_args.version = 0x00010001;
    JNI_GetDefaultJavaVMInitArgs(&vm_args);
    /* Append USER_CLASSPATH to the default system class path */
    sprintf(classpath, "%s%c%s",
        vm_args.classpath, PATH_SEPARATOR, USER_CLASSPATH);
    vm_args.classpath = classpath;
    /* Create the Java VM */
    res = JNI_CreateJavaVM(&jvm, &env, &vm_args);
#endif /* JNI_VERSION_1_6 */
    if (res < 0) {
        fprintf(stderr, "Can't create Java VM.\n");
        exit(0);
    }
    
    for (int i = 0; i < 5; i++) {
        _beginthread(thread_fun, 0, (void *)i);
    }
    Sleep(1000);
    jvm->DestroyJavaVM();

    system("pause");
}
Invocation Result.jpg

你可能感兴趣的:(七、JNI-调用接口)