实验源代码地址 C++ 实现 so库 https://github.com/DDjason/JNIGuides
当遇到有着native代码的java应用程序,大部分程序员都会问在java语言中的数据类型是怎么匹配到native编程语言例如c,c++中的数据类型。在上一章“Hello World”这个例子中,我们没有传递任何参数到native方法中,native方法也没有返回任何结果(返回NULL)。native方法只是简单地打印了一个信息并返回结束。
在练习中,大部分程序在native方法中都需要传递参数和返回结果。在本章,我们将会讨论怎样变换java代码和native代码的数据类型。我们将会从基本数据类型开始例如integers和公共的对象类型例如Strings和Arrays。对于任意类型对象,native code是怎么获得字段和产生方法返回的,我们将推迟到下一章讨论。
3.1 A Simple Native Method
让我们从一个简单的例子开始,这个例子和上一章的HelloWorld程序稍微有点不同。这个举例程序,Prompt.java 包含了一个native方法:打印一个String,等待用户的输入,然后返回用户输入的那一句。程序的源码如下:
class Prompt{
//native method that prints a promat and reads a line
private native String getLine(String promat);
public static void main(String args[]){
Prompt p = new Prompt();
String input = p.getLine("Type a line : ");
System.out.println("User type : " + input);
}
static {
System.load("");
}
}
在Prompt.java的main方法中调用了getLine这个native方法去接受用户输入。Static 静态初始化中调用System.load或者loadLibrary方法去加载
native库。
3.1.1 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 中)确保了这个方法能够被native library之外调用并且C编译器对这个方法会使用正确的编译方式。C方法中的方法名字将会以”Java_”为前缀,然后对象名,方法名这样的格式。在11.3小节中包含了更加详细的方法名格式细节。
3.1.2 Native Method Arguments
在2.4小节中简单讨论过,这个native方法实现例如Java_Prompt_getLine 接受两个标准的参数,另外,在native方法中定义的参数中,第一个参数是JNIEnv 接口指针,指向了函数表,函数表中每一项指向了一个个JNI 函数。native 方法中总是通过JNI 函数去获取JVM中的数据结构。在图3.1中描述了JNIEnv接口指针。
第二个参数区别于这个native method是一个实例方法还是一个类静态方法。如果是一个实例方法,第二个参数将会是一个相关联的对象,类似于c++中的指针,如果是静态类方法,第二个参数将会关联至这个类。在我们的例子中,Java_Prompt_getLine 实现的是一个实例方法,因此jobject参数指向的是这个对象本身。
3.1.3 Mapping of Types
在java中native方法中需要的数据类型会有相对应的数据类型再native 编程语言中。JNI定义了一组C或C++类型来匹配Java编程语言中的数据类型。
在Java编程语言中有两种数据类型: 基础数据类型比如int,float,char和引用变量比如classes,instances,arrays。在Java编程语言中Strings是一个java.lang,String类的实例。
JNI对待基础数据类型和引用变量类型是不一样的。基础数据类型的匹配是直接了当的。例如:在java中的int会直接匹配到出JNi的jint类型(定义在 jni.h 作为一个32位数据),同时,java中的float会匹配c/c++中的jfloat。
JNI 传递对象到native方法中是以一种不透明的引用方式。Opaque references是以一种C指针的形式指向JVM中的数据结构。然后这个数据结构在代码中是不可见的。本地代码必须通过JNIEnv中的一些JNI函数来操作这数据结构。例如:当传递一个Java 的String 对象对应的JNI类型是jstring。本地代码只能通过类似与GetStringUTFChars等JNI函数来获取string的值。
所有的引用类型都是jobject对象。为了方便和类型变换的安全。JNI定义了一组引用类型,它们都是jobject类型的子类型。这些子类型和java常用的数据类型相对应。例如:jstring对应string;jobject对应array of object。
3.2 Accessing Strings
在Java_prompt_getLine 方法接受一个prompt参数作为jstring类型。这jstring类型代表了JVM中的string类类型,所以会和C语言中的String类型(一个指向characters的char * 指针)有所不同。所以你不能硬jstring来当作传统的Cstring 。如下代码,如果运行,将不会得到预期的结果。事实上,它将可能造成JVM的crash。
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 Convering to Native Strings
在本地代码中,必须使用合适的JNI函数把jstring转化为C/C++字符串。JNI支持字符串再Unicode和UTF-8两种编码之间转换。Unicode字符串代表了16-bit的字符集合,UTF-8字符串使用了一种向上兼容ASCII字符串的编码协议。所有的7-bit的字符都在1~127之间,这些值再UTF-8中保持原样。一个字节如果最高位被设置了,意味着这是一个多字节字符(16-bit Unicode值)
函数Java_Prompt_getLine通过调用JNI函数GetStringUTFChars来读取字符串的内容。GetStringUTFChars可以把jstring指针(指向JVM内部的Unicode字符序列)转化成一个UTF-8的C字符串。你可以把转化后的字符串传递给常规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。因为JVM需要为新诞生的UTF-8字符串分配内存,这个操作有可能因为内存太少而失败。失败时,GetStringUTFChars会返回NULL,并抛出一个OutOgMemoryError异常。这些JNI抛出的异常与JAVA异常有所不同。JNI异常不会改变程序的执行流,因此,我们需要显示一个return 语句来跳过C函数的剩余语句。Java_Prompt_getLine函数返回后,异常会在Prompt.main这个JNI函数调用者中抛出(异常处理会在第6章讲述)。
3.2.2 Freeing Native String Resources
当你的本地代码对通过JNI函数GetStringUTFChars获取到的UTF-8字符串使用完毕后,应该调用ReleaseStringUTFChars函数。调用ReleaseStringUTFChars暗示着本地代码将不再需要这个通过GetStringUTFChars函数获取到的UTF-8字符串。因此UTF-8的字符串占据的内存将会被释放。如果调用ReleaseStringUTFChars失败,会到时可用内存溢出。这样的话会导致内存能力减弱。
3.2.3 Constructing New Strings
你能构造一个新的java.lang.string 实例在本地代码里通过调用JNI方法NewStringUTF。NewStringUTF方法将一个UTF-8的CString字符串生成一个Unicode的java.lang.string的实例。
如果虚拟机不能分配构造一个新String实例的内存。NewStringUTF将会抛出一个OutOgMenoryError异常并且返回NULL。在这个例子中,我们不需要检查这个返回值,因为在这之后程序将会立即返回。如果,NewStringUTF执行失败,OutOfMemoryError将会立即在调用者调用的地方抛出。如果成功,它将返回一个全新的String实例。
3.2.4 Other JNI String Functions
JNI 支持一些其他string相关的方法,另外,GetStringUTFChars、ReleaseStringUTFChars、NewStringUTF这些方法被介绍地早一些而已。
GetStringChars和ReleaseStringChars操作Unicode格式的字符串。这些方法有时候是十分 有效的。例如:将获取的系统支持的Unicode字符串当作本地String格式操作。
UTF-8字符串习惯以’\0’结束,然而Unicode并不是。为了得到jstring引用的Unicode的字符串长度,JNI程序可以使用GetStringLength。为了得到UTF-8格式的jstring所需要准备的内存大小,JNI程序可以调用ANSI C方法strlen来计算GetStringUTFChars返回值,或者直接用GetStringUTFLength来得到jstring对象的长度。
GetStringUTFChars和GetStringChars需要的第三个参数来附加额外的说明:
const jchar *
GetStringChars(JNIEnv *env, jstring str, jboolean *isCopy);
当JNI函数从GetStringChars中获得字符串时,当参数isCopy是TRUE,则获取字符串是JVM实例的拷贝,如果FALSE,则与原始字符串指向相同的数据结构,此时千万不能在本地代码中修改字符串,否则则破坏了JAVA语言String实例不能被修改的原则。
通常,不必关心JVM返回的是否是字符串的拷贝,我们只需要将isCopy传递NULL即可。
JVM是否将java.lang.String对象实例进行拷贝是不可预测的。程序员最好假设它进行了拷贝,而这个操作会花费时间和内存。通常而言,JVM会在heap堆上为对象分配空间。一旦一个JAVA字符串对象的指针被传递给本地代码,那么GC将不会去碰这块内存区域。
所以不要忘记调用ReleaseStringChars当你不在需要这个从GetStringChars获得的String对象。
3.2.5 New JNI String Functions in Java 2 SDK Release 1.2
×××
3.2.6 Summary of JNI String Functions
3.2.7 Choosing among the String Functions
3.3 Accessing Arrays
JNI在对待基本数据类型数组和对象数组上有所不同。基本数据数组包含的元素是一些基本数据类型例如int、boolean。对象数组包含的元素指的是引用类型比如类的实例和其他数据数组。举例:如下的代码片段是java语言的数组:
int[] iarr;
float[] farr;
Object[] oarr;
int[][] arr2;
在本地方法里获取基本数组需要于获取String类似的JNI方法。让我们看一个例子.接下来的程序包含一个sumArray的本地方法: 用于统计数组中和的值。
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 Accessing Arrays in C
Arrays are represented by the jarray reference type and its “subtypes” such as
jintArray . Just as jstring is not a C string type, neither is jarray a C array
type. You cannot implement the Java_IntArray_sumArray native method by
indirecting through a jarray reference. The following C code is illegal and would
not produce the desired results:
/* 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];
}
}
You must instead use the proper JNI functions to access primitive array ele-
ments, as shown in the following corrected example:
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 Accessing Arrays of Primitive Types
之前的例子使用GetIntArrayRegion方法将integer数组中的全部元素拷贝到c缓冲区。第三个参数是元素的开始标签,第四个参数是元素的个数。一旦,元素写入C缓冲区域,我们可以从本地代码中的得到它。之所以没有异常检查是因为为我们知道index值不会因为元素不够而IndexOverflow。
JNI支持一个相对应的SetIntArrayRegion方法,允许本地代码修改修改类型为inti的数组元素。其他基本类型的数组同样被支持
3.3.3 Summary of JNI Primitive Array Functions
3.3.4 Choosing among the Primitive Array Functions
If you need to copy to or copy from a preallocated C buffer, use the Get/
SetArrayRegio n family of functions. These functions perform bounds
checking and raise ArrayIndexOutOfBoundsException exceptions when neces-
sary. The native method implementation in Section 3.3.1 uses GetIntArray-
Region to copy 10 elements out of a jarray reference.
For small, fixed-size arrays, Get/SetArrayRegion is almost always
the preferred function because the C buffer can be allocated on the C stack very
cheaply. The overhead of copying a small number of array elements is negligible.
The Get/SetArrayRegion functions allow you to specify a starting
index and number of elements, and are thus the preferred functions if the native
code needs to access only a subset of elements in a large array.
If you do not have a preallocated C buffer, the primitive array is of undeter-
mined size and the native code does not issue blocking calls while holding the
pointer to array elements, use the Get/ReleasePrimitiveArrayCritical func-
tions in Java 2 SDK release 1.2. Just like the Get/ReleaseStringCritical func-
tions, the Get/ReleasePrimitiveArrayCritical functions must be used with
extreme care in order to avoid deadlocks.
It is always safe to use the Get/ReleaseArrayElements family of
functions. The virtual machine either returns a direct pointer to the array elements,
or returns a buffer that holds a copy of the array elements.
3.3.5 Accessing Arrays of Objects
JNI同时提供一组获取对象数组的方法。GetObjectArrayElement 通过被给的Index返回一个元素,相对应SetObjectArrayElement更新所选index的元素。与基础数据类型数组的方法不一样,你不能一次性获取所有的对象元素和同时拷贝多个元素。
Strings和arrays都是引用类型,你必须使用Get/SetObjectArrayElement来读写strings的数组或者数组的数组
接下来的例子将会调用一个本地方法,去生成一个元素是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");
}
}
静态本地方法initInt2DArray生成一个2维数组。这个本地方法将会初始化一个二维数组如下所示:
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方法首先将会调用JNI的FindClass方法去获得一个想对应的元素类型Class。FindClass返回NULL值并且抛出异常当加载class失败的时候。
之后NewObjectArray将会分配一个数组,这个数组中的元素类型将会使用intArrayCls的引用来标识。这个NewObjectArray方法只能分配出一维。JVM没有特别的数据结构来表示多维数组。二维数组只是简单的数组的数组。
确保调用DeleteLocalRef函数来循环释放空间,例如持有的JNI引用类型。