Java本地接口(JNI)编程指南和规范(第十章)

转载:http://blog.sina.com.cn/s/blog_53988c0c0100osox.html

第十章 陷阱和缺陷(Traps and Pitfalls)
为了突出在前面章节涉及的重要技术,本章涉及JNI程序员通常所犯的大量错误。这儿描述的每个错误都已在真实世界的工程中发生。

 

10.1 错误检查(Error Checking)
当写本地方法时,最通常的错误是忘记检查是否一个错误情况已经发生。不象"Java"编程语言,
本地语言不会提供标准的异常机制。"JNI"不能依赖任何特殊的本地异常机制(例如"C++"异常)。因此,在每个可能产生一个异常的"JNI"函数调用后,编程者被要求执行清楚地检查。并非所有的"JNI"函数都引发异常,但大多数可能。异常检查是繁琐(tedious),但必须确保使用本地方法的应用程序健壮(robust)。

 

错误检查的繁琐,更强调需要限制本地代码到那些需要使用"JNI"的应用程序的良好定义的子集中。

 

10.2 传送无效参数给"JNI"函数(Passing Invalid Arguments to JNI Functions)
"JNI"函数不会尝试检查和恢复无效参数。如果你传递"NULL"或(jobject)"oxFFFFFFFF"给一个希望得到一个引用的"JNI"函数,结果行为是不可预期的(be undefined)。实际上,这可能导致不正确的结果或虚拟器的奔溃。"Java 2 SDK release 1.2"
提供你一个命令行选项"-Xcheck:jni"。这选项命令虚拟器来侦测和报告许多传递无效逻辑参数给"JNI"函数的本地代码的情况,但不是全部对于参数有效性的检查招致大量的开销,因此默认是不开启的

 

在"C and C++"库中,不检查参数的有效性是通常实际情况。使用库的代码负责确保所有传递给库函数的参数是有效的。然而,如果你习惯"Java"编程语言,你可能必须在"JNI"编程中,调整以适合安全缺失的这特定方面。

 

10.3 混淆"jclass"和"jobject"(Confusing jclass withc jobject)
当第一次使用"JNI",实例的引用("jobject"类型的值)和类引用("jclass"类型的值)之间不同可能是混淆的。

实例的引用对应"java.lang.Object"或它的子类的一种的实例和数组。类引用对应"java.lang.Class"实例,代表类的类型(class types)。

 

一个操作例如"GetFieldID"是一个类操作,它得到的是一个"jclass",因为它得到来自一个类的成员域的描述符。相反,"GetIntField"是一个实例操作,得到一个"jobject",因为它得到来自一个实例的一个成员域的值。"jobject"和实例操作相关和"jclass"和类型操作相关是一致贯彻整个"JNI"函数的,因此很容易记住类型操作和实例操作的区别(be distinct from)。

 

10.4 截短"jboolean"参数(Truncating jboolean Argumnets)
一个"jboolean"是一个"8-bit"无符号"C"类型,它能存储0到255的值。0值对应常数"JNI_FALSE",1到255的值对应"JNI_TRUE"。但"32-bit or 16-bit"大于255的值,它的低"8 bits"是0时,造成一个问题。

 

假设你已经定义一个函数"print",有一个为"jboolean"类型的参数"condition":
void print(jboolean condition)
{
 
 if ( condition ){
  printf("true\n") ;
 }else {
  printf("false\n") ;
 }
}

 

前面的定义没有任何错误。然而,下面看是无害的"print"调用将产生一个稍微的不期望的结果:
int n = 256 ;
print(n) ;

 

我们传递一个非零值(256)到"print",期望它代表"true"。但因为超过低8位的所有"bits"被截去,参数是0。程序打印"false",不是期望的(contrary to expectations)。

 

当强制(coerce)整数类型(integral types),例如int,为"jboolean"类型时,"thumb"的一个好规则是总是在整数类型上评估条件,因此(thereby)避免不留意的错误(inadvertent errors)在强制期间。你能重写"print"的调用如下(as follow):
n = 256 ;
print(n? JNI_TRUE: JNI_FALSE) ;

 

10.5 "Java"应用和本地代码之间的边界线(Boundaries between Java Application and Native Code)
当设计一个被本地代码支持的"Java"应用程序时,一个通常的问题是"在本地代码中是什么和怎样做"。本地代码和用"Java"编程语言写的应用程序的剩余部分之间的边界是应用的特殊部分,但这有些一般地适应的规则(applicable principles):
.保持边界简单(simple)。在"Java"虚拟器和本地代码间的来回的复杂控制流可能调试和维护困难。如此控制流也是阻碍了通过高性能虚拟器实现来实现优化。例如,对于虚拟器的实现,在"Java"编程语言中定义内联方法比在"C and C++"中定义内联的本地方法更容易。
.在本地代码方保持代码尽力少。这儿有如此做的强制原因。本地代码不能移植和类型不安全。在本地代码中错误检查是麻烦的(10.1部分)。好的软件工程保持这样的部分为最小。
.保持本地带啊摹独立。实际上,这可能意味着所有本地方法是在一样的包里或一样的类中,独立于应用程序其他部分。包含必须的本地方法的这个包或类对于应用程序编程端口层。

 

