在
Java
应用链接本地代码时(
when interfacing Java applications with native code
),程序员们常常会有共同的一个疑问,那就是
Java
中的数据类型怎么与
C/C++
中的数据类型相互映射(
map
)?在上一章最后展示的“
Hello World
!”程序中,我们没有向本地代码传递任何参数,本地方法也没有返回任何返回值。本地方法只是简单地打印一条信息然后就
return
了。
实际上,绝大多数程序需要向
native
方法传递参数,同时也接收
nativ
方法的返回值。在这一章中,我们将介绍如何在
Java
代码和实现
native
方法的
native
代码之间进行数据类型的转换。我们将从基本类型如
integer
以及普通对象类型如
string
和
array
开始。我们把对任意对象的处理方式放到下一章讲解,在下一章我们将讲解
native
代码怎样访问对象那个字段和调用方法(
access fields and make method calls
)。
3.1 一个简单的native方法
让我们从一个与上一章的
HelloWorld
区别不大的简单实例开始。示例程序
Prompt.java
包含一个打印字符串的
native
方法,等待用户输入然后返回用户输入的一行字符串(
line
)。这个程序的源代码如下所示:
class Prompt {
// native method that prints a prompt and reads 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
调用
native
方法
Prompt.getLine
来接受用户输入。静态初始化程序调用
System.loadLibrary
方法加载叫做
Prompt
的
native
库。
3.1.1 要实现的native方法的 C原型
Prompt.getLine
方法在下面的
C
函数中被实现:
JNIEXPORT jstring JNICALL
Java_Prompt_getLine(JNIEnv *env, jobject this, jstring prompt)
你可以使用
javah
工具来生成包含上面函数原型的头文件。
JNIEXPORT
和
JNICALL
宏(在
jni.h
头文件中定义)确保这个函数是从
native
库中读取的(
is exported from the native libirary
)以及
C
编译器遵循正确的调用约定为这个函数生成代码。
C
函数的名称由“
Java_
”前缀、类名和方法名组成。在
11.3
这一节中包含了对
C
函数命名的更详细的讲述。
3.1.2 native方法参数(arguement)
正如在
2.4
节中简单提到过的,
native
方法实现如
Java_Prompt_getLine
除了
native
方法中声明的参数外,还传入了两个标准参数(
standard parameters
)。第一个参数,
JNIEnv
接口指针,指向一个指针与函数的映射表(
point to a location that contains a pointer to a function table
)。函数表中的每一个入口指向一个
JNI
函数。
native
方法通过其中的
JNI
函数访问
Java
虚拟机中的数据结构。图
3.1
说明了
JNIEnv
接口指针。
图
3.1 JNI
接口指针
第二个参数是有所不同的,由
native
方法时静态方法还是实例方法决定。实例(
instance
)
native
方法中的第二个参数是一个指向所调用方法所在对象的引用(
a reference to the object on which the method is invoked
),与
C++
中的
this
指针类似。静态
native
方法中的第二个参数是一个指向定义此方法的类的引用。在我们的
Java_Prompt_getLine
实例中,实现了一个实例
native
方法。
3.1.3 类型映射
native
方法声明中的参数类型与
native
语言中的类型一致。
JNI
中定义了一系列
C
和
C++
类型与
Java
中的类型相统一。
在
Java
中有两种类型:基本类型(
int
,
float
,
char
)和引用类型(
classes
,
instances
,
arrays
)。在
Java
中,字符串是
java.lang.String.class
的实例。
JNI
对基本类型和引用类型的处理方式不同。基本类型的映射是直接的。例如,
java
中的
int
类型映射到
C/C++
类型
jint
(在
jni.h
中定义为
32
位有符号整数),而
java
中的
float
对应
C/C++
类型
jfloat
(在
jni.h
中定义为
32
位浮点数)。
12.1.1
这一节中包含了所有基本类型在
JNI
中的定义。
JNI
将对象作为(
opaque reference
)传给
native
方法。
opaque reference
是
C
指针类型,指向
Java
虚拟机内部的数据结构。然而,内部数据结构的准确布局(
exact layout
)是对程序员隐藏的。
native
代码必须通过合适的
JNI
函数执行对底层对象(
the underlying object
)的操作,这些函数可以通过
JNIEnv
接口指针使用。例如
java.lang.String
对应的
JNI
类型是
jstring
。
jstring
引用的准确的值对
native
代码无关的(
the exact value of a jstring
)。
native
代码调用
JNI
函数,比如
GetStringUTFChars
(
3.2.1
),来访问字符串内容。
所有
JNI
引用都有
jobject
类型(
All JNI reference have type jobject
)。为了方便和增强类型安全,
JNI
定义了一组引用类型,可以从概念上看作是
jobject
的子类型(
conceptually
“
subtypes
”
of jobject
)。(如果
A
是
B
的每个实例的子类型,那么
A
也是
B
的实例)这些子类型对应
Java
中常用的引用类型。例如,
jstring
表示字符串;
jobjectArray
表示一个对象数组(
an array of object
)。
12.2
节中包含了完整的
JNI
引用类型与他们的子类型的关系列表。
3.2 访问字符串
Java_Prompt_getLine
函数接收
prompt
参数作为
jstring
类型。
jstring
类型在
Java
虚拟机中表示字符串,与常规的
C
语言字符串类型不同(指向连续字符的指针,
char *
)。你不能将
jstring
作为正常的
C
语言字符串使用。如果运行下面的代码将不会产生预期的结果。实际上,更有可能使
Java
虚拟机死机重启。
JNIEXPORT jstring JNICALL
Java_Prompt_getLine(JNIEnv *env, jobject obj, jstring prompt)
{
/* ERROR: incorrect use of jstring as a char* pointer */
printf("%s", prompt);
...
}
3.2.1 转换为native字符串
你的
native
代码必须使用合适的
JNI
函数将
jstring
对象转换为
C/C++
字符串。
JNI
支持
Unicode
和
UTF-8
字符串的相互转换。
Unicode
字符串使用用
16
位的值表示单个字符,然而,
UTF-8
字符串采用与
7
位
ASCII
码向上兼容的编码体制。(
whereas UTF-8 strings use an encoding scheme that is upward compatible with 7-bit ASCII strings.
)
UTF-8
字符串像以
NULL
做终结符的
C
字符串,即使
UTF-8
字符串包含非
ASCII
字符。在
UTF-8
编码中所有编码值在
1
到
127
之间的
ASCII
字符保持不变。高位的
1
个字节(
A byte with the highest bit set signals the beginning of a multi-byte encoded 16-bit Unicode value.
)
Java_Prompt_getLine
函数调用
JNI
函数
GetStringUTFChars
来读取字符串内容。
GetStringUTFChars
函数通过
JNIEnv
接口指针调用。它把
jstring
引用,通常在
Java
虚拟机实现(
implementation
)中表示为
Unicode
序列,转换为
UTF-8
格式的
C
语言字符串。如果你能确定初始的字符串仅包含
7
字节
ASCII
字符,那么你可以直接将转换过的字符串传递给常规
C
语言库函数比如
printf
。(我们将会在
8.2
节讨论怎么处理非
ASCII
字符串)
JNIEXPORT jstring JNICALL
Java_Prompt_getLine(JNIEnv *env, jobject obj, jstring prompt)
{
char buf[128];
const jbyte *str;
str = (*env)->GetStringUTFChars(env, prompt, NULL);
if (str == NULL) {
return NULL; /* OutOfMemoryError already thrown */
}
printf("%s", str);
(*env)->ReleaseStringUTFChars(env, prompt, str);
/* We assume here that the user does not type more than
* 127 characters */
scanf("%s", buf);
return (*env)->NewStringUTF(env, buf);
}
不要忘了检查
GetStringUTFChars
的返回值。因为
Java
虚拟机需要为
UTF-8
字符串分配内存,因此存在内存分配失败的风险。当这种情况发生时,
GetStringUTFChars
返回
NULL
并且抛出
OutOfMemoryError
异常。正如我们将要在第
6
章中学到的,通过
JNI
抛出异常与在
Java
中抛出异常时不同的。通过
JNI
抛出的
pending
异常(
pending exception
)不会自动改变
native C
代码中的控制流。相反,我们需要给出(
issue
)一个明确的返回值(
return statement
)以跳过
C
函数中遗留的返回值。当
Java_Prompt_getLine
返回后,异常将会在
Prompt.main
,
Prompt.getLinenative
方法的调用者中抛出。
3.2.2 释放native字符串资源
当你的
native
代码使用完通过
GetStringUTFChars
获得的
UTF-8
字符串之后,它将调用
ReleaseStringUTFChars
。调用
ReleaseStringUTFChars
表示
native
方法不再需要
GetStringUTFChars
返回的字符串,因此
UTF-8
字符串占用的内存可以被释放。没有正确调用
ReleaseStringUTFChars
将会导致内存泄露,最终会导致内存耗尽
3.2.3 构造新字符串
你可以通过调用
JNI
函数
NewStringUTF
在
native
方法中创建
java.lang.String
的实例。
NewStringUTF
取得(
takes
)一个
UTF-8
格式的
C
字符串,并创建一个
java.lang.String
实例。新创建的
java.lang.String
实例与给出的
UTF-8
格式的
C
语言字符串具有相同的
Unicode
字符序列。
如果虚拟机不能分配创建
java.lang.String
实例所需要的内存,
NewStringUTF
会抛出
OutOfMemoryError
异常并返回
NULL.
在这个示例中,我们不需要检查它的返回值,因为
native
方法随即(
immediately afterwards
)
return
。如果
NewStringUTF
执行失败,
OutOfMemoryError
异常将会在执行
native
方法调用的
Prompt.main
方法中抛出。如果
NewStringUTF
执行成功,它将返回一个指向新创建的
java.lang.String
实例的
JNI
引用。新实例通过
Prompt.gerLine
返回,然后赋给
Prompt.main
中的局部变量
input
。
3.2.4 其他JNI函数
除了之前介绍的
GetStringUTFChars
、
ReleaseStringUTFChars
和
NewStringUTF
之外,
JNI
还支持大量其他的字符串相关函数。
GetStringChars
和
ReleaseStringChars
获得
Unicode
格式的字符串。当操作系统支持
Unicode
作为
native
字符串格式时,这些函数是相当有用的。
UTF-8
字符串总是以
'\0'
结尾,而
Unicode
并非如此。
JNI
程序员可以调用
GetStringLength
获得(
find out
)
jstring
引用中
Unicode
字符的数目。为了知道表示(
represent
)一个
UTF-8
格式的
jstring
需要多少字节。
JNI
程序员也可以在
GetStringUTFChars
返回结果的基础上调用
ANSI C
函数
strlen
(
call the ANSI C functionstrlenon the result of GetStringUTFChars
),或者直接调用
JNI
函数
GetStringUTFLength
,传入
jstring
引用(
call the JNI functionGetStringUTFLength on the jstringreference directly
)。
GetStringChars
和
GetStringUTFChars
的第三个参数需要额外的声明(
explanation
):
const jchar *
GetStringChars(JNIEnv *env, jstring str, jboolean *isCopy);
紧跟着
GetStringChars
返回之后,如果返回的字符串是原始的
java.lang.String
实例中字符串的副本(
copy
),那么
isCopy
指向的内存位置将会被置为
JNI_TRUE
。如果返回的字符串是一个指向原始的
java.lang.String
实例中字符串(
characters
)的直接(
direct
)指针,那么
isCopy
指向的内存位置将会被置为
JNI_FALSE
。如果
isCopy
指向的位置被置为
JNI_FALSE
,
native
代码不能修改返回的字符串(
characters
)的内容。(
Upon returning from GetStringChars, the memory location pointed to by isCopy will be set toJNI_TRUE if the returned string is a copy of the characters in the original java.lang.String instance. The memory location pointed to by isCopy will be set to JNI_FALSE if the returned string is a direct pointer to the characters in the original java.lang.String instance. When the location pointed to by isCopy is set to JNI_FALSE, native code must not modify the contents of the returned string.
)违反这条规则会使得原始的
java.lang.String
实例也被修改。这样就破坏(
break
)了
java.lang.String
实例不可改变的不变性(
invariant
)。
最常见的情况是传递
NULL
作为
isCopy
参数,因为你根本不关心
java
虚拟机返回的是
java.lang.String
实例中字符串的副本还是直接指针指向的原始字符串。
通常来说,预测虚拟机是否复制给出的
java.lang.String
实例中的字符串(
characters
)是不可能的。因此程序员必须假定
GetStringChars
这样的函数可以根据
java.lang.String
实例中的字符数量自动消耗相称的时间和空间。(
Programmers must therefore assume functions such as GetStringChars may take time and space proportional to the number of characters in thejava.lang.String instance.
)在经典的
Java
虚拟机实现中,垃圾回收器在堆中重新放置对象。一旦指向
java.lang.String
实例的直接指针被传回
native
代码中,垃圾回收器将不再重新配置
java.lang.String
实例。
To put it another way, the virtual machine must pin the java.lang.String instance. Because excessive pinning leads to memory fragmentation, the virtual machine implementation may, at its discretion, decide to either copy the characters or pin the instance for each individual GetStringChars call.
当你不再访问
GetStringChars
返回的字符串单元(
element
)时,不要忘记调用
ReleaseStringChars
。无论
GetStringChars
将
*isCopy
设置为
JNI_TRUE
还是
JNI_FALSE
,调用
ReleaseStringChars
都是必须的。根据
GetStringChars
返回的是一个
copy
还是一个
direct
指针,
ReleaseStringChars
也相应的释放
copy
或
unpin
实例。
3.2.5 JDK 1.2中新的JNI字符串函数
为增加虚拟机返回指向
java.lang.String
实例中字符串(
characters
)的
direct
指针的可能性,
Java 2 SDK
发行版
1.2
中增加了一对新函数
Get/ReleaseStringCritical
。表面上看,它们跟
Get/ReleaseStringChars
函数非常类似,如果可能的话都会返回一个指向字符串的指针,否则,创建一个副本(
copy
)。然而,对于怎么使用这些函数有着明确的限制。
你必须认为(
treat
)这对函数内部的代码运行在临界区(
critical region
),
native
代码不能调用任意的
JNI
函数或任何可能造成当前线程被锁住和等待运行在另一个虚拟机里的线程的
native
方法。例如,当前线程不能等待另一个线程的
I/O
流中的输入(
the current thread must not wait for input on an I/O stream being written to by another thread.
)。
这些限制可能会导致这么一种情况:当
native
代码持有(
hold
)一个指向通过
GetStringCritical
获得的字符串单元的
direct
指针时,虚拟机的垃圾回收器可能会变为不可用状态。当垃圾回收器不可用时,任何启动(
trigger
)垃圾回收器的线程也将被锁住。
Get/ReleaseStringCritical
对中间的
native
代码必须不能造成阻塞调用(
blocking call
)或者在虚拟机中分配内存给一个新对象。否则,虚拟机将陷入停顿。考虑下列场景:
1
、被另一个线程启动(
tigger
)的垃圾回收器不能进行下一步(
make progress
)直到当前线程完成阻塞调用(
blocking call
)并使垃圾回收器重新变为可用状态。
2
、与此同时,当前线程不能进行下一步(
make progress
)因为阻塞调用(
blocking call)
需要获得一个早已被其他线程持有的锁,这个线程在等待使用垃圾回收器。
嵌套使用(
overlap multiple
)多对
GetStringCritical
和
ReleaseStringCritical
是安全的,例如:
jchar *s1, *s2;
s1 = (*env)->GetStringCritical(env, jstr1);
if (s1 == NULL) {
... /* error handling */
}
s2 = (*env)->GetStringCritical(env, jstr2);
if (s2 == NULL) {
(*env)->ReleaseStringCritical(env, jstr1, s1);
... /* error handling */
}
... /* use s1 and s2 */
(*env)->ReleaseStringCritical(env, jstr1, s1);
(*env)->ReleaseStringCritical(env, jstr2, s2);
Get/ReleaseStringCritical
不必严格按照堆顺序嵌套使用(
need not be strictly nested in a stack order
)。我们绝不能忘记检查它的返回值以免因为内存不足而返回
NULL
的场景(
We must not forget to check its return value against NULL for possible out of memory situations
)。因为如果虚拟机(
VM
)内部用不同的格式表示数组,
GetStringCritical
可能会分配一个缓冲区然后做一个数组的
copy
。例如,
Java
虚拟机可能不会将数组连续存放(
not store array contiguously
)。这样的话,
GetStringCritical
必须赋值
jstring
实例中所有的字符以便于返回连续的字符数组给
native
代码。
为避免死锁的出现,你必须确保
native
代码在它结束
GetStringCritical
之后和进行相应的
ReleaseStringCritical
调用之前部队任意(
arbitrary
)的
JNI
函数进行调用。在临界区(
critical region
)允许调用的函数只有嵌套的
Get/ReleaseStringCritical
函数和
Get/RealeasePrimitiveArrayCritical
。
JNI
不支持
GetStringUTFCritical
函数的
ReleaseStringUTFCritical
函数,这些函数可能会要求虚拟机对字符串进行复制,因为大部分虚拟机内部字符串都是
Unicode
格式。
Java 2 SDK
发行版
1.2
新增的还有
GetStringRegion
和
GetStringUTFRegion
。这些函数把字符串单元拷到预分配的(
preallocated
)缓冲区中。
Prompt.getLine
方法可以使用
GetStringUTFRegion
重写:
JNIEXPORT jstring JNICALL
Java_Prompt_getLine(JNIEnv *env, jobject obj, jstring prompt)
{
/* assume the prompt string and user input has less than 128
characters */
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
字符数(
both counted as number of Unicode characters
)。此函数也会进行边界检查,如果必要的话会抛出
StringIndexOutOfBoundsException
。在上面的代码中,我们从字符串引用自身获得字符串长度因此确定没有索引溢出(
index overflow
)。(然而,上面的代码缺少必要的检查来确保
prompt
字符串少于
128
个字符。)
这个代码比使用
GetStringUTFChars
简单点。因为
GetStringUTFRegion
不进行内存分配,我们不需要检查可能的内存不足的情况。(另外,上面的代码缺少必要的检查来确保
prompt
字符串少于
128
个字符。)
3.2.6 JNI字符串函数总结
表
3.1
总结了所有
JNI
的字符串相关函数。
Java 2 SDK 1.2
发行版增加了大量新函数,提高了对特定字符串进行操作的性能。新增的函数不支持任何新的操作,除了带来性能的改善。
表
3.1 JNI
字符串函数总结
3.2.7 在字符串函数中进行选择
图
3.2
对程序员应该如何选择使用
JDK1.1
和
JDK 1.2
中的字符串相关函数进行了说明:
如果你想适用于
1.1
或
1.1
和
1.2
版本,除了
Get/ReleaseStringChars
和
Get/ReleaseStringUTFChars
没有其他的选择。
如果你使用
JDK1.2
或更高版本
JDK
,并且你想将字符串内容复制到预分配好的
C
缓冲区中,使用
GetStringRegion
或
GetStringUTFRegion
。
对于一些固定长度的短字符串,
Get/SetStringRegion
和
Get/SetStringUTFRegion
几乎总是应该被优先使用的函数,因为
C
语言缓冲区可以在
C
语言的栈中以非常小的代价进行分配(
the C buffer can be allocated on the C stack very cheaply
)。复制字符串中少量字符的开销(
overhead
)是可以忽略不计的。
Get/SetStringRegion
和
Get/SetStringUTFRegion
的一个优势是,他们不进行内存分配,因此不会抛出意料之外的内存不足异常。如果你确信索引溢出不会发生,就没有必要进行异常检查。
Get/SetStringRegion
和
Get/SetStringUTFRegion
的另一个优势是你可以指明索引其实位置与字符数目。如果
native
代码只需要访问一个长字符串的子字符串时,这些函数式比较适合的。
GetStringCritical
使用的时候必须格外小心。你必须确定,当持有通过
GetStringCritical
得到的指针时,
native
代码不再
Java
虚拟机中为新的对象分配内存或执行其他可能造成系统死锁的阻塞调用。
有一个实例来示范(
demonstrate
)在使用
GetStringCritical
时出现的微妙的问题。下面代码获取字符串内容然后调用
fprint
函数将字符串写到文件句柄
fd
中:
/* This is not safe! */
const char *c_str = (*env)->GetStringCritical(env, j_str, 0);
if (c_str == NULL) {
... /* error handling */
}
fprintf(fd, "%s\n", c_str);
(*env)->ReleaseStringCritical(env, j_str, c_str);
上面代码的问题在于,当垃圾回收器被当前线程禁用时,向一个文件句柄中写东西并不总是安全的。例如,假设有一个线程
T
正在等待读取
fd
文件句柄(
file handle
)。让我们进一步假设操作系统缓冲区这样设置:
fprintf
调用一直处于等待状态,直到线程
T
完成从
fd
中读取所有挂起数据的操作。我们建立了一个死锁的场景
:
如果线程
T
不能为从文件句柄中读取的数据分配足够的内存作为缓冲区的话,那么它必须请求使用垃圾回收器。然而,垃圾回收器请求将会被
锁住(
blocked
)直到当前线程执行完毕
ReleaseStringCritical
,而这不可能在
fprintf
调用返回之前发生。然而,
fprintf
调用等待线程
T
结束从文件句柄中读取数据。
下面的代码虽然与上面的代码比较相似,但是几乎可以肯定没有死锁:
/* This code segment is OK. */
const char *c_str = (*env)->GetStringCritical(env, j_str, 0);
if (c_str == NULL) {
... /* error handling */
}
DrawString(c_str);
(*env)->ReleaseStringCritical(env, j_str, c_str);
DrawString
是一个系统调用,可以直接向屏幕上写字符串。除非屏幕显示驱动也是运行于同一个虚拟机的
Java
应用,否则
DrawString
不会因无限期的等待垃圾回收器而被锁住。
总之,在
Get/ReleaseStringCritical
调用之间,你必须考虑所有锁的行为。
3.3 访问数组
JNI
对于基本数组(
primitive array
)和对象数组(
object array
)的处理是不同的。基本数组包含的元素是基本类型如
int
和
boolean
。对象数组包含的元素是引用类型比如类的实例和其他数组。例如,在下面的
Java
代码段中,
iarr
和
farr
是基本数组,而
oarr
和
arr2
是对象数组:
int[] iarr;
float[] farr;
Object[] oarr;
int[][] arr2;
在
native
代码中访问基本数组需要向访问字符串那样使用
JNI
函数。让我们来看一个简单的例子。下面程序调用了一个
native
方法
sunArray
来把
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 < 10; i++) {
arr[i] = i;
}
int sum = p.sumArray(arr);
System.out.println("sum = " + sum);
}
static {
System.loadLibrary("IntArray");
}
}
3.3.1 在C中访问数组
数组用
jarray
类型和它的“子类型”比如
jintarray
表示。正如
jstring
不是
C
语言字符串类型一样,
jarray
类型也不是
C
语言数组类型。你不能间接通过
jarray
引用实现
Java_IntArray_sumArray native
方法。下面的
C
语言代码是非法的,将不会产生预期的结果:
/* This program is illegal! */
JNIEXPORT jint JNICALL
Java_IntArray_sumArray(JNIEnv *env, jobject obj, jintArray arr)
{
int i, sum = 0;
for (i = 0; i < 10; i++) {
sum += arr[i];
}
}
正如下面实例所示,你必须使用合适的
JNI
函数来访问基本数组元素:
JNIEXPORT jint JNICALL
Java_IntArray_sumArray(JNIEnv *env, jobject obj, jintArray arr)
{
jint buf[10];
jint i, sum = 0;
(*env)->GetIntArrayRegion(env, arr, 0, 10, buf);
for (i = 0; i < 10; i++) {
sum += buf[i];
}
return sum;
}
3.3.2 访问基本类型数组
前面的例子使用
GetIntArrayRegion
函数复制
int
数组中所有的元素到一个
C
缓冲区(
buf
)中。第三个参数是元素开始时的索引(
index
),第四个参数是要复制的元素的数目。一旦元素被复制到
C
缓冲区后,我们就可以在
native
代码中访问他们。因为我们知道例子中数组长度是
10
,因此不会出现
index
溢出,因此不必进行异常检查。
JNI
支持对应的
SetIntArrayRegion
函数,这个函数允许
native
代码修改
int
类型的数组元素。
JNI
也提供了对其他基本类型此操作的支持。
JNI
支持同源的(
a family of
)
Get/Release<Type>ArrayElements
函数(包括
Get/ReleaseIntArrayElements
)
,
这些函数允许
native
代码获得一个指向基本数组元素的
direct
指针。因为底层的垃圾回收器可能不支持
pinning
,因此虚拟机可能会返回指向原始基本数组副本的指针。我们可以像下面这样用
GetIntArrayElements
重写
3.3.1
节中
native
方法的实现:
JNIEXPORT jint JNICALL
Java_IntArray_sumArray(JNIEnv *env, jobject obj, jintArray arr)
{
jint *caar;
jint i, sum = 0;
carr = (*env)->GetIntArrayElements(env, arr, NULL);
if(carr == NULL){
return 0;//exception occurred
}
for(i=0; i<10; i++){
sum += carr[i];
}
(*env)->ReleaseIntArrayElements(env, arr, carr, 0);
return sum;
}
GetArrayLength
函数返回一个基本数组或对象数组中元素个数。当第一次给数组分配空间时,数组的长度就被确定了。
JDK1.2
引入了
Get/ReleasePrimitiveArrarCritical
函数。这些函数允许当
native
代码访问基本数组内容时虚拟机使垃圾回收机制不可用。程序员必须像使用
Get/ReleaseStringCritical
那样小心。在
Get/ReleasePrimitiveArrayCritical
函数之间,
native
代码不允许调用任意(
arbitrary
)的
JNI
函数,或者执行任何可能造成程序死锁的阻塞操作。
3.3.3 JNI基本数组函数总结
表
3.2
是对所有与基本数组相关的
JNI
函数的总结。
JDK 1.2
增加了大量新函数,使对特定数组进行操作的性能大大提高。新增的函数除了性能的提升不支持新的操作。
3.3.4 在基本数组函数之间进行选择
图
3.3
对程序员应该如何选择使用
JDK1.1
和
JDK 1.2
中的
JNI
函数来访问基本数组进行了说明:
如果你需要复制或从预分配的
C
缓冲区中复制,使用
Get/Set<Type>ArrayRegion
族函数(
family of functions
)。这些函数会执行边界检查并且如果必要会抛出
ArrayIndexOutOfBoundsException
异常。
3.3.1
节中的
native
方法使用
GetIntArrayRegion
从
jarray
引用中(
out of a jarray reference
)复制
10
个元素。
对一些长度确定的小的数组,
Get/Set<Type>ArrayRegion
几乎总是最优先选择的函数,因为
C
语言缓冲区可以在
C
语言的栈中以非常小的代价进行分配。复制数组中少量元素的开销(
overhead
)是可以忽略不计的。
Get/Set<Type>ArrayRegion
函数允许你指明开始下标(
index
)和元素数目,因此如果
native
代码只需要范文一个大数组子集时,这是优先选择的函数。
如果你没有预分配的
C
缓冲区,基本数组是不定长的并且当
native
代码持有指向数组元素的指针时,不会造成阻塞调用,那么可以使用
JDK 1.2
中的
Get/ReleasePrimitiveArrayCritical
函数。正如
Get/ReleaseStringCritical
函数一样,
Get/ReleasePrimitiveArrayCritical
使用时必须格外小心以避免死锁。
使用
Get/Release<type>ArrayElements
族函数(
family of function
)总是安全的。虚拟机返回一个指向数组元素的
direct
指针或返回一个保存有数组元素副本的缓冲区(
buffer
)。
3.3.5 访问对象数组
JNI
提供了一对单独的函数来访问对象数组,
GetObjectArrayElement
返回指定下标(
index
)处的元素,而
SetObjectArrayElement
更新指定下标(
index
)处的元素。不像基本数组类型那样,你不能获取所有的对象元素或者一次同时复制多个元素。
字符串和数组都是引用类型。你可以使用
Get/SetObjectArrayElement
访问字符串数组和高维数组(
array of arrays
)。
下面的例子调用
native
方法创建一个二维
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 < 3; i++) {
for (int j = 0; j < 3; j++) {
System.out.print(" " + i2arr[i][j]);
}
System.out.println();
}
}
static {
System.loadLibrary("ObjectArrayTest");
}
}
静态
native
方法
initInt2DArray
创建一个给定尺寸的二维数组。为二维数组分配内存并初始化的
native
方法可以向下面这样写:
JNIEXPORT jobjectArray
JNICALL Java_ObjectArrayTest_initInt2DArray(JNIEnv *env, jclass cls, int size)
{
jobjectArray result;
int i;
jclass intArrCls = (*env)->FindClass(env, "[I");
if(intArrCls == NULL){
return NULL;//exception thrown
}
result = (*env)->NewObjectArray(env, size, intArrCls, NULL);
if(result == NULL){
return NULL;//out of memory error thrown
}
for(i = 0; i < size; i++){
jint tmp[256]; //make sure it is large enough!
int j;
jintArray iarr = (*env)->NewIntArray(env, size);
if(iarr == NULL){
return NULL;//out of memory error thrown
}
for(j = 0; j < size; j++) {
tmp[j] = i + j;
}
(*env)->SetIntArrayRegion(env, iarr, 0, size, tmp);
(*env)->SetObjectArrayElement(env, result, i, iarr);
(*env)->DeleteLocalRef(env, iarr);
}
return result;
}
newInt2DArray
(?
Java_ObjectArrayTest_initInt2DArray
)方法首先调用
JNI
函数
FindClass
来获得一个指向二维
int
数组中元素类的引用(
a reference of the element class of the two-dimensionalintarray
)。
FindClass
的“
[I
”参数是
JNI
类说明符(
class descriptor
),对应的是
Java
中的
int[]
类型。如果类加载失败,
FIndClass
返回
NULL
并抛出异常(可能由于丢失类文件或内存不足)。
然后
NewObjectArray
函数为数组分配内存,这个数组的元素类型为
intArrCls
类引用(
denoted by the intArrCls class reference
)。
NewObjectArray
只负责分配第一维的尺寸(
acllocate the fiest dimension
),我们仍然有一个剩余的任务,那就是填充数组元素构成第二维。
Java
虚拟机没有特别的数据结构供多维数组使用。二维数组是简单的“数组的数组”(
array of arrays
)。
创建第二维的代码相当简单。
NewIntArray
为每个数组元素分配内存,
SetIntArrayRegion
复制
tmp[]
缓冲区中内容放到新的一维数组中。完成对
SetObjectArrayElement
的调用之后,第
i
个一维数组的第
j
个元素值为
i+j
。
运行
ObjectArrayTest.main
方法产生如下输出:
0 1 2
1 2 3
2 3 4
在循环结束时调用的
DeleteLocalRef
确保虚拟机不会由于持有像
iarr
这样的
JNI
引用。
5.2.1
节当中详细解释了什么时候以及为什么需要调用
DeleteLocalRef
。