JNI Charter3 Basic ypes,Strings,and Arrays

命令行下编译命令可参考HelloWorld

实验源代码地址 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接口指针。
JNIEnv Interface Pointer

第二个参数区别于这个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

Summary of JNI String Functions

3.2.7 Choosing among the String Functions

ChoosingAmongJNIStringFuntion.png

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

Summary of JNI Primitive Array Functions

3.3.4 Choosing among the Primitive Array Functions

Choosing among 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引用类型。

阅读更多
个人分类: JNI
上一篇抛开死丢丢,在Terminal下写一次JNI --HelloWorld
下一篇Node.js 模块的加载初探
想对作者说点什么? 我来说一句

没有更多推荐了,返回首页

关闭
关闭
关闭