原文链接:
第八章 JNI的附加功能(Additional JNI Features)
我们已经讨论了JNI被使用来写本地方法和嵌入一个Java虚拟器实现到一个本地应用程序中的功能。这章介绍JNI剩余的功能。
8.1 JNI和线程(JNI and Threads)
Java虚拟器支持控制并发的在一样地址空间中执行的多线程。这并发性引起一个复杂(complexity)的程度,这在一个单线程环境中是没有的。多线程可以访问同一个对象,同一个文件描述符--简而言之,一样的共享资源--同一个时间。
为最有效的理解(get the most out of)这部分,你应该了解多线程编程的概念。你应该知道怎样写使用多线程的Java应用程序和怎样对共享资源的同步访问。一个好的关于用Java编程语言编写多线程的参考书是"Concurrent Programming in Java, Design Principles and Patterns,by Doug Lea(Addsion-Wesley 1997)"
8.1.1 限制(Constraints)
当写本地方法为在都线程环境中运行时,我们必须在思想上记住某些局限。通过理解这些局限和使用这些局限编程,你的本地方法将安全执行,无论多少线程同时地(simultaneously)执行一个给定本地方法。例如:
.一个"JNIEnv"指针只在和其关联的线程中有效。你不必传递这个指针从一个线程到另一个线程,或者在多线程中缓冲和使用它。在从同一个线程的并发调用中,"Java"虚拟器传递给一个本地方法同样的"JNIEnv"指针,但当从不同线程中调用那个本地方法时,传递不同的"JNIEnv"指针。避免在一个线程缓冲"JNIEnv"指针和在另一线程中使用这个指针的一般性错误。
.局部引用只在创建它们的线程中是有效的。你不必传递局部引用从一个线程到另一个线程。你应该总是转换局部引用为全局引用,任何时候多线程可以使用一样的引用成为可能。
8.1.2 监视入口和出口(Monitor Entry and Exit)
监视是在Java平台上的基本同步机制(primitive sychronization mechanism)。每个对象能被动态地关联到一个监视上。"JNI"允许你使用监视来同步,因此在Java编程语言中,实现了等同于(equivalent to)一个同步块(synchronized block)的功能(functionality):
synchronized(obj){
}
Java虚拟器保证了在线程执行块中的任何语句前,线程获得和"obj"对象关联的监视。这确保在给定的任何时候这儿至多有一个线程拥有监视和在同步块中执行。当一个线程等待另一个线程退出监视时,这线程阻塞。
在JNI引用上,本地代码能使用"JNI"函数来实现一样的同步。你能使用"MonitorEnter"函数来进入监视和"MonitorExit"函数来退出监视:
if( (*env)->MonitorEnter(env,obj) != JNI_OK){
}
...
if( (*env)->MonitorExit(env, obj) != JNI_OK){
}
执行以上代码,在执行在同步块中的任何代码前,一个线程首先必须进入和"obj"关联的监视。"MonitorEnter"操作使用一个"jobject"来作为一个参数,同时如果另一线程已经进入了和这个"jobject"关联的监视,这个线程被阻塞。当当前线程没有拥有这个监视时,调用"MonitorExit"导致一个错误同时产生一个"IllegalMonitorStateExcep
"MonitorEnter"和"MonitorExit"能运行在"jclass, jstring, and jarray"类型上,这是"jobject"引用的特殊类型。
记得用恰当(appropriate)数目的"MonitorExit"调用来匹配一个"MonitorEnter",特别是在处理错误和异常的代码中:
if( (*env)->MonitorEnter(env, obj) != JNI_OK) ...;
...
if ( (*env)->ExceptionOccurred(env) ){
}
...
if ( (*env)->MonitorExit(env, obj) != JNI_OK) ... ;
调用"MonitorExit"的失败将几乎可能导致死锁。通过比较上面"C"代码片度和这部分开始处的代码片段,你能认识到用Java编程语言编程比用JNI更容易。因此,在Java编程语言中表示同步结构更可行(preferable)。例如,如果一个静态本地方法需要进入和它的定义类关联的监视,你应该定义一个静态变量来同步本地方法,而不是在本地方法中执行JNI层(JNI-level)同步监视(monitor sychronization)。
8.1.3 监视等待和通知(Monitor Wait and Notify)
"Java API"包含一些其他的方法,对于线程的同步是有用的。它们是"Object.wait, Object.notify and Object.notifyAll"。没有JNI函数被提供来直接对应这些方法,因为监视等带和通知操作没有和监视进入和退出操作一样的关键性能(performance cirtical)。本地代码可以用JNI方法调用机制来调用在Java API中对应的方法来替代:
static jmethodID MID_Object_wait ;
static jmehtodID MID_Object_notify ;
static jmethodID MID_Object_notifyAll ;
void
JNU_MonitorWait(JNIEnv *env, jobject object, jlong timeout)
{
}
void
JNU_MonitorNotify(JNIEnv *env, jobject object)
{
}
void
JNU_MonitorNotifyAll(JNIEnv *env, jobject object)
{
}
我们假设对"Object.wait, Object.notify, and Object.notify"的方法"IDs"已经被得到在其他地方同时缓冲到全局变量中。像在Java编程语言中一样,只在拥有和jobject参数关联的监视时,你能调用上面监视相关的函数。
8.1.4 在任意上下文中获取一个指向"JNIEnv"指针(Obtaining a JNIEnv Pointer in Arbitrary Contexts)
我们较早地解释一个"JNIEnv"指针只在它关联的线程中有效。一般地,这对于本地方法不是一个问题,因为他们从虚拟器得到"JNIEnv"指针作为第一个参数。然而,有时候(Occasionally),它可能对一小段本地代码时必须的,它不能从虚拟器被直接调用来得到属于当前线程的"JNIEnv"接口指针。例如,本地代码可以是一个被操作系统调用的"callback"函数,在这种情况中"JNIEnv"指针将可能不是作为一个参数的变量。
通过"AttachCurrentThread"函数的接口调用,你能为当前线程获得"JNIEnv"指针:
JavaVM *jvm
f()
{
}
当当前线程已经被附加到虚拟器上的时候,"AttachCurrentThread"返回属于当前线程的"JNIEnv"的接口指针。
这儿有许多方法来获得"JavaVM"指针:通过在创建虚拟器的时候记录它;通过使用"JNI_GetCreateJavaVMs"来查询被创建的虚拟器;通过在一个一般的本地方法中调用"JNI"函数"GetJavaVM";或者通过定义一个"JNI_OnLoad"处理。和"JNIEnv"指针不一样,在多线程中"JavaVM"指针保持有效,因此它能被缓冲在一个全局变量中。
"Java 2 SDK release 1.2"提供一个新的调用接口函数"GetEnv",所以你能检查当前线程是不是被附加到虚拟器上,同时如果如此,返回属于当前线程的"JNIEnv"指针。如果当期线程已经附加到虚拟器上,"GetEnv"和"AttachCurrentThread"有一样的功能。
8.1.5 匹配线程模型(Matching the Thread Models)
假设(suppose)运行在多线程中的本地代码访问一个全局资源。本地代码应该使用"JNI"函数"MonitorEner and MonitorExit",或者使用在主机环境中的本地线程同步原语(例如"mutex_lock"在Solaris系统上)?相似地,如果本地代码需要创建新的线程,它应该创建一个"java.lang.Thread"对象和通过"JNI"来执行"Thread.start"的回调,或者它应该在本机环境中使用本地线程创建原语(creation primitive)(例如"thr_create"在Solaris系统上)?
答案是如果"Java"虚拟器实现支持一种线程模型,且线程模型匹配了通过本地代码的虚拟器,那时所有这些方法(approach)都能工作。线程模型(thread model)指示(dictat)系统怎样实现必要的线程操作,例如时序安排(scheduling),上下文的切换(context switching),同步(sychronization),和在系统调用中的阻塞(blocking)。另一方面,在一个用户的线程模型中,应用程序代码实现了线程的操作。例如,在Solaris系统上被"JDK"和"Java 2 SDK releases"导出的"Green thread"模型使用"ASCI C"函数"setjmp"和"longjmp"来实现上下文的切换。
许多模型的操作系统(例如"Solaris"和"Win32")都支持本地线程模型。不幸地,一些操作系统任然缺少本地线程的支持。替代地,在这些操作系统上有一个或多个用户线程包。
如果你严格地用Java编程语言来写应用程序,你不需要担心虚拟器实现的底层线程模型。"Java"平台能被移植到任何支持线程原语请求设置的主机环境。大多本地和用户线程包提供必需的线程原语,为实现一个"Java"虚拟器。
另一方面,JNI编程者必须注意线程模型。如果"Java"虚拟器实现和本地代码有不同的线程和同步概念(a different notion of threading and sychronization),使用本地代码应用程序可能无法正常执行(function properly)。例如,一个本地方法能在它自己线程模型中的一个同步操作中被阻塞,但"Java"虚拟器运行在一个不同的线程模型中,可能不知道执行本地方法的线程被阻塞了。因为没有其他线程将本安排执行,所以应用程序锁死。
如果本地代码和"Java"虚拟器实现使用一样的线程模型,这线程模型匹配。如果"Java"虚拟器实现使用本地线程支持,本地代码能自由地调用在主机环境中的相关线程原语。如果"Java"虚拟器实现是基于一个用户线程包,本地代码因该链接到一样的用户线程包上或依赖于非线程操作。后者可能比你想象的更难实现:大多C库调用(例如,"I/O"和内存分配功能)在下面(underneath)执行了线程同步。除非本地代码执行纯粹的计算和不使用库的调用,它很可能间接地(indirectly)使用了线程原语。
大多虚拟器实现只支持一个特别的线程模式为"JNI"本地代码.支持本地线程的实现是最灵活的,因此当本地线程可用时,它是在被给定主机环境的首选。可能严格地限制,依赖于一个特别的用户线程包的虚拟器实现,做为它们能操作本地代码的类型。
一些虚拟器实现可以支持大量的不同的线程模型。一个更灵活(more flexible)类型的虚拟器实现可以允许你提供一个自定义的线程模式实现为虚拟器的内部使用,因而确保虚拟器实现能带了你的本地代码工作。在在可能请求本地代码的工程上着手处理(embark)前,你应该参考(consult)说明你的虚拟器实现的线程模式限制的文档。
8.2 写国际化的代码(Writing Internationalized Code)
必须特别关心写的代码能很好地工作在多个地区。"JNI"给程序员完整的访问Java平台的国际化的功能。我们将使用字符串转换(string conversion)作为一个例子,因为文件的名字和消息在许多地域中可以包含非ASCII字符。
"Java"虚拟器用"Unicode"格式来表示字符串(string)。虽然一些本地平台(例如Window NT)也提供"Unicode"的支持,但大多使用本地指定的编码来表示字符串。
不能使用"GetStringUTFChars"和"GetStringUTFRegion"函数来做在"jstring"和本地指定字符串之间的转换,除非在平台上本地的编码是"UTF-8"。"UTF-8"字符串是有用的,当表示名字和描述符(例如"GetMethodID"的参数)来被传递给"JNI"函数时,但表示本地指定编码的字符串例如文件名字是不恰当的。
8.2.1 从本地字符串创建"jstring"(Create jstring from Native Strings)
使用"string(byte[] bytes)"构造器(constructor)来转换一个本地字符串为一个"jstring"。下面工具函数创建一个"jstring"从一个本地指定的本地"C"字符串(a local-specific native C string):
jstring JNU_NewStringNative(JNIEnv *env, const char *str)
{
}
这个函数创建一个"byte"数组,复制本地"C"字符串到"byte array",和最终调用String(byte[] bytes)构造函数来创建"jstring object"的结果。"Class_java_lang_String"是对于"java.lang.String"类的一个全局引用,同时"MID_String_init"是字符构造器的方法"ID"。因为这是一个工具函数,我们确保删除"byte array"的局部引用,这局部引用被创建来临时储存字符串的。
如果你需要在"JDK release 1.1"中使用这个函数,删除"EnsureLocalCapacity"的调用。
8.2.2 转换"jstrings"到本地字符串(Translating1 jstrings to Native Strings)
使用"String.getBytes"方法来转换一个"jstring"为恰当的本地编码。下面工具函数转换一个"jstring"为一个区域指定(locale-specific)的本地"C"字符串:
char *JNU_GetStringNativeChars(JNIEnv *env, jstring jstr)
{
}
这个函数传递"java.lang.String"引用到"String.getBytes"方法,然后复制"byte array"的元素到一个最新分配的"C"数组。"MID_String_getBytes"是"String.getBytes"方法的预先计算的(precomputed)方法ID。因为这是个工具函数,我们确保删除"byte array"的局部引用和异常对象。记住(Keep in mind)删除一个异常对象的"JNI"引用不会清除未决的异常。
再次,如果你需要使用这个函数在JDK release 1.1中,删除"EnsureLocalCapacity"的调用。
8.3 注册本地方法(Registering Native Methods)
在一个应用程序执行一个本地方法前,它通过一个两步处理(a two-step process)来载入包含本地方法实现的本地库和然后(and then)链接到本地方法实现上:
1."System.loadLibrary"定位和载入命名的本地库。例如,"System.loadLibrary("foo")"可以在Win32平台载入"foo.dll"。
2.虚拟器在载入的一个本地库中定位本地方法实现本地方法实现。例如,一个"Foo.g"本地犯法调用请求定位和链接本地函数"Java_Foo_g",它驻留在"foo.dll"中。
这章将介绍另一个方法来完成第二步。替代依赖虚拟器来搜索本地方法在已经载入的本地库中,"JNI"编程者能手动地(manually)链接本地函数,通过注册带有一个类引用,方法名字和方法描述符的一个函数指针:
JNINativeMethod nm;
nm.name = "g";
nm.signature = "()V" ;
nm.fnPtr = g_impl ;
(*env)->RegisterNatives(env, cls, *nm, 1) ;
上面代码注册本地函数"g_impl"做为"Foo.g"本地方法的实现:
void JNICALL g_impl(JNIEnv *env, jobject self) ;
这个本地函数"g_impl"不需要按照"JNI"命名转换,因为只涉及函数指针,不需要从库中导出(因此这儿不需要用JNIEXPORT来声明函数)。然而,本地函数"g_impl"任然,跟在"JNICALL"调用的转换后。
"RegisterNatives"函数对于许多用途是很有用的:
.有时候积极地(eagerly)注册大量的本地方法的实现更方便和高效(more convenient and more effcient),相对于让虚拟器懒洋洋地(lazily)链接这些入口。
.你可以在一个方法中调用"RegisterNatives"多次,允许本地方法实现在运行时被更新。
.在一个本地应用程序嵌入一个虚拟器实现和需要链接一个定义在本地应用程序中的本地方法实现,"RegisterNatives"是非常(particularly)有用的。虚拟器应该不能自动地找到这个本地方法实现,因为它只能在本地库中搜索,不会在应用程序自身中搜索。
8.4 载入和载出处理程序(Load and Unload Handlers)
载入和载出处理程序允许本地库导出两个函数:一个在"System.loadLibrary"载入本地库时调用,另一个在虚拟器载出本地库时调用。这个特征在"Java 2 SDK release 1.2"中被加入。
8.4.1 JNI_OnLoad处理程序(The JNI_OnLoad Handler)
当"System.loadLibrary"载入一个本地库时,虚拟器在本地库中搜索下面导出入口:
JNIEXPORT jint JINCALL JNI_OnLoad(JavaVM *jvm, void *reserved) ;
在"JNI_OnLoad"的实现中,你能调用任何"JNI"函数。"JNI_OnLoad"处理程序的典型使用时缓冲"JavaVM"指针,类的引用,或者方法和域的"IDs",像在下面例子中显示的(as shown in the following example):
JavaVM *cached_jvm ;
jclass Class_C ;
jmethodID MID_C_g ;
JNIEXPORT jint JNICALL
JNI_OnLoad(JavaVM *jvm, void *reserved)
{
}
"JNI_OnLoad"函数首先缓冲"JavaVM"指针到全局变量"cached_jvm"中。然后通过调用"GetEnv",来获得"JNIEnv"指针。最后载入"C class", 缓冲这个类引用,和计算了"C.g"的方法"ID"。"JNI_OnLoad"函数在错误时返回"JNI_ERR"(12.4部分),否则放回被本地库需要的"JNIEnv"版本号"JNI_VERSION_1_2"。
我们将在下一部分中解释为什么我们缓冲"C class"在一个弱全局引用中来替代一个全局引用。
被给一个缓冲"JavaVM"接口指针,对于实现一个允许本地代码来得到当前线程的"JNIEnv"接口指针的工具是微不足道的。
JNIEnv *JNU_GetEnv()
{
}
8.4.2 JNI_OnUnload处理程序(The JNI_OnUnload Handler)
直觉地(intuitively),当虚拟器载出一个"JNI"本地库时,虚拟器调用"JNI_OnUnload"处理程序。然而(However),这不够精确(precise enough)。什么时候虚拟器决定它载出一个本地库?哪个线程运行"JNI_OnUnload"处理程序?
载出本地库的规则是如下(as follows):
.虚拟器关联每个本地库使用"class C"的类载入器"L"调用了"System.loadLibrary"函数。
.在它决定类载入器"L"不在是一个活的对象(a live object)后,虚拟器调用"JNI_OnUnload"处理,同时载出本地库。因为一个类载入器查看了这个虚拟器定义的所有的类,这暗示(imply)类C也能被载出。
."JNI_OnUnload"处理程序在最后运行,且被"java.lang.System.runFinalization"同步地调用或者被虚拟器同步地调用。
"JNI_OnUnload"处理程序的定义清除了在上一部分中"JNI_OnLoad"分配的资源:
JNIEXPORT void JNICALL
JNI_OnUnload(JavaVM *jvm, void *reserved)
{
}
"JNI_OnUnload"函数删除了"C class"在"JNI_OnLoad"处理程序中创建的弱全局引用。我们不需要删除方法ID "MID_C_g",因为在载出"class C"定义时,虚拟器自动地回收代表"C"的方法IDs的需要的资源。
我们现在准备解释为什么我们缓冲"C class"到一个弱全局应用替代一个全局应用。一个全局引用应该保持"C"活跃,转而它应该保持"C"的类载入器活跃(alive)。由于(Given that)本地库是关联在"C"的类载入器"L"上,本地库不应该被载出和"JNI_OnUnload"不应该被调用。
"JNI_OnUnload"处理程序在最后(in a finalizer)运行。相反(In contrast),"JNI_OnLoad"处理程序在发起(initiate)"System.loadLibrary"调用的线程中运行。因为"JNI_OnUnload"在一个未知的线程上下文中运行,为了避免可能死锁,在"JNI_OnUnload"中你应该避免复杂的同步和锁操作。"JNI_OnUnload"处理程序典型地运行简单的任务例如释放被本地库分配的资源。
在类载入器载入库和被这个类载入器定义的所有类不在活跃时,"JNI_OnUnload"处理程序运行。"JNI_OnUnload"处理程序不得以任何方式(in any way)使用这些类。在上面"JNI_OnUnload"定义中,你不得执行任何操作,操作假设"Class_C"任然指向一个有效的类。在这个例子中"DeleteWeakGlobalRef"调用释放弱全局引用自己,但不以任何方式操作引用的"class C"。
总之(In summary),当写"JNI_OnUnload"处理程序时,你应该小心。避免复杂的可能导致死锁的锁操作。记住当"JNI_OnUnload"处理程序被调用时,类已经被载出了。
8.5 反馈支持(Reflection Support)
一般地,反馈提到在运行时操作语言级的构造。例如,反馈允许你在运行(at run time)时发现任何类对象,系列域和在类中定义的方法的名字。在Java编程语言级通过"java.lang.reflect"包来提供反馈的支持,和在"java.lang.Object"和"java.lang.Class"类中的一些方法一样。虽然你总能调用对应的"Java API"来执行(carry out)反馈操作,"JNI"提供下面函数使来自本地代码的频繁的反馈操作更有效和方便。
."GetSuperclass"返回一个被给类引用的父类。
."IsAssignableFrom"检查一个类的实体是否能被用,当另一个类的事例期待使用时。
."GetObjectClass"返回被给"jobject"引用的类。
."IsInstanceOf"检查一个"jobject"对象是否是一个被给类的实体。
."FromReflectedField and ToReflectedField"允许本地代码在域"ID"和"java.lang.reflect.Field"对象之间转换。他们是在"Java 2 SDK release 1.2"中新增的。
."FromReflectedMethod and ToReflectedMethod"允许本地代码在方法"IDs","java.lang.reflect.Method objects"和"java.lang.reflect.Constructor objects"之间转换。他们是在"Java 2 SDK release 1.2"中新增的。
8.6 JNI在C++中的编程(JNI Programming in C++)
"JNI"对于"C++"编程者表示一个稍微(slightly)简单接口。"jni.h"文件包含一系列定义,所以C++编程者能写,例如:
jclass cls = env->FindClass("java/lang/String");
替代在"C"中:
jclass cls = (*env)->FindClass(env, "java/lang/String");
在"env"上额外级别的间接寻址,和"FindClass"的"env"参数的对编程者的隐藏。"C++"编译器内联"C++"成员函数调用来等同于C成员函数调用(同行(counterparts);结果的编码是一样的。在"C"或"C++"中使用"JNI"之间没有内在的(inherent)性能差别。
此外(In addition),"jni.h"文件也定义一些列空的"C++"类来强制在不同的"jobject"子类型中子类化联系:
// JNI reference type defined in C++
class _jobject{} ;
class _jclass: public _jobject{} ;
class _jstring: public _jobject{} ;
...
typedef _jobject * jobject ;
typedef _jclass* jclass ;
typedef _jstring* jstring ;
...
"C++"编译器能在编译时发现你是否传递,例如,一个"jobject"给"GetMethdodID":
// ERROR: pass jobject as a jclass :
jobject obj = env->NewObject(...) ;
jmethodID mid = env->GetMethodID(obj, "foo", "()V") ;
因为"GetMethodID"希望一个"jclass"引用,"C++"编译将给出一个错误消息。在"C"类型定义的JNI中,jclass是和jobject一样的:
typedef jobject jclass ;
因此,一个C编译器不能检查你错误地(mistakenly)传递一个"jobject"替代"jclass"。
在C++中加入类型的层次(type hirrarchy)有时额外的转换(casting)成必要(necessitate)。在"C"中,你能从一个字符串数组中得到一个字符串,赋结果到一个"jstring":
jstring jstr = (*env)->GetObjectArrayElement(env, arr, i) ;
然而, 在"C++"中你需要插入一个清晰的转换:
jstring jstr = (jstring)env->GetObjectArrayElement(arr, i) ;