Java本地接口(JNI)编程指南和轨范(第三章)
Java本地接口(JNI)编程指南和规范(第三章)
Java本地接口(JNI)编程指南和规范(第三章)
2011年07月26日
Java本地接口(JNI)编程指南和规范(第三章)
[/b]
[b]第二部分: 编程者的指南(Part Two: Programmer's Guide)
第三章 基本类型,字符串和数组(Basic Types, Strings, and Arrays)
当面对带有本地代码的Java的应用程序时,程序员问的最通常的问之一,是在Java编程语言中的数据类型怎样对映到本地编程语言例如"C"和"C++"中的数据类型。在上一章节中出现的"Hello World!"例子中,我们没有传递任何参数到本地方法中,本地方法没有放回任何结果。本地方法简单地答应了一个消息和放回。
实际上,大多数程序将需要传递参数给本地方法,和也从本地方法接受结果。在这章节中,我们将描述怎样转换数据类型在用Java编程语言写的代码和实现本地方法的本地代码中。我们将从基本的类型开始,如整型(intergers)和普通的对象类型,如字符串(stirngs)和数组(arrays).我们推迟任意对象的彻底解决到下一章,在下一章我们将解释本地代码能怎样访问域和调用方法。
3.1 一个简单本地方法(A Simple Native Method)
让我们从一个简单的例子开始,这个例子不同于在上一章的"HelloWorld"程序。这个例子程序,"Prompt.java",是一个打印字符串,等待用户输入,然后返回一行用户的输入的本地方法。这个程序的源代码如下(as follows):
class Prompt{
// native method that prints a prompt and read a line
private native String getLine(String prompt) ;
public static void main(String args[]){
Prompt p = new Prompt() ;
String input = p.getLine("Type a line: ") ;
System.out.println("User typed:"+input) ;
}
static {
System.loadLibrary("Prompt") ;
}
}
"Prompt.main"调用本地方法"Prompt.getLine"来得到用户的输入。静态初始化调用了"System.loadLibrary"方法来在载入一个本地库"Prompt"。
3.1.1 为实现本地方法的C原型(C Prototype for Implementing the Native Method)
"Prompt.getLine"方法能用下面的"C"函数来实现:
JNIEXPORT jstring JNICALL
Java_Prompt_getLine(JNIEnv *env, jobject this, jstring prompt);
你能用"javah"工具来产生一个包含上面函数原型的同文件。"JNIEXPORT"和"JNICALL"宏(被定义在"jni.h"头文件里)确保这个函数从本地库中导出和"C"编译器产生带有为这个函数正确调用约定的代码。C函数的名字被格式为连接"Java_"前缀,类名和方法名。11.3部分包含了一个更精确的怎样格式化C函数名字的描述。
3.1.2 本地方法参数(Native Method Arguments)
在2.4部分中被简单的讨论,本地方法实现如"Java_Prompt_getLine"接受两个标准参数,除了参数在本地方法中被声明外。第一参数,"JNIEnv"接口指针,指向一个包含指向函数表的地方。在函数表中每个条目都指向一个"JNI"函数.本地方法总是通过这些"JNI"函数的一个来访问在Java虚拟器中的数据结构。"Figure 3.1"说明"JNIEnv"接口指针。
JNIEnv *
|
|---> pointer ----> Pointer ---> an interface function
(Internal virtual Pointer ---> an interface function
machine data Pointer ---> an interface function
structures) ...
Figure 3.1 the JNIEnv Interface Pointer
对于本地方法是一个静态或是一个实例方法,第二个参数不同。对一个实例化的本地方法的第二个参数,是一个关于被调用方法的对象的参考,类似于在"C++" 中的"this"指针。对于一个静态的本地方法的第二个参数,是一个定义这个方法的类的参考。我们的例子,"Java_Prompt_getLine", 实现了一个实例化的本地方法。因此这个"jobject"参数是对象(object)自己的参考。
3.1.3 类型的映射(Mapping of Types)
在本地方法声明中参数类型有对应的在本地编程语言中的类型。"JNI"定义了一套"C"和"C++"类型来对应在"Java"编程语言中的类型。
在"Java"编程语言中的两种类型:基本来型如"int,float",和"char";和参考类型如"classes","instances"和"arrays".在"Java"编程语言中,strings是"java.lang.String"类的一个实例。
"JNI"不同地对待基本类型和参考类型。基本类型的映射是简单易懂的。例如,在"Java"编程语言中的"int"类型映射到"C/C++" 的"jint"类型(定义在"jni.h"作为一个有符号32bit整型),同时在"Java"编程语言中的"float"类映射到"C++" 的"jfloat"类型(定义在"jni.h"作为一个有符号32bit浮点数),12.1.1部分包含了在"JNI"中定义的所有基本类型的定义。
"JNI"传递"objects"到本地方法作为不透明的引用(opaque references)。不透明的引用是一个C指针类型,引用了在Java虚拟机中的内部数据结构。然而,内部数据结构的精确安排,对编程者是隐藏的。本地代码必须通过恰当的"JNI"函数处理下面的对象(objects),"JNI"函数通过"JNIEnv"接口指针是可用的。例如,为"java.lang.String"对应的"JNI"类型是"jstring"。一个"jstring"引用(reference)的确切值是和本地代码无关的。本地代码调用"JNI"函数例如"GetStringUTFChars"来访问一个string的内容。
所有的"JNI"引用都是类型jobject。为了方便和增强类型的安全,"JNI"定义了一组引用类,它们概念上为"jobject"的子类型 ("subtypes").(A是B的子类,A的实例也是B的实例。)这些子类对应在"Java"编程语言中常用地引用类型。例如,"jstring"指示"strings";"jobjextArray"指示一组"objects"。12.1.2部分包含"JNI"引用类型和其他关系的资料性的完整列表。
3.2 访问Strings(Accessing Stirngs)
"Java_Prompt_getLine"函数接受"prompt"的一个"jstring"类型作为参数."jstring"类型表示在Java虚拟机中的"strings",同时和规定的“C string"类型不同(指向字符的指针,char *).你不能用一个一般"C string"来作为一个"jstring"来使用。下面的代码,如果运行,它们不能产生想要的结果。事实上,它将很可能使Java虚拟机奔溃。
JNIEXPORT jstring JNICALL
Java_Prompt_getLine(JNIEnv *env, jobjext obj, jstring prompt)
{
printf("%s", prompt) ;
......
}
3.2.1 转换到本地字串(Converting to Native Strings)
你的本地方法代码必须使用恰当的"JNI"函数来转化"jstirng objects"为"C/C++ strings"。"JNI"支持转换到或从"Unicode"和"UTF-8"的"strings"。"Unicode strings"表示字符是16bit的值,而"UTF-8 strings"使用了一编码规则,它享受兼容了"7-bit ASCII strings"。"UTF-8 strings"有想"C strings"空符号结尾,即使他们包含非"ASCII"(non-ASCII)字符.所有7-bit ASCII字符的值在1到127之间,在UTF-8编码中任然保留了。一个"byte"的最高"bit"被设置为1,标记一个多"byte"编码"16-bit Unicode"的值的开始。
"Java_Prompt_getLine"函数调用"JNI"函数"GetStringUTFChars"来阅读"string"的内容。通过"JNIEnv"的接口指针m"GetStringUTFChars"函数是能被调用的。它转换了作为一个"Unicode"序列通过Java虚拟机的实现来表示"jstring"的引用到用"UTF8" 格式表示的一个”C string"。如果你确定一个原始的字符只包含"7-bit ASCII"字符,你可以传递转换字符串到到规定"C"库函数例如"printf"。(我们讨论怎样处理"non-ASII string" 在8.2部分。)
JNIEXPORT jstring JNICALL
Java_Prompt_getLine(JNIEvn *env, jobjext obj, jstring prompt)
{
char buf[128] ;
const jbyte *str ;
str = (*env)->GetStringUTFChars(env, prompt , NULL) ;
if (NULL == str){
return NULL;
}
printf("%s", str) ;
(*env)->ReleaseStringUTFChars(env, prompt, str) ;
scanf("%s", buf) ;
return (*env)->NewStringUTF(env, buf) ;
}
不要忘记检查"GetStringUTFChars"的返回值。因为Java虚拟机实现需要分配空间来存储"UTF-8 string",这有有机会内配失败。当这样的事发生时,"GetStringUTFChars"返回"NULL",同时通过"JNI"抛出一个异常,它不同于在Java编程语言中抛出的一个异常。通过"JNI"抛出的一个未决的异常不自动地改变在本地"C"代码中的控制流程。替代是,我们需要发布一个清楚的返回声明来在"C"函数中跳过剩余的语句。在"Java_Prompt_getLine"返回后,在"Prompt.main"中抛出这个异常,"Prompt.getLine"的本地方法的调用者。
3.2.2 释放本地字符资源(Freeing Native String Resources)
当你本地代码结束使用通过"GetStringUTFChars"得到的"UTF-8 string"时,它调用"ReleaseStringUTFChars"。调用"ReleaseStringUTFChars"指明本地方法不再需要这"GetStringUTFChars"返回的"UTF-8 string"了;因此"UTF-8 string"占有的内存将被释放。没有调用"ReleaseStringUTFChars"将导致内存泄漏,这将可能最终导致内存的耗尽。
3.2.3 构建新的字符串(Constructing New Strings)
在本地方法中通过调用"JNI"函数"NewStringUTF",你能构建一个新的"java.lang.String"实例。"NewStringUTF"函数使用一个带有"UTF-8"格式的"C string",同时构建一个"java.lang.String"实例。最新被构建的"java.lang.String"实例表现为和被给的"UTF-8 C string"一样的"Unicode"字符序列。
如果虚拟机没有内存分配来构建"java.lang.String"实例,"NewStringUTF"抛出一个"OutofMemoryError"异常,同时返回"NULL"。在这个例子中,我们不需要检查这个来放回值,因为本地方法过后立即返回了。如果"NewStringUTF"失败,"OutOfMemoryError"异常将在"Prompt.main"方法中被抛出,来说明本地方法的调用。。如果"NewStringUTF" 成功,它返回一个"JNI"的最新构建的"java.lang.String"实例的引用。这最新实例被"Prompt.getLine"放回,然后赋给在"Prompt.main"中的"input"局部变量。
3.2.4 其他的JNI字符串函数(Other JNI String Functions)
"JNI"支持大量的其他字符串相关的函数(string-related functions),除了前面介绍的"GetStringUTFChars","ReleaseStringUTFChars"和"NewStringUTF"函数。
"GetStringChars"和"ReleaseStringChars"获得字符串使用"Unicode"格式。例如,在操作系统支持Unicode为本地字符串格式的时候,这些函数有用。
"UTF-8 string"总是以"\0"字符结尾,Unicode字符不是。为在一个"jstring"引用中得到Unicode字符的个数,JNI程序员能调用"GetStringLength"。为得到用"UTF-8"格式表示的一个"jstring"需要的"bytes"数,"JNI"程序员可以调用 ASCII C函数strlen在"GetStringUTFChars"的结果上,或直接地调用"JNI"函数"GetStringUTFLength" 在"jstring"引用上。
"GetStringChars"和"GetStringUTFChar"的第三个参数的需要额外的解释:
const jchar *
GetStringChars(JNIEnv *env, jstring str, jboolean *isCopy) ;
当来自"GetStringChars"返回时,通过"isCopy"被设置为"JNI_TRUE",返回的字符串是个在原始"java.lang.String"实例的字符串的一个复制,内存定位指向它。通过"isCopy"被设置为"JNI_FALSE",返回字符串时一个直接指向在原始的"java.lang.String"实例中的字符串,内存定位指向它。当通过"isCopy"被设置为"JNI_FALSE",内存定位指向的字符串时,本地代码必须不能修改返回的字符串的内容。违反了这个规则将引起原始"java.lang.String"实例也被修改。这破坏了"java.lang.String"永恒不可变性。
你最常传递"NULL"作为"isCopy"参数,因为你不关心Java虚拟机是否返回在"java.lang.String"实例中的一个复制字符串和直接指向原始字符串。
一般不可能预测虚拟机将是不是复制字串对一个给定的"java.lang.String"实例。因此程序员必须假设函数如"GetStringChars"可以使用对应的空间和时间到在"java.lang.String"实例中的大量字符上。在一个典型Java虚拟器实现中,垃圾收集器搬迁堆上的对象。一旦直接指向一个"java.lang.String"实例的指针被传递给本地代码,垃圾收集器再不能搬移这"java.lang.String"实例了。换另一种说法,虚拟器必须固定住"java.lang.String"实例。因为过多地固定导致内存碎片,虚拟器实现可以,为灭个单独地"GetStringChars"调用,酌情地决定是复制字串还是固定实例。
不要忘记调用"ReleaseStringChars",当你不再需要访问来自"GetStringChars"返回的"string"元素的时候。"ReleaseStringChars"调用是必须的,无论"GetStringChars"设置"* isCopy"为"JNI_TRUE"或"JNI_FALSE"。"ReleaseStringChars"释放副本,或解除实例的固定,这依赖"GetStringChars"是返回一个副本还是指向实例。
3.2.5 在"Java 2 SDK Release 1.2"中新建JNI字符串函数(New JNI String Functions in Java 2 SDK Release 1.2)
为了增加虚拟器能够返回直接指向在一个"java.lang.String"实例中的字符串的可能,"Java 2 SDK release 1.2"介绍了一对新函数,"Get/ReleaseStringCritical"。表面上,它们表现和"Get/ReleaseStringChars"函数相似。如果可能,在那两个函数中返回字符串的指针;否则产生一个副本。然而,这些函数怎样使用要有充分地限制。
你必须把这对函数中的代码放在一个排斥关键区域(critical region)中运行对待.在这个排斥关键域中,本地代码不应该调用任意"JNI"函数,或者任何可以引起当前线程阻塞和等待另一运行在Java虚拟器中的个线程的本地函数。例如,当前线程不应该等待在一个I/O流上的输入,这I/O是被另一个线程写入的。
这些限制使虚拟器能够暂停垃圾收集,当本地代码通过"GetStringCritical"来获得直接指向"string"的指针时。当垃圾收集暂停时,任何其他引发垃圾收集的线程也将本阻塞。在"Get/ReleaseStringCritical"对之间的本地代码,不应该出现阻塞调用或在Java虚拟器中分配新对象。否则,虚拟器可能锁死。思考下面的情况:
.另一线程出发垃圾收集不能进展下去,指导当前线程结束阻塞调用和使垃圾收集再能够。
.然而,当前线程不能进行下去,因为这阻塞调用需要获得一个已经被另一个等待执行垃圾收集的线程持有的锁。
交替的多对"GetStringCritical"和"ReleaseStringCritical"函数的使用时安全的。例如:
jchar *s1, *s2 ;
s1=(*env)->GetStringCritical(env, jstrl) ;
if ( s1 == NULL) {
....
}
.....
(*env)->ReleaseStringCritical(env, jstr1,s1) ;
(*env)->ReleaseStringCritical(env, jstr2,s2) ;
"Get/ReleaseStringCritical"对不需要严格的以一栈顺序来嵌套。我们不该忘记检查它们的返回值,阻止为内存状态可能是"NULL",因为如果VM内部用不同格式来表示数组,"GetStringCritical"可以分配一个"buffer"同时复制一个数组副本。例如,Java虚拟器(VM)可以不连续地保存数组。在这个例子中,"GetStringCritical"必须复制在"jstring"实例中的说有的字符为了返回本地代码一个连续的字符数组。
为避免锁死,你必须确信本,在调用"GetStringCritical"后和在调用"ReleaseStringCritical"前,地代码没有调用任何JNI函数。在排斥关键域中被允许调用的"JNI"函数是重载的"Get/ReleaseStringCritical"和"Get /ReleasePrimitiveArrayCritical"调用。
"JNI"不支持"GetStringUTFCritical"和"ReleaseStringUTFCritical"函数。这样函数可能需要虚拟器来复制一份"string",因为虚拟器实现大都在内部用"Unicode"编码表示"strings"。
对于"Java 2 SDK release 1.2"的其他附加是"GetStringRegion"和"GetStringUTFRegion"。这些函数复制"string"元素到预分配的"buffer"中。"Prompt.getLine"方法可以使用"GetStringUTFRegion"来实现的如下:
JNIEXPORT jstring JNICALL
Java_Prompt_getLine(JNIEnv *env, jobject obj, jstring prompt)
{
char outbuf[128], inbuf[128] ;
int len = (*env)->GetStringLength(env, prompt) ;
(*env)->GetStringUTFRegion(env, prompt, 0, len, outbuf) ;
printf("%s", outbuf) ;
scanf("%s", inbuf) ;
return (*env)->NewStringUTF(env, inbuf) ;
}
"GetStringUTFRegion"函数使用一个开始索引和长度,这两个备用来计算Unicode字符的个数。函数也执行了边际检查,和如果必要放出"StringIndexOutOfBoundsException"。在上面代码中,我们从自己的"string"引用中得到长度,因此确保不会索引溢出。(然而上面的代码缺少必要检查来确保"prompt string"的字符少于128。)
这个代码是比"GetStringUTFChars"稍微更简单的使用。因为"GetStringUTFRegion"不执行内存的分配,我们不需要检查可能没有内存的情况。(再说一次,上面的代码缺少必要检查来确保"prompt string"的字符少于128。)
3.2.6 "JNI String"函数的总结(Summary of JNI String Functions)
Table 3.1摘要所有字符串相关的"JNI"函数。"Java 2 SDK 1.2 release"增加一写新的函数,来增强对某些字符串操作的执行。这些增加的函数不支持新的操作,而是带来性能的改善。
Table 3.1 Summary of JNI String Functions
JNI Function Description since
GetStringChars Obtains or release a pointer to the JDK1.1
RetleaseStringChars contents of a string in Unicode format.
May return a copy of the string.
GetStringUTFChars Obtains or release a pointer to the JDK1.1
ReleaseStringUTFChars contents of a string in UTF-8 format.
May return a copy of the string.
GetStringLength Returns the number of Unicode char- JDK1.1
acters in the string.
GetStringUTFLength Returns the number of bytes needed JDK1.1
(not including the trailing 0) to repre-
sent a string in the UTF-8 Format.
NewString Create a java.lang.String instance JDK1.1
that contains the same sequence of
characters as the given Unicode C string
NewStringUTF Create a java.lang.String instance JDK1.1
that contains the same sequence of
characters as the given UTF-8 encoded C
string.
GetStringCritical Obtains a pointer to the contents of a Java 2
ReleaseStringCritical string in Unicode format. May return a SDK1.2
copy of theh string.Native code must
not block between a pair of Get/
ReleaseStringCritical calls.
GetStringRegion Copies the content of a string to or Java 2
SetStringRegion from a preallocated C buffer in the SDK1.2
Unicode format.
GetStringUTFRegion Copies the content of a string to or Java 2
SetStringUTFRegion from a preallocated C buffer in the SDK1.2
UTF-8 format.
3.2.7 在字符串函数中选择(Choosing among the String Functions)
Figure 3.2说明一个程序员可以怎样在"JDK release 1.1"和"Java 2 SDK release 1.2"上的字符串相关的函数中选择函数。
1.2 and
Targeting beyond Prealocated C Y GetStringRegion
release 1.1 or 1.2? --------> string buffer, ----> SetStringRegion
| small fixede-size GetStringUTFRegion
| string, or small SetStringUTFRegion
| substrings?
| 1.1 or both |
| | N
| |--> Any blocking or N
| JNI calls while ----> GetStringCritical
| accessing string ReleaseStringCritical
| contents?
| |
| |Y
|--> GetStringChars SDK release 1.2或更高的版本上编程,你需要复制字串的内容到一个已经存在"C buffer"中,请使用"GetStringRegion"或"GetStringUTFRegion"。
对于少的固定大小的字符串,"Get/SetStringRegion"和"Get/SetStringUTFRegion"几乎总是被执行函数,因为C buffer能被很容易在"C"栈来分配。复制在字符串中很少的字符开销可以忽略不计。
"Get/SetStringRegion"和"Get/SetStringUTFRegion"的一个好处就是它们不用执行内存的分配,因此不会产生不期望的没有内存的异常。不必没有异常检测,如果你确信没有索引溢出发生。
"Get/SetStringRegion"和"Get/SetStringUTFRegion"的另一好处你能指定开始的索引和复制的字符数目。如果本地代码只需要访问在长字符中的一部分,这函数很适合。
"GetStringCritical"必须特别小心使用。你必须保证在你通过"GetStringCritical"来得到一个指针时,本地代码在Java虚拟器中不能分配新的对象或执行另一个阻塞调用,这会引起系统的锁死。
这又个例子,示范使用"GetStringCritical"的精细说明。下面的代码获得"string"的内容和调用"fprintf"韩素来写出字符到文件句柄"fd"中:
const char *c_str = (*env)->GetStringCritical(env, j_str, 0) ;
if ( c_str == NULL ){
...
}
fprintf(fd, "%s\n", c_str) ;
(*env)->ReleaseStringCritical(env, j_str, c_str) ;
带有上面代码的问题是,当当前线程使收集垃圾无效时,对于写文件句柄不总是安全的。例如,假设另一个线程"T"等待从文件fd句柄中读数据。让我们更远的设想,操作系统缓冲的建立,是通过"fprintf"调用等待直到线程"T"从"fd"文件中完成读取所有未决的数据后的方法的。我们已经创建了一个有可能锁死情况:如果线程"T"不能分配足够内存来作为缓冲服务于从文件句柄中读数据,它必须请求一个垃圾收集,垃圾收集请求将被阻塞直到当前线程执行"ReleaseStringCritical",而这不会发生知道"fprintf"调用返回后。然而"fprintf"调用是等待的,因为线程"T"去完成从文件句柄中读数据。
下面代码,虽然相似于上面的例子,可几乎确定没有死锁:
const char *c_str = (*env)->GetStringCritical(env, j_str, 0) ;
if ( c_str == NULL ){
...
}
DrawString(C_str) ;
(*env)->ReleaseStringCritical(env, j_str, c_str) ;
"DrawString"是一个系统调用,直接写字符到屏幕上。除非屏幕显示驱动也是一个在一样虚拟机中运行的Java应用程序。这个"DrawString"函数将不被阻塞指明第等待垃圾收集的发生。
总之,你需要考虑所有的可能阻塞行为在"Get/ReleaseStringCritical"成对的调用之间。
3.3 访问数组(Accessing Array)
"JNI"对待基本的数组和对象数组是不同地。基础数组包含原始是基本类型的例如"int"和"boolean"。对象数组(Object arrays)包含元素是应用类型例如class实例和其他数组。例如,在下面用Java编程语言下的代码片段中:
int[] iarr;
float[] farr;
object[] oarr ;
int[][] arr2 ;
"iarr"和"farr"是基本数组,然而"oarr"和"arr2"是对象数组。
在请求"JNI"函数使用的本地方法中,访问基本数组相似于用它们对"strings"访问。让我们看一个简单的例子。下面的代码调用一个本地方法"sumArray"来添加一个"int"数组的内容。
class IntArray{
private native int sumArray(int[] arr) ;
public static void main(String[] args){
IntArray p = new IntArray() ;
int arr[] = new int[10] ;
for ( int i = 0 ; i 不能实现"Java_IntArray_sumArray"本地方法来非直接访问一个"jarray"引用。下面"C"代码是不合逻辑和不能产生需要的结果的:
JNIEXPORT jint JNICALL
Java_IntArray_sumArray(JNIEnv *env, jobject obj, jintArray arr)
{
jint i , sum = 0 ;
for ( i = 0 ; i GetIntArrayRegion(env, arr, 0, 10, buf) ;
for ( i = 0 ; i 代码中访问它们。不需要异常检测,因为我们知道在我们例子中数组的长度是10,因此没有索引的溢出。
"JNI"支持一个对应的"SetIntArrayRegion"函数,它允许本地代码来修改"int"类型的数组元素。其他基本类的数组(例如"boolean, short and float")也有类似的支持函数。
"JNI"支持一系列"Get/ReleaseArrayElements"函数(例如,包含"Get /ReleaseIntArrayElements"),它们允许本地代码来得到一个直接指向基本类型数组元素的指针。因为底层的垃圾收集器可能不支持固定,虚拟器可能返回一个指向原始基本类型数组的一个副本。我们能够使用"GetIntArrayElements"来重写在3.3.1部分的本地方法的实现,如下:
JNIEXPORT jint JNICALL
Java_IntArray_sumArray(JNIEnv *env, jobjext obj, jintArray arr)
{
jint *carr ;
jint i, sum =0 ;
carr = (*env)->GetIntArrayElements(env, arr, NULL ) ;
if ( carr == NULL){
return 0 ;
}
for (i = 0 ; i ReleaseIntArrayElements(env, arr, carr, 0) ;
return sum ;
}
"GetArrayLength"函数得到在基本类型或"object"类型数组中的元素的个数。当数组被第一次分配空间时,决定了一个数组的固定长度。
"Java 2 SDK release 1.2"介绍了"Get/ReleasePrimitiveArrayCritical"函数。这些函数允许虚拟器来使垃圾收集停止,当本地代码访问基本类型数组的能容时候。编程者必须有和在使用"Get/ReleaseStringCritical"函数时同样的注意点。在"Get/ReleasePrimitiveArrayCritical"函数对之间,本地代码必须没有调用任意的"JNI"函数,或执行任何的阻塞操作,这些可以引起应用的死锁。
3.3.3 "JNI"基本类型数组函数概要(Summary of JNI Primitive Array Functions)
Table 3.2是和基本类型数组相关的所有"JNI"函数的一个概要。"Java 2 SDK release 1.2"添加一些新的函数来增强对某些数组操作的执行。添加函数不是支持新的操作,而是带来执行性能的改善。
Table 3.2 Summary of JNI Primitive Array Functions
JNI Function Description since
GetArrayRegion Copies the contents of primitive JDK1.1
SetArrayRegion arrays to or from a preallicated
buffer.
GetArrayElements Obtains a pointer to the contents JDK1.1
ReleaseArrayElements of a primitive array.May return a
copy of the array.
GetArrayLength Returns the number of elements in JDK1.1
the array.
NewArray Creates an array with the given JDK1.1
length.
GetPrimitiveArrayCritical Obtain or releases a pointer to Java 2
ReleasePrimitiveArrayCritical the contents of a primitive array. SDK1.2
May disable garbage collection, or
return a copy of the array.
3.3.4 在基本类型数组函数中选择(Choosing among the Primitive Array Functions)
Figure 3.3 说明一个编程者可以怎样在"JDK release 1.1"和"Java 2 SDK release 1.2"的访问基本类的数组的JNI函数中选择函数:
Preallocated C array Y
buffer, small fixed- ---------------------> GetArrayRegion
size arrays, or small SetArrayRegion
subarrays
| 1.2 and beyond
|--> Targeting release -----------> Any blocking or JNI
1.1 or 1.2 Calls while accessing -----------|
| array contents? |
1.1 or both | | |
| Y | N |
|--> GetArrayElements ArrayElements ReleasePrimitiveArrayCritical
Figure 3.3 Choosing among Primitive Array Functions
如果你需要复制到或来自一个预分配的"C buffer",就使用"Get/SetArrayRegion"系列函数。这些函数执行了边界的检查,和在需要时产生"ArrayIndexOutOfBoundsException"异常。在3.3.1部分中本地方法实现使用了"GetIntArrayRegion"来复制出自一个jarray引用的10个元素。
对于小的固定大小的数组,"Get/SetArrayRegion"几乎总是首选函数,因为"C buffer"能在"C stack"上很容易地分配。复制一个少量数组元素的开销可以忽略。
"Get/SetArrayRegion"函数允许你指定开始的索引和元素的个数,因此如果本地代码需要访问在一个大数组总的一部分元素,这是最好的函数。
如果你没有预分配的"C buffer",基本类型的数组来决定大小,同时本地代码在得到数组元素的指针时没有调用阻塞调用,我们使用在"Java 2 SDK release 1.2"中的"Get/ReleasePrimitiveArrayCritical"函数。就象"Get/ReleaseStringCritical"函数,"Get/ReleasePrimitiveArrayCritical"函数必须格外小心使用,为了避免死锁。
使用"Get/ReleaseArrayElements"系列函数总是安全的。虚拟器返回一个直接指向数组元素的指针,或返回一个缓冲保存这个数组元素的副本。
3.3.5 访问Objects类型数组(Accessing Arrays of Objects)
"JNI"提供单独一对函数来访问"Objects"类型数组。"GetObjectArrayElement"函数返回被给的一个索引的元素,然而"SetObjectArrayElement"函数更新被给的一个索引的元素。不象基本类型数组情况,你不能立马得到所有"objext"类型的元素或复制多个"object"类型的元素。
字符串(Stings)和数组(arrays)都是类型的引用。你使用"Get/SetObjectArrayElement"来访问字符串和数组的数组。
下面的例子调用一个本地方法来创建一个二维int类型数组,然后打印这个数组的内容。
class ObjectArrayTest{
private static native int[][] initInt2DArray(int size) ;
public static void main(String[] args){
int[][] i2arr = initInt2DArray(3) ;
for (int i = 0 ; i FindClass(env, "[I");
if (intArrCls == NULL){
return NULL ;
}
result = (*env)->NewObjectArray(env, size, intArrCls, NULL) ;
if (result == NULL ){
return NULL ;
}
for( i = 0 ; i NewIntArray(env, size) ;
if ( iarr == NULL){
return NULL ;
}
for (j = 0 ; j SetIntArrayRegion(env, iarr, 0, size, tmp) ;
(*env)->SetObjectArrayElement(env, result, i, iarr) ;
(*env)->DeleteLocalRef(env, iarr) ;
}
return result ;
}
"newInt2DArray"方法首先调用了"JNI"函数"FindClass"来得到一个二维int类型数组的元素类型的引用。"FindClass"的"[I"参数是"JNI class descriptor(描述符)",对应在Java编程语言中的"int[]"类型。如果类型载入失败,"FindClass"返回"NULL"同时抛出一个异常(例如,在一个错误类型文件或者没有存储空间的情况)
下一个"NewObjextArray"函数分配一个数组,数组元素类型被用"intArrCls"类型引用来指示。"NewObjectArray"函数只分配了第一个维度,我们剩下构建二维来填充数组元素任务。Java虚拟机没有为多维数组指定特定的数据结构。一个二维的数组是一个简答的数组的数组。
创建二维的代码是相当简单的。"NewIntArray"分配了各个数组的元素,同时"SetIntArrayRegion"复制"temp[] buffer"的内容到新分配的一维数组中。在完成"SetObjectArrayElement"调用,第i个一维数组的第j个元素值是"i+j"。
运行"ObjectArrayTest.main"方法产生如下输出:
0 1 2
1 2 3
2 3 4
在循环最后的"DeleteLocalRef"调用确保虚拟器不会用光被用来持有JNI参考例如"iarr"的内存。5.2.1部分详细解释什么时候和为什么你需要调用"DeleteLocalRef"。
Java本地接口(JNI)编程指南和规范(第三章)
2011年07月26日
Java本地接口(JNI)编程指南和规范(第三章)
[/b]
[b]第二部分: 编程者的指南(Part Two: Programmer's Guide)
第三章 基本类型,字符串和数组(Basic Types, Strings, and Arrays)
当面对带有本地代码的Java的应用程序时,程序员问的最通常的问之一,是在Java编程语言中的数据类型怎样对映到本地编程语言例如"C"和"C++"中的数据类型。在上一章节中出现的"Hello World!"例子中,我们没有传递任何参数到本地方法中,本地方法没有放回任何结果。本地方法简单地答应了一个消息和放回。
实际上,大多数程序将需要传递参数给本地方法,和也从本地方法接受结果。在这章节中,我们将描述怎样转换数据类型在用Java编程语言写的代码和实现本地方法的本地代码中。我们将从基本的类型开始,如整型(intergers)和普通的对象类型,如字符串(stirngs)和数组(arrays).我们推迟任意对象的彻底解决到下一章,在下一章我们将解释本地代码能怎样访问域和调用方法。
3.1 一个简单本地方法(A Simple Native Method)
让我们从一个简单的例子开始,这个例子不同于在上一章的"HelloWorld"程序。这个例子程序,"Prompt.java",是一个打印字符串,等待用户输入,然后返回一行用户的输入的本地方法。这个程序的源代码如下(as follows):
class Prompt{
// native method that prints a prompt and read a line
private native String getLine(String prompt) ;
public static void main(String args[]){
Prompt p = new Prompt() ;
String input = p.getLine("Type a line: ") ;
System.out.println("User typed:"+input) ;
}
static {
System.loadLibrary("Prompt") ;
}
}
"Prompt.main"调用本地方法"Prompt.getLine"来得到用户的输入。静态初始化调用了"System.loadLibrary"方法来在载入一个本地库"Prompt"。
3.1.1 为实现本地方法的C原型(C Prototype for Implementing the Native Method)
"Prompt.getLine"方法能用下面的"C"函数来实现:
JNIEXPORT jstring JNICALL
Java_Prompt_getLine(JNIEnv *env, jobject this, jstring prompt);
你能用"javah"工具来产生一个包含上面函数原型的同文件。"JNIEXPORT"和"JNICALL"宏(被定义在"jni.h"头文件里)确保这个函数从本地库中导出和"C"编译器产生带有为这个函数正确调用约定的代码。C函数的名字被格式为连接"Java_"前缀,类名和方法名。11.3部分包含了一个更精确的怎样格式化C函数名字的描述。
3.1.2 本地方法参数(Native Method Arguments)
在2.4部分中被简单的讨论,本地方法实现如"Java_Prompt_getLine"接受两个标准参数,除了参数在本地方法中被声明外。第一参数,"JNIEnv"接口指针,指向一个包含指向函数表的地方。在函数表中每个条目都指向一个"JNI"函数.本地方法总是通过这些"JNI"函数的一个来访问在Java虚拟器中的数据结构。"Figure 3.1"说明"JNIEnv"接口指针。
JNIEnv *
|
|---> pointer ----> Pointer ---> an interface function
(Internal virtual Pointer ---> an interface function
machine data Pointer ---> an interface function
structures) ...
Figure 3.1 the JNIEnv Interface Pointer
对于本地方法是一个静态或是一个实例方法,第二个参数不同。对一个实例化的本地方法的第二个参数,是一个关于被调用方法的对象的参考,类似于在"C++" 中的"this"指针。对于一个静态的本地方法的第二个参数,是一个定义这个方法的类的参考。我们的例子,"Java_Prompt_getLine", 实现了一个实例化的本地方法。因此这个"jobject"参数是对象(object)自己的参考。
3.1.3 类型的映射(Mapping of Types)
在本地方法声明中参数类型有对应的在本地编程语言中的类型。"JNI"定义了一套"C"和"C++"类型来对应在"Java"编程语言中的类型。
在"Java"编程语言中的两种类型:基本来型如"int,float",和"char";和参考类型如"classes","instances"和"arrays".在"Java"编程语言中,strings是"java.lang.String"类的一个实例。
"JNI"不同地对待基本类型和参考类型。基本类型的映射是简单易懂的。例如,在"Java"编程语言中的"int"类型映射到"C/C++" 的"jint"类型(定义在"jni.h"作为一个有符号32bit整型),同时在"Java"编程语言中的"float"类映射到"C++" 的"jfloat"类型(定义在"jni.h"作为一个有符号32bit浮点数),12.1.1部分包含了在"JNI"中定义的所有基本类型的定义。
"JNI"传递"objects"到本地方法作为不透明的引用(opaque references)。不透明的引用是一个C指针类型,引用了在Java虚拟机中的内部数据结构。然而,内部数据结构的精确安排,对编程者是隐藏的。本地代码必须通过恰当的"JNI"函数处理下面的对象(objects),"JNI"函数通过"JNIEnv"接口指针是可用的。例如,为"java.lang.String"对应的"JNI"类型是"jstring"。一个"jstring"引用(reference)的确切值是和本地代码无关的。本地代码调用"JNI"函数例如"GetStringUTFChars"来访问一个string的内容。
所有的"JNI"引用都是类型jobject。为了方便和增强类型的安全,"JNI"定义了一组引用类,它们概念上为"jobject"的子类型 ("subtypes").(A是B的子类,A的实例也是B的实例。)这些子类对应在"Java"编程语言中常用地引用类型。例如,"jstring"指示"strings";"jobjextArray"指示一组"objects"。12.1.2部分包含"JNI"引用类型和其他关系的资料性的完整列表。
3.2 访问Strings(Accessing Stirngs)
"Java_Prompt_getLine"函数接受"prompt"的一个"jstring"类型作为参数."jstring"类型表示在Java虚拟机中的"strings",同时和规定的“C string"类型不同(指向字符的指针,char *).你不能用一个一般"C string"来作为一个"jstring"来使用。下面的代码,如果运行,它们不能产生想要的结果。事实上,它将很可能使Java虚拟机奔溃。
JNIEXPORT jstring JNICALL
Java_Prompt_getLine(JNIEnv *env, jobjext obj, jstring prompt)
{
printf("%s", prompt) ;
......
}
3.2.1 转换到本地字串(Converting to Native Strings)
你的本地方法代码必须使用恰当的"JNI"函数来转化"jstirng objects"为"C/C++ strings"。"JNI"支持转换到或从"Unicode"和"UTF-8"的"strings"。"Unicode strings"表示字符是16bit的值,而"UTF-8 strings"使用了一编码规则,它享受兼容了"7-bit ASCII strings"。"UTF-8 strings"有想"C strings"空符号结尾,即使他们包含非"ASCII"(non-ASCII)字符.所有7-bit ASCII字符的值在1到127之间,在UTF-8编码中任然保留了。一个"byte"的最高"bit"被设置为1,标记一个多"byte"编码"16-bit Unicode"的值的开始。
"Java_Prompt_getLine"函数调用"JNI"函数"GetStringUTFChars"来阅读"string"的内容。通过"JNIEnv"的接口指针m"GetStringUTFChars"函数是能被调用的。它转换了作为一个"Unicode"序列通过Java虚拟机的实现来表示"jstring"的引用到用"UTF8" 格式表示的一个”C string"。如果你确定一个原始的字符只包含"7-bit ASCII"字符,你可以传递转换字符串到到规定"C"库函数例如"printf"。(我们讨论怎样处理"non-ASII string" 在8.2部分。)
JNIEXPORT jstring JNICALL
Java_Prompt_getLine(JNIEvn *env, jobjext obj, jstring prompt)
{
char buf[128] ;
const jbyte *str ;
str = (*env)->GetStringUTFChars(env, prompt , NULL) ;
if (NULL == str){
return NULL;
}
printf("%s", str) ;
(*env)->ReleaseStringUTFChars(env, prompt, str) ;
scanf("%s", buf) ;
return (*env)->NewStringUTF(env, buf) ;
}
不要忘记检查"GetStringUTFChars"的返回值。因为Java虚拟机实现需要分配空间来存储"UTF-8 string",这有有机会内配失败。当这样的事发生时,"GetStringUTFChars"返回"NULL",同时通过"JNI"抛出一个异常,它不同于在Java编程语言中抛出的一个异常。通过"JNI"抛出的一个未决的异常不自动地改变在本地"C"代码中的控制流程。替代是,我们需要发布一个清楚的返回声明来在"C"函数中跳过剩余的语句。在"Java_Prompt_getLine"返回后,在"Prompt.main"中抛出这个异常,"Prompt.getLine"的本地方法的调用者。
3.2.2 释放本地字符资源(Freeing Native String Resources)
当你本地代码结束使用通过"GetStringUTFChars"得到的"UTF-8 string"时,它调用"ReleaseStringUTFChars"。调用"ReleaseStringUTFChars"指明本地方法不再需要这"GetStringUTFChars"返回的"UTF-8 string"了;因此"UTF-8 string"占有的内存将被释放。没有调用"ReleaseStringUTFChars"将导致内存泄漏,这将可能最终导致内存的耗尽。
3.2.3 构建新的字符串(Constructing New Strings)
在本地方法中通过调用"JNI"函数"NewStringUTF",你能构建一个新的"java.lang.String"实例。"NewStringUTF"函数使用一个带有"UTF-8"格式的"C string",同时构建一个"java.lang.String"实例。最新被构建的"java.lang.String"实例表现为和被给的"UTF-8 C string"一样的"Unicode"字符序列。
如果虚拟机没有内存分配来构建"java.lang.String"实例,"NewStringUTF"抛出一个"OutofMemoryError"异常,同时返回"NULL"。在这个例子中,我们不需要检查这个来放回值,因为本地方法过后立即返回了。如果"NewStringUTF"失败,"OutOfMemoryError"异常将在"Prompt.main"方法中被抛出,来说明本地方法的调用。。如果"NewStringUTF" 成功,它返回一个"JNI"的最新构建的"java.lang.String"实例的引用。这最新实例被"Prompt.getLine"放回,然后赋给在"Prompt.main"中的"input"局部变量。
3.2.4 其他的JNI字符串函数(Other JNI String Functions)
"JNI"支持大量的其他字符串相关的函数(string-related functions),除了前面介绍的"GetStringUTFChars","ReleaseStringUTFChars"和"NewStringUTF"函数。
"GetStringChars"和"ReleaseStringChars"获得字符串使用"Unicode"格式。例如,在操作系统支持Unicode为本地字符串格式的时候,这些函数有用。
"UTF-8 string"总是以"\0"字符结尾,Unicode字符不是。为在一个"jstring"引用中得到Unicode字符的个数,JNI程序员能调用"GetStringLength"。为得到用"UTF-8"格式表示的一个"jstring"需要的"bytes"数,"JNI"程序员可以调用 ASCII C函数strlen在"GetStringUTFChars"的结果上,或直接地调用"JNI"函数"GetStringUTFLength" 在"jstring"引用上。
"GetStringChars"和"GetStringUTFChar"的第三个参数的需要额外的解释:
const jchar *
GetStringChars(JNIEnv *env, jstring str, jboolean *isCopy) ;
当来自"GetStringChars"返回时,通过"isCopy"被设置为"JNI_TRUE",返回的字符串是个在原始"java.lang.String"实例的字符串的一个复制,内存定位指向它。通过"isCopy"被设置为"JNI_FALSE",返回字符串时一个直接指向在原始的"java.lang.String"实例中的字符串,内存定位指向它。当通过"isCopy"被设置为"JNI_FALSE",内存定位指向的字符串时,本地代码必须不能修改返回的字符串的内容。违反了这个规则将引起原始"java.lang.String"实例也被修改。这破坏了"java.lang.String"永恒不可变性。
你最常传递"NULL"作为"isCopy"参数,因为你不关心Java虚拟机是否返回在"java.lang.String"实例中的一个复制字符串和直接指向原始字符串。
一般不可能预测虚拟机将是不是复制字串对一个给定的"java.lang.String"实例。因此程序员必须假设函数如"GetStringChars"可以使用对应的空间和时间到在"java.lang.String"实例中的大量字符上。在一个典型Java虚拟器实现中,垃圾收集器搬迁堆上的对象。一旦直接指向一个"java.lang.String"实例的指针被传递给本地代码,垃圾收集器再不能搬移这"java.lang.String"实例了。换另一种说法,虚拟器必须固定住"java.lang.String"实例。因为过多地固定导致内存碎片,虚拟器实现可以,为灭个单独地"GetStringChars"调用,酌情地决定是复制字串还是固定实例。
不要忘记调用"ReleaseStringChars",当你不再需要访问来自"GetStringChars"返回的"string"元素的时候。"ReleaseStringChars"调用是必须的,无论"GetStringChars"设置"* isCopy"为"JNI_TRUE"或"JNI_FALSE"。"ReleaseStringChars"释放副本,或解除实例的固定,这依赖"GetStringChars"是返回一个副本还是指向实例。
3.2.5 在"Java 2 SDK Release 1.2"中新建JNI字符串函数(New JNI String Functions in Java 2 SDK Release 1.2)
为了增加虚拟器能够返回直接指向在一个"java.lang.String"实例中的字符串的可能,"Java 2 SDK release 1.2"介绍了一对新函数,"Get/ReleaseStringCritical"。表面上,它们表现和"Get/ReleaseStringChars"函数相似。如果可能,在那两个函数中返回字符串的指针;否则产生一个副本。然而,这些函数怎样使用要有充分地限制。
你必须把这对函数中的代码放在一个排斥关键区域(critical region)中运行对待.在这个排斥关键域中,本地代码不应该调用任意"JNI"函数,或者任何可以引起当前线程阻塞和等待另一运行在Java虚拟器中的个线程的本地函数。例如,当前线程不应该等待在一个I/O流上的输入,这I/O是被另一个线程写入的。
这些限制使虚拟器能够暂停垃圾收集,当本地代码通过"GetStringCritical"来获得直接指向"string"的指针时。当垃圾收集暂停时,任何其他引发垃圾收集的线程也将本阻塞。在"Get/ReleaseStringCritical"对之间的本地代码,不应该出现阻塞调用或在Java虚拟器中分配新对象。否则,虚拟器可能锁死。思考下面的情况:
.另一线程出发垃圾收集不能进展下去,指导当前线程结束阻塞调用和使垃圾收集再能够。
.然而,当前线程不能进行下去,因为这阻塞调用需要获得一个已经被另一个等待执行垃圾收集的线程持有的锁。
交替的多对"GetStringCritical"和"ReleaseStringCritical"函数的使用时安全的。例如:
jchar *s1, *s2 ;
s1=(*env)->GetStringCritical(env, jstrl) ;
if ( s1 == NULL) {
....
}
.....
(*env)->ReleaseStringCritical(env, jstr1,s1) ;
(*env)->ReleaseStringCritical(env, jstr2,s2) ;
"Get/ReleaseStringCritical"对不需要严格的以一栈顺序来嵌套。我们不该忘记检查它们的返回值,阻止为内存状态可能是"NULL",因为如果VM内部用不同格式来表示数组,"GetStringCritical"可以分配一个"buffer"同时复制一个数组副本。例如,Java虚拟器(VM)可以不连续地保存数组。在这个例子中,"GetStringCritical"必须复制在"jstring"实例中的说有的字符为了返回本地代码一个连续的字符数组。
为避免锁死,你必须确信本,在调用"GetStringCritical"后和在调用"ReleaseStringCritical"前,地代码没有调用任何JNI函数。在排斥关键域中被允许调用的"JNI"函数是重载的"Get/ReleaseStringCritical"和"Get /ReleasePrimitiveArrayCritical"调用。
"JNI"不支持"GetStringUTFCritical"和"ReleaseStringUTFCritical"函数。这样函数可能需要虚拟器来复制一份"string",因为虚拟器实现大都在内部用"Unicode"编码表示"strings"。
对于"Java 2 SDK release 1.2"的其他附加是"GetStringRegion"和"GetStringUTFRegion"。这些函数复制"string"元素到预分配的"buffer"中。"Prompt.getLine"方法可以使用"GetStringUTFRegion"来实现的如下:
JNIEXPORT jstring JNICALL
Java_Prompt_getLine(JNIEnv *env, jobject obj, jstring prompt)
{
char outbuf[128], inbuf[128] ;
int len = (*env)->GetStringLength(env, prompt) ;
(*env)->GetStringUTFRegion(env, prompt, 0, len, outbuf) ;
printf("%s", outbuf) ;
scanf("%s", inbuf) ;
return (*env)->NewStringUTF(env, inbuf) ;
}
"GetStringUTFRegion"函数使用一个开始索引和长度,这两个备用来计算Unicode字符的个数。函数也执行了边际检查,和如果必要放出"StringIndexOutOfBoundsException"。在上面代码中,我们从自己的"string"引用中得到长度,因此确保不会索引溢出。(然而上面的代码缺少必要检查来确保"prompt string"的字符少于128。)
这个代码是比"GetStringUTFChars"稍微更简单的使用。因为"GetStringUTFRegion"不执行内存的分配,我们不需要检查可能没有内存的情况。(再说一次,上面的代码缺少必要检查来确保"prompt string"的字符少于128。)
3.2.6 "JNI String"函数的总结(Summary of JNI String Functions)
Table 3.1摘要所有字符串相关的"JNI"函数。"Java 2 SDK 1.2 release"增加一写新的函数,来增强对某些字符串操作的执行。这些增加的函数不支持新的操作,而是带来性能的改善。
Table 3.1 Summary of JNI String Functions
JNI Function Description since
GetStringChars Obtains or release a pointer to the JDK1.1
RetleaseStringChars contents of a string in Unicode format.
May return a copy of the string.
GetStringUTFChars Obtains or release a pointer to the JDK1.1
ReleaseStringUTFChars contents of a string in UTF-8 format.
May return a copy of the string.
GetStringLength Returns the number of Unicode char- JDK1.1
acters in the string.
GetStringUTFLength Returns the number of bytes needed JDK1.1
(not including the trailing 0) to repre-
sent a string in the UTF-8 Format.
NewString Create a java.lang.String instance JDK1.1
that contains the same sequence of
characters as the given Unicode C string
NewStringUTF Create a java.lang.String instance JDK1.1
that contains the same sequence of
characters as the given UTF-8 encoded C
string.
GetStringCritical Obtains a pointer to the contents of a Java 2
ReleaseStringCritical string in Unicode format. May return a SDK1.2
copy of theh string.Native code must
not block between a pair of Get/
ReleaseStringCritical calls.
GetStringRegion Copies the content of a string to or Java 2
SetStringRegion from a preallocated C buffer in the SDK1.2
Unicode format.
GetStringUTFRegion Copies the content of a string to or Java 2
SetStringUTFRegion from a preallocated C buffer in the SDK1.2
UTF-8 format.
3.2.7 在字符串函数中选择(Choosing among the String Functions)
Figure 3.2说明一个程序员可以怎样在"JDK release 1.1"和"Java 2 SDK release 1.2"上的字符串相关的函数中选择函数。
1.2 and
Targeting beyond Prealocated C Y GetStringRegion
release 1.1 or 1.2? --------> string buffer, ----> SetStringRegion
| small fixede-size GetStringUTFRegion
| string, or small SetStringUTFRegion
| substrings?
| 1.1 or both |
| | N
| |--> Any blocking or N
| JNI calls while ----> GetStringCritical
| accessing string ReleaseStringCritical
| contents?
| |
| |Y
|--> GetStringChars SDK release 1.2或更高的版本上编程,你需要复制字串的内容到一个已经存在"C buffer"中,请使用"GetStringRegion"或"GetStringUTFRegion"。
对于少的固定大小的字符串,"Get/SetStringRegion"和"Get/SetStringUTFRegion"几乎总是被执行函数,因为C buffer能被很容易在"C"栈来分配。复制在字符串中很少的字符开销可以忽略不计。
"Get/SetStringRegion"和"Get/SetStringUTFRegion"的一个好处就是它们不用执行内存的分配,因此不会产生不期望的没有内存的异常。不必没有异常检测,如果你确信没有索引溢出发生。
"Get/SetStringRegion"和"Get/SetStringUTFRegion"的另一好处你能指定开始的索引和复制的字符数目。如果本地代码只需要访问在长字符中的一部分,这函数很适合。
"GetStringCritical"必须特别小心使用。你必须保证在你通过"GetStringCritical"来得到一个指针时,本地代码在Java虚拟器中不能分配新的对象或执行另一个阻塞调用,这会引起系统的锁死。
这又个例子,示范使用"GetStringCritical"的精细说明。下面的代码获得"string"的内容和调用"fprintf"韩素来写出字符到文件句柄"fd"中:
const char *c_str = (*env)->GetStringCritical(env, j_str, 0) ;
if ( c_str == NULL ){
...
}
fprintf(fd, "%s\n", c_str) ;
(*env)->ReleaseStringCritical(env, j_str, c_str) ;
带有上面代码的问题是,当当前线程使收集垃圾无效时,对于写文件句柄不总是安全的。例如,假设另一个线程"T"等待从文件fd句柄中读数据。让我们更远的设想,操作系统缓冲的建立,是通过"fprintf"调用等待直到线程"T"从"fd"文件中完成读取所有未决的数据后的方法的。我们已经创建了一个有可能锁死情况:如果线程"T"不能分配足够内存来作为缓冲服务于从文件句柄中读数据,它必须请求一个垃圾收集,垃圾收集请求将被阻塞直到当前线程执行"ReleaseStringCritical",而这不会发生知道"fprintf"调用返回后。然而"fprintf"调用是等待的,因为线程"T"去完成从文件句柄中读数据。
下面代码,虽然相似于上面的例子,可几乎确定没有死锁:
const char *c_str = (*env)->GetStringCritical(env, j_str, 0) ;
if ( c_str == NULL ){
...
}
DrawString(C_str) ;
(*env)->ReleaseStringCritical(env, j_str, c_str) ;
"DrawString"是一个系统调用,直接写字符到屏幕上。除非屏幕显示驱动也是一个在一样虚拟机中运行的Java应用程序。这个"DrawString"函数将不被阻塞指明第等待垃圾收集的发生。
总之,你需要考虑所有的可能阻塞行为在"Get/ReleaseStringCritical"成对的调用之间。
3.3 访问数组(Accessing Array)
"JNI"对待基本的数组和对象数组是不同地。基础数组包含原始是基本类型的例如"int"和"boolean"。对象数组(Object arrays)包含元素是应用类型例如class实例和其他数组。例如,在下面用Java编程语言下的代码片段中:
int[] iarr;
float[] farr;
object[] oarr ;
int[][] arr2 ;
"iarr"和"farr"是基本数组,然而"oarr"和"arr2"是对象数组。
在请求"JNI"函数使用的本地方法中,访问基本数组相似于用它们对"strings"访问。让我们看一个简单的例子。下面的代码调用一个本地方法"sumArray"来添加一个"int"数组的内容。
class IntArray{
private native int sumArray(int[] arr) ;
public static void main(String[] args){
IntArray p = new IntArray() ;
int arr[] = new int[10] ;
for ( int i = 0 ; i 不能实现"Java_IntArray_sumArray"本地方法来非直接访问一个"jarray"引用。下面"C"代码是不合逻辑和不能产生需要的结果的:
JNIEXPORT jint JNICALL
Java_IntArray_sumArray(JNIEnv *env, jobject obj, jintArray arr)
{
jint i , sum = 0 ;
for ( i = 0 ; i GetIntArrayRegion(env, arr, 0, 10, buf) ;
for ( i = 0 ; i 代码中访问它们。不需要异常检测,因为我们知道在我们例子中数组的长度是10,因此没有索引的溢出。
"JNI"支持一个对应的"SetIntArrayRegion"函数,它允许本地代码来修改"int"类型的数组元素。其他基本类的数组(例如"boolean, short and float")也有类似的支持函数。
"JNI"支持一系列"Get/ReleaseArrayElements"函数(例如,包含"Get /ReleaseIntArrayElements"),它们允许本地代码来得到一个直接指向基本类型数组元素的指针。因为底层的垃圾收集器可能不支持固定,虚拟器可能返回一个指向原始基本类型数组的一个副本。我们能够使用"GetIntArrayElements"来重写在3.3.1部分的本地方法的实现,如下:
JNIEXPORT jint JNICALL
Java_IntArray_sumArray(JNIEnv *env, jobjext obj, jintArray arr)
{
jint *carr ;
jint i, sum =0 ;
carr = (*env)->GetIntArrayElements(env, arr, NULL ) ;
if ( carr == NULL){
return 0 ;
}
for (i = 0 ; i ReleaseIntArrayElements(env, arr, carr, 0) ;
return sum ;
}
"GetArrayLength"函数得到在基本类型或"object"类型数组中的元素的个数。当数组被第一次分配空间时,决定了一个数组的固定长度。
"Java 2 SDK release 1.2"介绍了"Get/ReleasePrimitiveArrayCritical"函数。这些函数允许虚拟器来使垃圾收集停止,当本地代码访问基本类型数组的能容时候。编程者必须有和在使用"Get/ReleaseStringCritical"函数时同样的注意点。在"Get/ReleasePrimitiveArrayCritical"函数对之间,本地代码必须没有调用任意的"JNI"函数,或执行任何的阻塞操作,这些可以引起应用的死锁。
3.3.3 "JNI"基本类型数组函数概要(Summary of JNI Primitive Array Functions)
Table 3.2是和基本类型数组相关的所有"JNI"函数的一个概要。"Java 2 SDK release 1.2"添加一些新的函数来增强对某些数组操作的执行。添加函数不是支持新的操作,而是带来执行性能的改善。
Table 3.2 Summary of JNI Primitive Array Functions
JNI Function Description since
GetArrayRegion Copies the contents of primitive JDK1.1
SetArrayRegion arrays to or from a preallicated
buffer.
GetArrayElements Obtains a pointer to the contents JDK1.1
ReleaseArrayElements of a primitive array.May return a
copy of the array.
GetArrayLength Returns the number of elements in JDK1.1
the array.
NewArray Creates an array with the given JDK1.1
length.
GetPrimitiveArrayCritical Obtain or releases a pointer to Java 2
ReleasePrimitiveArrayCritical the contents of a primitive array. SDK1.2
May disable garbage collection, or
return a copy of the array.
3.3.4 在基本类型数组函数中选择(Choosing among the Primitive Array Functions)
Figure 3.3 说明一个编程者可以怎样在"JDK release 1.1"和"Java 2 SDK release 1.2"的访问基本类的数组的JNI函数中选择函数:
Preallocated C array Y
buffer, small fixed- ---------------------> GetArrayRegion
size arrays, or small SetArrayRegion
subarrays
| 1.2 and beyond
|--> Targeting release -----------> Any blocking or JNI
1.1 or 1.2 Calls while accessing -----------|
| array contents? |
1.1 or both | | |
| Y | N |
|--> GetArrayElements ArrayElements ReleasePrimitiveArrayCritical
Figure 3.3 Choosing among Primitive Array Functions
如果你需要复制到或来自一个预分配的"C buffer",就使用"Get/SetArrayRegion"系列函数。这些函数执行了边界的检查,和在需要时产生"ArrayIndexOutOfBoundsException"异常。在3.3.1部分中本地方法实现使用了"GetIntArrayRegion"来复制出自一个jarray引用的10个元素。
对于小的固定大小的数组,"Get/SetArrayRegion"几乎总是首选函数,因为"C buffer"能在"C stack"上很容易地分配。复制一个少量数组元素的开销可以忽略。
"Get/SetArrayRegion"函数允许你指定开始的索引和元素的个数,因此如果本地代码需要访问在一个大数组总的一部分元素,这是最好的函数。
如果你没有预分配的"C buffer",基本类型的数组来决定大小,同时本地代码在得到数组元素的指针时没有调用阻塞调用,我们使用在"Java 2 SDK release 1.2"中的"Get/ReleasePrimitiveArrayCritical"函数。就象"Get/ReleaseStringCritical"函数,"Get/ReleasePrimitiveArrayCritical"函数必须格外小心使用,为了避免死锁。
使用"Get/ReleaseArrayElements"系列函数总是安全的。虚拟器返回一个直接指向数组元素的指针,或返回一个缓冲保存这个数组元素的副本。
3.3.5 访问Objects类型数组(Accessing Arrays of Objects)
"JNI"提供单独一对函数来访问"Objects"类型数组。"GetObjectArrayElement"函数返回被给的一个索引的元素,然而"SetObjectArrayElement"函数更新被给的一个索引的元素。不象基本类型数组情况,你不能立马得到所有"objext"类型的元素或复制多个"object"类型的元素。
字符串(Stings)和数组(arrays)都是类型的引用。你使用"Get/SetObjectArrayElement"来访问字符串和数组的数组。
下面的例子调用一个本地方法来创建一个二维int类型数组,然后打印这个数组的内容。
class ObjectArrayTest{
private static native int[][] initInt2DArray(int size) ;
public static void main(String[] args){
int[][] i2arr = initInt2DArray(3) ;
for (int i = 0 ; i FindClass(env, "[I");
if (intArrCls == NULL){
return NULL ;
}
result = (*env)->NewObjectArray(env, size, intArrCls, NULL) ;
if (result == NULL ){
return NULL ;
}
for( i = 0 ; i NewIntArray(env, size) ;
if ( iarr == NULL){
return NULL ;
}
for (j = 0 ; j SetIntArrayRegion(env, iarr, 0, size, tmp) ;
(*env)->SetObjectArrayElement(env, result, i, iarr) ;
(*env)->DeleteLocalRef(env, iarr) ;
}
return result ;
}
"newInt2DArray"方法首先调用了"JNI"函数"FindClass"来得到一个二维int类型数组的元素类型的引用。"FindClass"的"[I"参数是"JNI class descriptor(描述符)",对应在Java编程语言中的"int[]"类型。如果类型载入失败,"FindClass"返回"NULL"同时抛出一个异常(例如,在一个错误类型文件或者没有存储空间的情况)
下一个"NewObjextArray"函数分配一个数组,数组元素类型被用"intArrCls"类型引用来指示。"NewObjectArray"函数只分配了第一个维度,我们剩下构建二维来填充数组元素任务。Java虚拟机没有为多维数组指定特定的数据结构。一个二维的数组是一个简答的数组的数组。
创建二维的代码是相当简单的。"NewIntArray"分配了各个数组的元素,同时"SetIntArrayRegion"复制"temp[] buffer"的内容到新分配的一维数组中。在完成"SetObjectArrayElement"调用,第i个一维数组的第j个元素值是"i+j"。
运行"ObjectArrayTest.main"方法产生如下输出:
0 1 2
1 2 3
2 3 4
在循环最后的"DeleteLocalRef"调用确保虚拟器不会用光被用来持有JNI参考例如"iarr"的内存。5.2.1部分详细解释什么时候和为什么你需要调用"DeleteLocalRef"。