"JNI"提供访问虚拟器的功能例如类的载入,对象创建,成员域访问,方法调用,线程同步,等等。当事实上用"Java"编程语言更简单的完成同样的任务时,有时在本地代码中,尝试用"Java"虚拟器的功能表达复杂的交互。下面的例子显示使用本地代码的"Java"编程为什么是不好做法。认识一个简单声明,它用"Java"程序语言创建一个新的线程:
new JobThread().start();

 

同样的声明也能使用本地代码来写:

aThreadObject = (*env)->NewObject(env, Class_JobThread, MID_Thread_init) ;
if(aThreadObject == NULL){
 ...
}
(*env)->CallVoidMethod(env, aThreadObject, MID_Thread_start) ;
if( (*env)->ExceptionOccurred(env) ){
 ...
}


尽管实际上我们忽略了为错误检查需要的代码行,但本地代码比用"Java"编程语言写的同样实现复杂还是太多了。

 

通常可取的是用"Java"编程语言来定义有一个辅助的方法,同时用本地代码调用这个辅助方法的一个回调,而不是写个本地代码操作"Java"虚拟器的一个复杂片段。

 

10.6 混淆IDs和引用(Confusing IDs with References)
"JNI"解释
对象为引用类型类,字符串和数组是引用的特别类型。"JNI"揭示方法和成员域为IDs一个ID不是一个引用。不能认为一个类的引用是一个"Class ID",或者一个方法ID是一个"方法引用"。

 

引用是被本地代码明确管理的虚拟机资源例如,"JNI"函数"DeleteLocalRef",允许本地代码来删除一个局部引用。相反,成员域和方法"IDs"被虚拟机管理,保持有效直到他们定义的类被载出在虚拟机载出定义的类前,本地代码不能明确地删除一个成员域或方法ID。

 

本地代码可以创建多个引用,指向同一个对象。例如,一个全局和一个局部引用,可以查阅同一个对象。相反,一个唯一成员域或方法ID被导出为一个成员域或一个方法的定义如果"class A"定义方法"f",同时"class B"从"A"继承"f",在下面代码中,两个"GetMethodID"调用总是返回一样的结果:
jmethodID MID_A_f = (*env)->GetMethodID(env, A, "f", "()V") ;
jmethodID MID_B_f = (*env)->GetMethodID(env, B, "f", "()V") ;

 

10.7 缓冲成员域和方法IDs(Caching Field and Method IDs)
本地代码通过指定的成员域或方法的名字和类型描述符作为字符串,从虚拟器得到成员域或方法"IDs"(4.1部分,4.2部分)。成员域和方法查看使用的名字和类型字符串是很慢的。通常为解决这问题(pay off)来缓冲"IDs"。在本地代码中,缓冲成员域和方法ID的失败是一种常见的性能问题。

 

在一些例子中,缓冲"IDs"比一个性能提高会更多。缓冲一个"ID"可能必须确保正确的成员域或方法被本地代码访问。下面例子说明缓冲一个成员域ID的失败怎样导致一个狡猾的问题:
class C {
 private int i ;
 native void f() ;
}

 

假设本地方法"f"需要获得在"C"类实例中的成员域"i"的值。没有缓冲一个ID的一个直接的实现,用三步来完成这个:1)得到对象的类型;2)为来自类型的引用的"i",查到成员域的ID;3)访问俄成员域的值,基于对象的引用和成员域"ID":
// No field IDs cached
JNIEXPORT void JNICALL
Java_C_f(JNIEnv *env, jobject this){
 jclass cls = (*env)->GetObjectClass(env, this) ;
 ...
 jfieldID fid = (*env)->GetFieldID (env, cls, "i", "I") ;
 ...
 ival = (*env)->GetIntField(env, this, fid) ;
 ...
}

 

这个代码运行的很好,直到我们定义另一个类"D"继承"C",同时声明了一个私有成员域也是"i"名字在"D"类中:
// Trouble in the absence of ID caching
class D extends C{
 private int i ;
 D(){
  f() ; // 继承 from C
 }
}

 

