Java™ Native Interface (JNI)【读书笔记3:Basic Types, Strings, and Arrays】

来源:互联网 发布:超市会员管理系统源码 编辑:程序博客网 时间:2024/06/10 08:22
通常开发者在做应用代码与本地代码接口时候都会考虑到一个问题:如何将java语言的数据类型与本地编程语言的数据类型进行mapping。

在之前的“hello world”例子里面,我们没有pass任何参数给本地方法,也没有让本地方法回传任何返回值。

 We will defer the full treatment of arbitrary objects to the next chapter, where we will explain how the native code can access fields and make method calls.

但是在实际应用中,大多需要进行数据的传递,无论是从应用到本地方法,还是本地方法回传数据给应用。

下面我们进行描述如何进行java code与本地实现code进行数据交互。我们先从主要的数据类型开始,如 strings arrays。

后面我再进行更加全面的任意数据对象。

先看一些简单的例子:

class Prompt {// native method that prints a prompt and reads a lineprivate 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");}}

C Prototype for Implementing the Native Method

JNIEXPORT jstring JNICALLJava_Prompt_getLine(JNIEnv *env, jobject this, jstring prompt);

注意本地方法的名字格式“Java_” prefix, the class name, and the method name

注意在这里理清一些术语:

本地方法:java里面申明的。

本地方法实现:C 形态的、C里面定义的JNI call function

the native method implementation such as Java_Prompt_getLine accepts two standard parameters, in addition to the arguments declared in the native method。

即,在本地C实现的方法里面除了包含两个标准参数,同时还要有java本地方法里面定义的一些参数。

先解释前面的两个标准参数

1、第一个参数,JNIEnv 的接口指针,指向一个函数列表,这个函数列表包含许多函数入口。每个函数入口指向一个JNI接口函数。

java本地方法通过这些JNI函数来去访问虚拟机里面的数据结构。如下图表述:

The first parameter, the JNIEnv interface pointer, points to a location that contains a pointer to a function table. Each entry
in the function table points to a JNI function. Native methods always access data
structures in the Java virtual machine through one of the JNI functions.


2、第二个参数,取决于本地方法是静态还是实例方法。
当本地方法作为一个实例方法时,第二个参数相当于对象本身,即this.

当本地方法作为一个静态方法时,指向所在类. 

在本例中,Java_Prompt_getLine是一个本地实例方法实现,所以jobject 指向对象本身

The second argument differs depending on whether the native method is a static or an instance method. The second argument to an instance native method is a reference to the object on which the method is invoked, similar to the this pointer in C++. The second argument to a static native method is a reference to the class in which the method is defined. Our example, Java_Prompt_getLine, implements an instance native method. Thus the jobject parameter is a referenceto the object itself.


Mapping of Type:

There are two kinds of types in the Java programming language: 

primitive types such as int, float, and char, and 

reference types such as classes, instances, and arrays. 

In the Java programming language, strings are instances of the java.lang.String class.

The mapping of primitive types is straightforward。比如如下对应:



相比基本类型,对象类型的传递要复杂很多。 Java层对象作为opaque references(指针)传递到JNI层。 Opaque references是一种C的指针类型,它指向JavaVM内部数据结构。使用这种指针的目的是:不希望JNI用户了解JavaVM内部数据结构。对Opaque reference所指结构的操作,都要通过JNI方法进行. 比如,"java.lang.String"对象,JNI层对应的类型为jstring,对该opaque reference的操作要通过JNIEnv->GetStringUTFChars进行。

The native code must manipulate the underlying objects via the appropriate JNI functions, which are available through the JNIEnv interface pointer. 

For example, the corresponding JNI type for java.lang.String is jstring. The exact value of a jstring reference is irrelevant to the native code.The native code calls JNI functions such as GetStringUTFChars to access the contents of a string.

一定要按这种原则编程,千万不要为了效率或容易的取到某个值,绕过JNI,直接操作opaque reference.
JNI是一套完善接口,所有需求都能满足。
在JNI中对象的基类即为jobject. 为方便起见,还定义了jstring,jclass, jobjectArray等结构,他们都继承自jobject。

HOW TO Accessing Strings

The jstring type represents strings in the Java virtual machine, and is different from the regular C string type (a pointer to characters, char *). You cannot use a jstring as a normal C string. The following code,if run, would not produce the desired results. In fact, it will most likely crash the Java virtual machine.

JNIEXPORT jstring JNICALLJava_Prompt_getLine(JNIEnv *env, jobject obj, jstring prompt){/* ERROR: incorrect use of jstring as a char* pointer */printf("%s", prompt);...}

Converting to Native Strings
Your native method code must use the appropriate JNI functions to convert jstring objects to C/C++ strings. The JNI supports conversion both to and from Unicode and UTF-8 strings.

使用对应的JNI函数把jstring转成C/C++字串。JNI支持Unicode/UTF-8字符编码互转。Unicode以16-bits值编码;UTF-8是一种以字节为单位变长格式的字符编码,并与7-bits ASCII码兼容。UTF-8字串与C字串一样,以NULL('\0')做结束符, 当UTF-8包含非ASCII
码字符时,以'\0'做结束符的规则不变。7-bit ASCII字符的取值范围在1-127之间,这些字符的值域与UTF-8中相同。当最高位被设置时,表示多字节编码。

如下,调用GetStringUTFChars,把一个Unicode字串转成UTF-8格式字串,如果你确定字串只包含7-bit ASCII字符。这个字串可以使用C库中的相关函数,如printf.

如何操作non-ASCII字符,后面有介绍。

JNIEXPORT jstring JNICALLJava_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);}

