Sun-JNI文档系列之四——第三章:基本类型、字符串类型与数组类型

           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
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值