当"D"的构造函数调用"C.f",本地方法受到"D"的实例作为"this"参数, "cls"指的是"D class",同时"fid"代表"D.i"。在本地方法的最后,"ival"包含了"D.i"的值,替代了"C.i"。但实现本地方法"C.f"时候,这可能不是你期望的。

 

解决方法是计算和缓冲成员域"ID",当你确定你有个"C"的类引用,而不是D的时。来自缓冲ID的后续访问也将指向正确的成员域"C.i"。这儿正确的版本:
// Version that caches IDs in static initializers
class C {
 private int i ;
 native void f() ;
 private static native void initIDs() ;
 static{
  initIDs() ;// Call an initializing navtive method
 }
}

 

修改本地代码是(The modified native code is):
static jfieldID FID_C_i ;
JNIEXPORT void JNICALL
Java_C_initIDs(JNIEnv *env, jclass cls){
 
 FID_C_i = (*env)->GetFieldID(env, cls, "i", "I") ;
}

JNIEXPORT void JNICALL
Java_C_f(JNIEnv *env, jobject this){
 ival = (*env)->GetIntField(env, this, FID_C_i) ;
 ...
}

 

成员域ID是在"C"的静态初始化函数中被计算和缓冲的。这保证"C.i"的成员域ID将被缓冲,因此本地方法实现"Java_C_f"将取得"C.i"的值,独立于"this"对象的实际类型。

 

缓冲对于一些方法调用也是需要的。如果我们稍微地改变上面的例子,使每个类型"C"和"D"都有自己定义的私有方法"g","f"需要缓冲"C.g"的方法"ID"来避免意外(accidentally)调用"D.g"方法。为调用正确的虚拟方法,缓冲是不需要的。被定义的虚拟方法,动态绑定到方法调用的实例上。因此你能安全地使用"JNU_CallMehtodByName"工具函数(6.2.3部分)来调用虚拟方法。然而,前面例子告诉我们,为什么我们不定义一个类似的"JNU_GetFieldByName"工具函数。

 

10.8 Unicode字符串的结束(Terminating Unicode Strings)
从"GetStringChars or GetStringCritical"得到Unicode字符串是没有"NULL"结束的(NULL-terminated)。调用"GetStringLength"来发现"16-bit Unicode"字符个数在字符串中。一些系统操作,例如Windows NT, 期望两个拖尾的为0的byte值(two trailing 0 byte value)来结束"Unicode"字符串。你不能传递"GetStringChars"的结果到期望一个"Unicode"字符串的"Windows NT API"。你必须做字符串的另外复制和插入两个拖尾的为0的byte值。

 

10.9 违反访问控制规则(Violating Access Control Rules)
"JNI"不能强制"class, field, and method"访问控制限制,限制就是在"Java"编程语言层通过修饰符的使用例如"private"和"final"来表示的。写本地代码访问或修改一个对象的成员域是可能的,即使在"Java"编程语言层如此做将导致一个"IllagalAccessException"。"JNI"的放任是有意的设计决定,给了本地代码访问和修改任何在堆上的内存地址。

 

迂回过源代码语言层访问检查的本地代码,可以在程序执行上有不受欢迎的影响。例如,一个矛盾被创建,如果在运行时编译的编译器内联访问这成员域后一个本地方法修改了一个"final"成员域。类似地,本地方法应该不能修改不可变的对象例如在"java.lang.String or java.lang.Integer"的实例中的成员域。如此做(就是修改了)可以导致在"Java"平台实现中,不变量的破损。

 

10.10 漠视国际化(Disregarding Internationalization)
在"Java"虚拟器中字符串包含"Unicode"字符,但是本地字符串是典型的一个本地特定的编码。使用工具函数例如"JNU_NewStringNative"(8.2.1部分)和"JNU_GetStringNativeChars"(8.2.2部分)来在"Unicode jstrings"和底层主机环境的本地指定的本地字符串之间转化。特别注意消息字符串和文件名字,他们是典型地国际化的。如果一个本地方法得到一个文件名字作为一个"jstring",在文件名传递给一个"C"库函数前,文件名字必须转化为本地字符串。

 

下面本地方法,"MyFile.open",打开一个文件和返回一个文件描述符为它的结果:
JNIEXPORT jint JNICALL
Java_MyFile_open(JNIEnv *env, jobject seld, jstring name, jint mode)
{
 jint result;
 char *cname = JNU_GetStringNativeChars(env, name) ;
 if( cname == NULL ){
  return 0 ;
 }
 result = open(cname, mode) ;
 free(cname) ;
 return result ;
}

 

我们使用"JNU_GetSringNativeChars"函数来转化"jstring"参数,因为"open"系统调用希望文件名字是本地指定编码的。

 