1、记得检测GetStringUTFChars的返回值,因为调用该函数会有内存分配操作,失败后,该函数返回NULL,并抛OutOfMemoryError异常。如何处理异常,后面会有介绍。

JNI处理异常,不同于Java中的try...catch。在JNI中,发生异常,不会改变代码执行轨迹,所以,当返回NULL,要及时返回,或马上处理异常。

2、调用ReleaseStringUTFChars释放GetStringUTFChars中分配的内存(Unicode -> UTF-8转换的原因)。

3、使用JNIEnv->NewStringUTF构造java.lang.String;如果此时没有足够的内存,NewStringUTF将抛OutOfMemoryError异常,同时返回NULL。

(1)GetStringUTFChars可以把一个 jstring指针(指向JVM内部的Unicode字符序列)转化成一个UTF-8格式的C 字符串。
(2)从GetStringUTFChars 中获取的UTF-8字符串在本地代码中使用完毕后,要使用ReleaseStringUTFChars 告诉 JVM 这个 UTF-8 字符串不会被使用了,因为这个UTF-8字符串占用的内存会被回收。
(3)JNI 函数 NewStringUTF 在本地方法中创建一个新的java.lang.String字符串对象.这个新创建的字符串对象拥有一个与给定的UTF-8编码的C类型字符串内容相同的 Unicode 编码字符串。


Other JNI String Functions

GetStringChars and ReleaseStringChars obtain string characters represented in the Unicode format. These functions are useful when, for example, the operating system supports Unicode as the native string format.
UTF-8 strings are always terminated with the ‘\0’ character, whereas Unicode strings are not. To find out the number of Unicode characters in a jstring reference, JNI programmers can call GetStringLength. To find out how many bytes are needed to represent a jstring in the UTF-8 format, JNI programmers can either call the ANSI C function strlen on the result of GetStringUTFChars, or call the JNI function GetStringUTFLength on the jstring reference directly.

从java程序中传过去的String对象在本地方法中对应的是jstring类型,jstring类型和c中的char*不同,所以如果你直接当做char*使用的话,就会出错。因此在使用之前需要将jstring转换成为c/c++中的char*,这里使用JNIEnv的方法转换.

