第七章:调用接口
这一章用于说明在你的本地代码中如何嵌入一个Java虚拟机。Java虚拟机实现通过作为一个本地库来传输,本地应用程序可以连接此库并使用调用接口来加载Java虚拟机。的确,在JDK或Java 2 SDK版本中的标准启动器指令只不过是一个和Java虚拟机链接的简单c程序。启动器解析命令行参数、加载虚拟机、并通过调用借口运行Java程序。
7.1 创建Java虚拟机
为了说明调用借口,让我们先看一个加载一个Java虚拟机并调用按照如下定义的Prog.main方法的C程序
1 2 3 4 5 |
public class Prog { public static void main(String[] args) { System.out.println("Hello World " + args[0]); } } |
接下来的C程序,invoke.c,加载一个Java虚拟机并调用Prog.main方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 |
#include <jni.h> #define PATH_SEPARATOR ';' /* define it to be ':' on Solaris */ #define USER_CLASSPATH "." /* where Prog.class is */ main() { JNIEnv *env; JavaVM *jvm; jint res; jclass cls; jmethodID mid; jstring jstr; jclass stringClass; jobjectArray args; #ifdef JNI_VERSION_1_2 JavaVMInitArgs vm_args; JavaVMOption options[1]; options[0].optionString = "-Djava.class.path=" USER_CLASSPATH; vm_args.version = 0x00010002; vm_args.options = options; vm_args.nOptions = 1; vm_args.ignoreUnrecognized = JNI_TRUE; /* Create the Java VM */ res = JNI_CreateJavaVM(&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_2 */ if (res < 0) { fprintf(stderr, "Can't create Java VM\n"); exit(1); } cls = (*env)->FindClass(env, "Prog"); if (cls == NULL) { goto destroy; } mid = (*env)->GetStaticMethodID(env, cls, "main", "([Ljava/lang/String;)V"); if (mid == NULL) { goto destroy; } jstr = (*env)->NewStringUTF(env, " from C!"); if (jstr == NULL) { goto destroy; } stringClass = (*env)->FindClass(env, "java/lang/String"); args = (*env)->NewObjectArray(env, 1, stringClass, jstr); if (args == NULL) { goto destroy; } (*env)->CallStaticVoidMethod(env, cls, mid, args); destroy: if ((*env)->ExceptionOccurred(env)) { (*env)->ExceptionDescribe(env); } (*jvm)->DestroyJavaVM(jvm); |
上面的代码条件性的编译特定于JDK 1.1版本的Java虚拟机实现的初始化结构体JDK1_1InitArgs。Java 2 SDK 1.2版本中仍旧可以支持JDK1_1InitArgs,尽管其引入了一种称为JavaVMInitArgs的初始化结构体。常量JAVA_VERSION_1_2在Java 2 SDK 1.2版本中定义,而在JDK 1.1版本中是没有定义的。
当它针对的是1.1版本时,C代码通过调用JNI_GetDefaultJavaVMInitArgs来获取默认的虚拟机设置。JNI_GetDefaultJavaVMInitArgs在vm_args参数中返回诸如堆大小、栈大小、默认类路径等值。然后我们追加Prog.class所在的目录到vm_args.classpath中。
当它是针对的是1.2版本时,C代码创建一个JavaVMInitArgs结构体。虚拟机初始化结构体保存在JavaVMOption数组当中,你可以设置和Java命令行选项对应的常规选项(如-Djava.class.path=.)和虚拟机实现特定的选项(如-Xmx64m)。设置ignoreUnrecognized字段为JNI_TRUE说明虚拟机忽略不能识别的虚拟机特定选项。
在设置好虚拟机初始化结构体后,C程序调用JNI_CreateJavaVM来加载和初始化Java虚拟机。JNI_CreateJavaVM将填充两个返回值:
- jvm,指向新创建的Java虚拟机的接口指针
- env,指向当前线程的JNIEnv的接口指针,本地方法将通过env接口指针来调用JNI方法
当JNI_CreateJavaVM成功返回时,当前本地线程就已经将自身引导到Java虚拟机中。在这个点上,它就像一个本地方法一样运行,因此,除了别的以外,它可以发出JNI调用来调用Prog.main方法。
最终程序会调用DestroyJavaVM来卸载Java虚拟机。(不幸的是,你不能在JDK 1.1版本和Java 2 SDK 1.2版本中卸载Java虚拟机实现,在这些版本中,DestroyJavaVM总是返回错误代码。)运行上面的程序,将得到:
Hello World from C!
7.2 将本地应用程序与Java虚拟机相连
调用接口需要你将你的程序例如invoke.c,和一个Java虚拟机实现相连。如何和Java虚拟机相连取决于本地引用程序是仅部署在特定的Java虚拟机实现还是它被设计为与来自不同供应商的各种虚拟机实现一起工作。
7.2.1 与已知的Java虚拟机链接
你可能决定了你的程序仅部署在特定的Java虚拟机实现上。在这种情况下,你可以将本地应用程序和实现了虚拟机的本地库相连,例如在Solaris系统,JDK 1.1版本中,你可以使用下面的指令来编译和链接invoke.c:
1 |
cc -I<jni.h dir> -L<libjava.so dir> -lthread -ljava invoke.c |
-lthread选项表明我们使用Java虚拟机以及本地线程支持(8.1.5节)。-ljava选项表明libjava.so是实现了Java虚拟机的Solaris共享库。
在Win32系统使用Microsoft Visual C++编译器,编译和链接同一个程序的命令行为
1 |
cl -I<jni.h dir> -MD invoke.c -link <javai.lib dir>\javai.lib |
当然你需要提供与机器上的JDK安装相对应的正确的包含和库目录。-MD选项确保你的本地应用程序和Win32多线程C库链接,在JDK 1.1版本和Java 2 JDK 1.2版本的Java虚拟机实现使用的是同一个本地C库。cl命令参考Win32中附带的JDK版本1.1中的javai.lib文件,以获取有关在虚拟机中实现的调用接口函数(JNI_CreateJavaVM)的链接信息。在运行时真实使用的JDK 1.1虚拟机实现被包含在一个名为javai.dll的单独动态链接库中。相反,在链接时和运行时都是用相同的Solaris动态库(.so文件)。
在Java 2 JDK 1.2版本中,虚拟机库的名字已更改为Solaris上的libjvm.so,以及Win32上的jvm.lib和jvm.dll。通常不同的供应商可能会以不同的方式命名虚拟机实现。
一旦编译和链接完成,你可以从命令行运行生成的可执行文件。你可能会收到系统找不到共享库或动态链接库的错误。在Solaris系统上,如果错误信息显示系统找不到动态库libjava.so(libjvm.so在Java 2 JDK 1.2版本上),则需要将包含虚拟机库的目录添加到LD_LIBRARY_PATH变量中。在Win32系统上,错误信息可能表示为它找不到动态链接库javai.dll(或者jvm.dll在Java 2 JDK 1.2版本上),如果是这种情,请将包含该DLL的目录添加到PATH环境变量中。
7.2.2 与未知的Java虚拟机链接
如果应用程序旨在使用来自不同供应商的虚拟机实现,则无法将本地应用程序链接到特定的虚拟机实现库。因为JNI没有指定实现Java虚拟机的本地库的名称,所以你应该准备好使用不同的Java虚拟机实现。例如,在Win32上,虚拟机在JDK版本1.1中作为javai.dll发布,在Java 2 SDK版本1.2中作为jvm.dll发布。
解决方案是使用运行时动态链接来加载应用程序所需的特定虚拟机库。虚拟机库的名称可以轻松的以应用程序特定的方式配置,例如以下Win32代码为虚拟机库的路径找到JNI_CreateJavaVM的函数入口点:
1 2 3 4 5 6 7 8 |
/* Win32 version */ void *JNU_FindCreateJavaVM(char *vmlibpath) { HINSTANCE hVM = LoadLibrary(vmlibpath); if (hVM == NULL) { return NULL; } return GetProcAddress(hVM, "JNI_CreateJavaVM"); } |
LoadLibrary和GetProcAddreee是在Win32系统上,用于动态链接的API函数。尽管LoadLibrary能够接受实现了Java虚拟机的本地库的名字(例如“jvm”)或者路径(例如”C:\jdk1.2\jre\bin\classic\jvm.dll”),传送本地库绝对路劲给JNU_FindCreateJavaVM将是最好的选择。依靠LoadLibrary来搜索jvm.dll,使您的应用程序易于进行配置更改,例如PATH环境变量的添加。
Solaris系统的版本是类似的:
1 2 3 4 5 6 7 8 |
/* Solaris version */ void *JNU_FindCreateJavaVM(char *vmlibpath) { void *libVM = dlopen(vmlibpath, RTLD_LAZY); if (libVM == NULL) { return NULL; } return dlsym(libVM, "JNI_CreateJavaVM"); } |
dlopen和dlsym函数支持在Solaris系统上动态链接共享库。
7.3 附加本地线程
假设你有一个多线程应用程序,例如一个用C写的Web服务器。随着HTTP请求的到来,服务器创建一些本地线程来同时处理HTTP请求。我们希望在此服务器中嵌入一个Java虚拟机,以便多个线程可以同时在Java虚拟机中执行操作,如图7.1所示:
服务器生成的本地方法的生命周期可能比Java虚拟机更短。因此,我们需要一种方式来将本地线程附加到已经运行的Java虚拟机,在附加的本地线程中执行JNI调用,然后将本地线程从Java虚拟机中脱离不会其他已连接的线程。接下来的示例,attach.c,说明如何使用调用接口将本地线程附加到虚拟机上,改程序是使用Win32线程API编写的。可以为Solaris和其他操作系统编写类似的版本:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 |
/* Note: This program only works on Win32 */ #include <windows.h> #include <jni.h> JavaVM *jvm; /* The virtual machine instance */ #define PATH_SEPARATOR ';' #define USER_CLASSPATH "." /* where Prog.class is */ 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; /* Pass NULL as the third argument */ #ifdef JNI_VERSION_1_2 res = (*jvm)->AttachCurrentThread(jvm, (void**)&env, NULL); #else res = (*jvm)->AttachCurrentThread(jvm, &env, NULL); #endif if (res < 0) { fprintf(stderr, "Attach failed\n"); return; } cls = (*env)->FindClass(env, "Prog"); if (cls == NULL) { goto detach; } mid = (*env)->GetStaticMethodID(env, cls, "main", "([Ljava/lang/String;)V"); if (mid == NULL) { goto detach; } sprintf(buf, " from Thread %d", threadNum); jstr = (*env)->NewStringUTF(env, buf); if (jstr == NULL) { goto detach; } stringClass = (*env)->FindClass(env, "java/lang/String"); args = (*env)->NewObjectArray(env, 1, stringClass, jstr); if (args == NULL) { goto detach; } (*env)->CallStaticVoidMethod(env, cls, mid, args); detach: if ((*env)->ExceptionOccurred(env)) { (*env)->ExceptionDescribe(env); } (*jvm)->DetachCurrentThread(jvm); } main() { JNIEnv *env; int i; jint res; #ifdef JNI_VERSION_1_2 JavaVMInitArgs vm_args; JavaVMOption options[1]; options[0].optionString = "-Djava.class.path=" USER_CLASSPATH; vm_args.version = 0x00010002; vm_args.options = options; vm_args.nOptions = 1; vm_args.ignoreUnrecognized = TRUE; /* Create the Java VM */ res = JNI_CreateJavaVM(&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_2 */ if (res < 0) { fprintf(stderr, "Can't create Java VM\n"); exit(1); } for (i = 0; i < 5; i++) /* We pass the thread number to every thread */ _beginthread(thread_fun, 0, (void *)i); Sleep(1000); /* wait for threads to start */ (*jvm)->DestroyJavaVM(jvm); } |
attach.c是invoke.c的变体,本地代码不是在主线程中调用Prog.main,而是启动五个线程。一旦它启动线程,它等待他们启动然后调用DestroyJavaVM。每个产生的线程都将自己链接到Java虚拟机,调用Prog.main方法,最后在其终止运行前将其从Java虚拟机中分离出来。DestroyJavaVM将在所有五个线程终止后返回。现在我们先忽略DestroyJavaVM的返回值,因为这个方法在JDK 1.1版本和Java 2 JDK 1.2版本上并没有完全实现。
JNI_AttachCurrentThread将NULL作为其第三个参数,Java 2 JDK 1.2版本中引入JNI_ThreadAttachArgs结构体,它允许你指定其他参数,例如你要附加的线程组。JNI_ThreadAttachArgs结构体的细节将在13.2节中作为JNI_AttachCurrentThread规范的一部分进行描述。
当程序执行DetachCurrentThread函数时,它将释放属于当前线程的所有本地引用。运行程序会产生以下输出:
1 2 3 4 5 |
Hello World from thread 1 Hello World from thread 0 Hello World from thread 4 Hello World from thread 2 Hello World from thread 3 |
输出的确切顺序可能会根据线程调度中的随机因素而变化。