10.11 保留虚拟器资源(Retaining Virtual Machine Resources)
在本地方法中一个通常的错误是忘记释放虚拟器的资源。程序员在错误发生时执行的代码过程(in code paths)中,需要特别地(particularly)细心。下面代码片段,在6.2.2部分的一个例子的稍微地修改,错失一个"ReleaseSringChars"调用:
JNIEXPORT void JNICALL
Java_pkg_Cls_f(JNIEnv *env, jclass cls, jstring jstr)
{
 const jchar *cstr =
  (*env)->GetStringChars(env, jstr, NULL) ;
 if( cstr == NULL){
  return ;
 }
 ...
 if( ...){ 
  
  return ;
 }
 ...
 
 (*env)->ReleaseStringChars(env, jstr, cstr) ;
}

 

忘记调用"ReleaseStringChars"函数可能引起"jstirng"对象被无限期地保留,导致内存碎片,或者"C"语言的副本被无限期地保留,一个内存泄漏。

 

无论"GetStringChars"得到一个字符串的副本有没有,这儿必须有个对应的"ReleaseStringChars"调用。下面代码没有正确的释放虚拟器的资源(fail to release virtual machine resources properly):

JNIEXPORT void JNICALLL
Java_pkg_Cls_f(JNIEnv *env, jclass cls, jstring jstr)
{
 jboolean isCopy ;
 const jchar *cstr = (*env)->GetStringChars(env, jstr, &isCopy) ;
 
 if( cstr == NULL ){
  return ;
 }
 ...
 
 if( isCopy){
  (*env)->ReleaseStringChars(env, jstr, cstr) ;
 }
}

 

当"isCopy"是"JNI_FALSE"时,"ReleaseStringChars"的调用也是需要的,以便虚拟器释放"jstring elements"。

 

10.12 过度的局部引用创建(Excessive Local Reference Creation)
过度局部引用的创建导致程序不必要的保留内存。一个不必要的局部引用浪费为被引用对象的内存和为引用自己的内存。

 

特别注意长时间运行(long-running)本地方法,在循环和工具函数中创建得分局部引用。在"Java 2 SDK release 1.2"中利用新的"Push/PopLocalFrame"函数来管理局部引用更有效率。参考5.2.1和5.2.2部分为这个问题的更多的细节的讨论。

 

在"Java 2 SDK release 1.2"中你能指定"-verbaose: jni"选项来请求虚拟器来侦测和报告过度局部引用创建。假设你带有这个选项运行一个类"Foo":
%java -verbose:jni Foo
同时输出包含下面:
***ALART: JNI local ref creation excedded capacity
     (creating: 17,, limit: 16).
 at Baz.g(Native method)
 at Bar.f(Compiled method)
 at Foo.main(Compiled method)


本地方法实现"Baz.g"不正确的管理局部引用是可能的。

 

10.13 使用无效的局部引用(Using Invalid Local Reference)
局部引用只在一个本地方法的单次调用(invocation)中是有效的。当实现方法的本地函数返回后,在一个本地方法调用中创建的局部引用自动被释放。本地代码不应该存储一个局部引用到一个全局变量中,同时期望在这个本地方法的以后调用中使用它。

 

局部引用只在创建它们的线程中是有效的。你不应该传递一个局部引用从一个线程到另一线程。当必须在线程之间传递一个引用时,要创建一个全局引用。

 

10.14 在线程间使用"JNIEnv"(Using the JNIEnv across Threads)
"JNIEnv"指针,作为第一个参数传递给每一个本地方法,只能在和它关联的线程中被使用。从一个线程得到缓冲的"JNIEnv"接口指针,同时在另一个线程中使用这个指针,是错误的。8.1.4部分解释你能怎样为当前线程得到"JNIEnv"接口指针。

 

10.15 不匹配的线程模式(Mismatched Thread Models)
"JNI"才运行,只有主机的本地代码和"Java"虚拟器实现共享一样线程的模式(8.1.5部分)。例如,程序员不能附加本地平台线程到一个使用一个用户线程包实现的嵌入式Java虚拟器。

 

在"Solaris"上,"Sun"附带一个基于一个名为"Green threads"的用户线程包的虚拟器实现。如果你的本地代码依赖于"Solaris"本地线程支持,它将不能和一个基于"Green thread"(Green-thread-based)的Java虚拟器实现一起工作。你需要一个虚拟器实现,它被设计来和"Solaris"本地线程一起工作的。在"Solaris"上支持本地线程的"JDK release 1.1"需要单独下载。本地线程支持是捆绑到"Solaris Java 2 SDK release 1.2"。

 

"Sun"的Win32的虚拟器实现默认是支持本地线程的,同时能容易嵌入到本地Win32应用程序中。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值