下面是访问String的一些方法:
GetStringUTFChars将jstring转换成为UTF-8格式的char*
GetStringChars将jstring转换成为Unicode格式的char*
ReleaseStringUTFChars释放指向UTF-8格式的char*的指针
ReleaseStringChars释放指向Unicode格式的char*的指针
NewStringUTF创建一个UTF-8格式的String对象
NewString创建一个Unicode格式的String对象
GetStringUTFLengt获取 UTF-8格式的char*的长度 
GetStringLength获取Unicode格式的char*的长 度



UTF-8 字符串以’\0’结尾,而 Unicode 字符串不是。
如果一个jstring指向一个 UTF-8编码的字符串,为了得到这个字符串的字节长度,可以调用标准 C 函数 strlen,当然也可以用GetStringUTFLength

GetStringChars 和 GetStringUTFChars 函数中的第三个参数需要更进一步的解释:
const jchar * GetStringChars(JNIEnv *env, jstring str, jboolean *isCopy);

当从 JNI 函数 GetStringChars 中返回得到字符串B时,

如果B是原始字符串java.lang.String 的拷贝,则isCopy被赋值为 JNI_TRUE。
如果B和原始字符串指向的是JVM中的同一份数据,则 isCopy被赋值为 JNI_FALSE。
当 isCopy值为JNI_FALSE时,本地代码决不能修改字符串的内容,否则JVM中的原始字符串也会被修改,这会打破 JAVA语言中字符串不可变的规则。
通常,因为你不必关心 JVM 是否会返回原始字符串的拷贝,你只需要为 isCopy传递NULL作为参数。


GetStringCritical:是为了增加直接传回指向java字符串的指针的可能性(而不是拷贝),jdk1.2出来了新的函数:GetStringCritical/ReleaseStringCritical

Const jchar* GetStringCritical(jstring str, jboolean* copied)

Void realeaseStringCritical(jstring jstr, const jchar* str);

GetStringCritical/RealeaseStringCritical之间是一个关键区。在这关键区之中绝对不能呼叫JNI的其他函数和会造成当前线程中断或是会让当前线程等待的任何本地代码,否则将造成关键区代码执行区间垃圾回收器停止运作,任何触发垃圾回收器的线程也会暂停。其他的触发垃圾回收器的线程不能前进直到当前线程结束而激活垃圾回收器。

在关键区中千万不要出现中断操作,或是在jvm中分配任何新对象。否则会造成jvm死锁。

虽说这个函数会增加直接传回指向java字符串的指针的可能性,不过还是会根据情况传回拷贝过的字符串。

不支持GetStringUTFCritical,没有这样的函数。由于java字符串用的是UTF16,要转成UTF8编码的字符串始终需要进行一个拷贝。所以没有这样的函数。

It is safe to overlap multiple pairs of GetStringCritical and Release-StringCritical functions. For example: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);

GetStringCritical因VM实现的原因,会涉及内存操作,所以我们需要检查返回指. 比如,对于java.lang.String来说,VM内部并不是连续存储的,所以GetStringCritical要返回一个连续的字符数组,必然要有内存操作


First a correction: according to the specs, the GetXxxCritical() methods may disable garbage collection.

So what's the benefit of the Critical function,

The advantage of using the GetXxxCritical() methods is that they are more likely to return a pointer to the original data rather than a copy of the data. This means that they are likely to be faster. Of course, this comes at a cost in terms of the documented restrictions / caveats.

... and is it synonymous with disabling garbage collection while you access JVM managed memory?

Well no; see my correction. The methods may disable garbage collection, or they may not. It will depend on how your platform implements the JNI interfaces, and possibly on other thingssuch as JVM garbage collector options.

所以,GetXxxCritical() 更多是返回一个指向java string的jchar指针,先比copy数据会快一些。


GetStringRegion/GetStringUTFRegion

这个函数的动作,是把java字符串的内容直接拷贝到c/c++的字符数组中。

在呼叫这个函数之前必须有一个c/c++分配出来的字符串,然后传入到这个函数中进行字符串的拷贝由于c/c++中分配内存开销相对小,而且java中的String内容拷贝的开销可以忽略,更好的一点是此函数不分配内存,不会抛出OutOfMemoeryError异常。

//拷贝java字符串并以UTF-8编码传入buffer.

GetStringUTFRegion(String str, jsize start, jsize len, char* buffer);

//拷贝java字符串并以UTF-16编码传入buffer

GetStringRegion(jstring str, jsize start, jsize len, jchar* buffer);


JNIEXPORT jstring JNICALLJava_Prompt_getLine(JNIEnv *env, jobject obj, jstring prompt){/* assume the prompt string and user input has less than 128characters */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);}

其他的字符串函数:

.jstring NewString(const jchar* str, jszie len); 宽字符串(c/c++中的普通的字符串)

.jstring NewStringUTF(const char* str);

.jsize GetStringLength(jstring str);

.jsize GetStringUTFLength(jstring str);


总结一下JNI string function


使用策略


如果你使用JDK 1.1 或 JDK 1.2,你只能使用Get/ReleaseStringChars和Get/ReleaseStringUTFChars。
对于小尺寸字串的操作,首选Get/SetStringRegion和Get/SetStringUTFRegion,因为栈上空间分配,开销要小的多;而且没有内存分配,就不会有out-of-memory exception。如果你要操作一个字串的子集,本套函数的starting index和length正合要求。
GetStringCritical必须非常小心使用。你必须确保不分配新对象和任何阻塞系统的操作,以避免发生死锁。如下,因调用fprintf, 该c函数要执行IO操作,所以是不安全的。

/* 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);

上述代码,不安全的原因: 当前线程执行了GetStringCritical后将禁用GC. 假设,T线程正等待从fd读取数据. 进一步假设,调用fprintf时使用的系统缓存将等待T读取完毕后设置. 我们制造了一个死锁情景:如果T在读取数据时有内存分配需求,可能使JavaVM执行GC. 而此时的GC请求将被阻塞,直到当前线程执行ReleaseStringCritical,不幸的时,这个操作必须等fprintf调用完毕后才会执行。此时,死锁发生。所以,当你调用Get/RleaseStringCritical要时刻警惕死锁。

The following code, although similar to the example above, is almost certainlydeadlock free:/* 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 is a system call that directly writes the string onto the screen.
Unless the screen display driver is also a Java application running in the same virtual machine, the DrawString function will not block indefinitely waiting for garbage collection to happen.


Accessing Arrays in C

The following C code is illegal and wouldnot produce the desired results:/* This program is illegal! */JNIEXPORT jint JNICALLJava_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 elements,as shown in the following corrected example:JNIEXPORT jint JNICALLJava_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;}


JNI支持SetIntArrayRegion允许重新设置数组一个区域的值,其他基本类型(boolean,short, 和float)也有对应的支持。
JNI支持通过Get/Release<Type>ArrayElemetns返回Java数组的一个拷贝(实现优良的VM,会返回指向Java数组的一个直接的指针,并标记该内存区域,不允许被GC)。

JNIEXPORT jint JNICALLJava_IntArray_sumArray(JNIEnv *env, jobject obj, jintArray arr){jint *carr;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返回数组元素个数。
Java 2 SDK release 1.2支持Get/ReleasePrimitiveArrayCritical,该套函数的使用原则与上述String部分相同。

使用原则,与上述String部分相同,请阅读原文或回顾前面的内容。




Accessing Arrays of Objects

对于对象数组的访问,使用Get/SetObjectArrayElement,对象数组只提供针对数组的每个元素的Get/Set,不提供类似Region的区域性操作。
如下,二维数组示例,Java部分:

The following example calls a native method to create a two-dimensionalarray of int and then prints the content of the array.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");}}



0 0
原创粉丝点击