JNI学习笔记(四)——基础类型、Strings和数组

由于java编程语言和C、C++的数据类型不一致,所以在JNI和native代码直接数据类型的映射就成了问题。这里将学习java编程语言和native代码之间的类型如何转换。



一个简单的native方法


我们在java中实现这样一个类,保存为Prompt.java:

class 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");
	}
	
	/* native method that prints a prompt and reads a line */
	public native String getLine(String prompt);
}

回顾一下上节中讲的使用JNI的步骤:

首先用javac Prompt.java生成Prompt.class,然后用javah -jni Prompt自动生成Prompt.h。在头文件Prompt.h中我们将看到:

/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class Prompt */

#ifndef _Included_Prompt
#define _Included_Prompt
#ifdef __cplusplus
extern "C" {
#endif
/*
 * Class:     Prompt
 * Method:    getLine
 * Signature: (Ljava/lang/String;)Ljava/lang/String;
 */
JNIEXPORT jstring JNICALL Java_Prompt_getLine
  (JNIEnv *, jobject, jstring);

#ifdef __cplusplus
}
#endif
#endif


在上一节中我们讲过,JNIEXPORT宏和JNICALL的功能:

它们保证了该方法从native库中被导出,并且以正确的调用方式生成该函数(其实主要是指参数入栈方式)。

不知大家注意到了signature没有,

(Ljava/lang/String;)Ljava/lang/String;
它指示了native方法的参数和返回值类型:(Ljava/lang/String;)表示该方法在java语言中的的参数是String。括号后的Ljava/lang/String表示该方法在java语言中的返回值是String

根据我们在上一节中讲的,几遍没有java代码,我们也可以从这个头文件中看出:这个native方法在java中的定义:

class Prompt {
	xxx native String getLine(String);
}


native方法的参数

如上所述,可以发现每个native方法在从java转成C、C++格式的方法时,会增加额外的两个参数:

1)第一个参数JNIEnv *:它指向一个包含了函数表的指针的地址,每个指针都指向了一个JNI函数。native方法经常通过JNI方法来访问一个在java VM中的数据结构。下图是JNIEnv接口指针:


2)第二个参数,根据native方法是一个static方法还是一个实例方法而不同的。如果是一个实例方法,它就是调用该方法的对象的引用,和C++中的this指针类似(此时类型为jobject)。如果是一个static方法,那它就对应着包含该方法的类(此时类型为jclass)。在本例中,Java_Prompt_getLine方法,是一个实例方法,所以这个参数是对象的自己的引用。



类型映射


在native方法声明(java中的声明)中的参数类型,在native编程语言中有着对应的类型。JNI定义了一组和java编程语言对应的C、C++类型。

大家知道,在java编程语言中有两种类型:基础类型(例如:int, float, char...)以及引用类型(例如:类、实例、数组)。在java编程语言中,字符串是java.lang.String类的实例。

JNI对待基础类型和引用类型是不同的。基础类型的映射是简单、直接的。例如java语言中的int映射到C、C++的jint(定义在jni.h中,是一个有符号的32为整数),而java中的float对应于C++的jfloat(定义在jni.h中,是一个32为浮点型),下表是JNI和java的类型对应:
java语言类型native类型描述说明
booleanjbooleanunsigned 8 bits
bytejbytesigned 8 bits
charjcharunsigned 16 bits
shortjshortsigned 16 bits
intjintsigned 32 bits
longjlongsigned 64 bits
floatjfloat32bits
doublejdouble64 bits

此外还有一个jsize整型,被用来描述主要的索引和大小typedef jint jsize。

JNI把对象作为透明的引用传递给native方法。不透明的引用是和java VM中和内部数据结构相关的C的指针类型。而内部数据结构真正的布局,对于开发人员来说是不可见的,被隐藏的。native代码必须通过合适的JNI函数才能操控下面的对象,这些JNI函数可以通过JNIEnv接口指针来访问。例如在JNI中对应于java.lang.String的类型是jstring。而jstring引用的确切的值和native代码是不相干的。native代码调用JNI方法(例如GetStringUTFChars())来访问一个字符串的内容。


所有的JNI引用都有jobject类型。为了便捷和类型安全,JNI定义了一组引用类型,它们从概念上讲,是jobject的子类型。(A是B的一个子类型,那么A的每个实例自然也是B的一个实例)。这些子类型相当于java语言中常用的引用类型。例如(jstring表示字符串,jobjectArray表示一个对象数组)。以下是JNI引用类型和它的子类型关系的完整列表:



在C语言中,所有的其它的引用类型都被定义为同样的对象。例如

typedef jobject jclass;

在C++中,JNI引入一组虚类,来表达各个引用类型之间的子类型关系:

class _jobject {};
class _jclass : public _jobject {};
class _jthrowable : public _jobject {};

class _jstring : public _jobject {};
class _jarray : public _jobject {};
class _jbooleanArray : public _jarray {};
class _jbyteArray : public _jarray {};
class _jcharArray : public _jarray {};
class _jshortArray : public _jarray {};
class _jintArray : public _jarray {};
class _jlongArray : public _jarray {};
class _jfloatArray : public _jarray {};
class _jdoubleArray : public _jarray {};
class _jobjectArray : public _jarray {};

typedef _jobject *jobject;
typedef _jclass *jclass;
typedef _jthrowable *jthrowable;
typedef _jstring *jstring;
typedef _jarray *jarray;
typedef _jbooleanArray *jbooleanArray;
typedef _jbyteArray *jbyteArray;
typedef _jcharArray *jcharArray;
typedef _jshortArray *jshortArray;
typedef _jintArray *jintArray;
typedef _jlongArray *jlongArray;
typedef _jfloatArray *jfloatArray;
typedef _jdoubleArray *jdoubleArray;
typedef _jobjectArray *jobjectArray;



访问字符串


如上所说的,对于引用类型,native代码并不能直接访问,jstring表示java VM中字符串,并且和普通的C字符串是不一样的,在native代码中不能像使用普通C字符串一样使用它们。如下代码,不但达不到预期的效果,而且还有可能导致java VM当机。
JNIEXPORT jstring JNICALL Java_Prompt_getLine(JNIEnv *penv, jobject obj, jstring strPrompt)
{
	/*error: incorrect use of jstring as a char* pointer */
	printf("%s", strPrompt);
	...
}


转换到native字符串


由于上诉原因,在native方法的代码中,必须使用一个合适的JNI接口函数将jstring对象转换C、C++字符串。JNI支持从Unicode和向Unicode转换,同时也支持从UTF-8和向UTF-8转换。Unicode字符串用16bit值表示一个字符,而UTF-8用一个编码方案,它向上兼容7-bit的ASCII字符串。UTF-8像一个NULL结尾的C字符串,即便它们包含了非ASCII字符。所有7-bit的ASCII字符的值在1~127之间,而在UTF-8中也是这些值。一个字节的最高位被设置了,标志了多字节编码的16-bit Unicode值的开始。

Java_Prompt_getLine函数调用JNI函数GetStringUTFChars()来读取字符串的内容。它将jstring 引用(在java VM 中,一般是Unicode序列)转换为C字符串(一般以UTF-8格式代表)。如果能够确定原始字符串只包含7-bit的ASCII字符,你可以将转换后的字符串传给普通的C库的函数,例如printf。(在后面的章节,我们将介绍如何处理非ASCII字符串)。下面是Java_Prompt_getLine的实现(C++):
JNIEXPORT jstring JNICALL Java_Prompt_getLine(JNIEnv *penv, jobject obj, jstring strPrompt)
{
    char buf[128];
    const jbyte *str;
    str = penv->GetStringUTFChars(prompt, NULL);
    if (str == NULL) {
        return NULL; /* OutOfMemoryError already thrown */
    }
    printf("%s", str);
    penv->ReleaseStringUTFChars(prompt, str);
    /* We assume here that the user does not type more than
     * 127 characters */
    scanf("%s", buf);
    return penv->NewStringUTF(buf);
}


GetStringUTFChars()函数的返回值是指针,需要被检查。这是因为为保存UTF-8字符串,java VM需要分配内存资源,这样就有可能分配失败。当失败时GetStringUTFChars()返回NULL,并且抛出一个OutOfMemoryError异常。(在后面的章节中,我们会提到,JNI的异常抛出和java语言中的异常抛出是不一样的)。通过JNI抛出的关起异常,不会自动改变native C代码的控制流程。而是,我们需要一个显示的return语句,来避免该函数中程序继续运行。在Java_Prompt_getLine返回之后,这个异常会被抛送到Prompt.main——native方法:Prompt.getLine的调用者。


释放native字符串资源


当你的native代码完成了使用通过GetStringUTFChars()获得的UTF-8字符串,它调用ReleaseStringUTFChars()。调用ReleaseStringUTFChars(),意味着native方法不再使用该UTF-8字符串,这样被该UTF-8字符串占用的内存就可以被释放了。调用ReleaseStringUTFChars()失败,会导致一个内存泄露,并最重导致内存衰竭。


构造新的字符串


可以在native方法里,通过调用JNI函数:NewStringUTF,构造一个新的java.lang.String实例。这个函数利用一个UTF-8格式的C字符串来构造一个java.lang.String实例。新构造的java.lang.String实例,它是一个Unicode字符串序列,该序列和给定的UTF-8格式的C字符串一致。

如果java VM不能够为新的java.lang.String实例分配内存,NewStrigUTF抛出OutOfMemoryError异常,并且返回NULL,并且该异常会被抛送到Prompt.main方法中(native方法的调用者)。


其他JNI字符串函数


JNI在GetStringUTFChars、ReleaseStringUTFChars、NewStringUTF函数之外,还支持许多其他字符串相关的函数。

GetStringChars、ReleaseStringChars获取Unicode格式的字符串,当系统支持Unicode的时候,这个些函数非常有用。


UTF-8字符串,通常以‘\0’字符结尾,而Unicode字符串却不是。为了获得一个jstring引用中的Unicode字符的个数,JNI开发人员可以调用JNI函数GetStringLength。为了得知需要多少字节来保持一个UTF-8格式的jstring,开发人员可以在GetStringUTFChars的返回中调用strlen, 或者直接调用JNI函数GetStringUTFLength。


注意:

不管如何,当不再继续使用通过GetStringChars、GetStringUTFChars获取到的字符串时,需要调用ReleaseStringChars、ReleaseStringUTFChars来释放分配的内存资源。



Java2 JDK1.2中新的JNI字符串函数


为了增加java VM返回直接指向java.lang.String实例中的字符的指针的可能性,java2 JDK1.2引入了一对新的函数:GetStringCritical、ReleaseStringCritical。表面上它和GetStringChars、ReleaseStringChars相似(如果可能,它们都返回指向字符的指针,否则都是一个副本),然而,如何使用这对函数有很大的限制。

必须把在这对函数之间的代码当中临界区。在一个临界区中,native代码必须不能任意的调用JNI函数,或者或者不能调用任何一个可能会导致当前线程被阻塞,并且等待在java VM中的其它线程运行的native函数。例如,当前线程必须不能等待一个输入的I/O流(该流由其它线程写入)。

这些严格的限制使VM在native代码持有一个通过GetStringCritica函数获取到的直接执行字符串元素的指针时停止垃圾回收。当垃圾回收不被允许时,任何其它触发垃圾回收的线程都会被阻塞。所以在GetStringCritical和ReleaseStringCritical之间的native代码,必须不能引起阻塞调用,或者在java VM中分配一个新的对象,否则,VM可能会死锁。

当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);
在GetStringCritical和ReleaseStringcritical之间不允许其它JNI函数调用,唯一可以在其中调用的JNI函数是它们自己。


另外增加的JNI函数是:GetStringRegion和GetStringUTFRegion,它们将字符串元素复制到一个预先分配好了的缓冲中。所以,Prompt.getLine方法也可以这样实现:

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字符计。这个方式,在某些程度上比GetStringUTFChars简单,因为GteStringUTFRegion没有内存分配,我们不需要检查out-of-memory条件。



JNI字符串函数的汇总


JNI函数描述起始
GetStringChars
ReleaseStringChars
获取或释放一个指向Unicode格式字符串内容的指针。
可能返回该字符串的副本。
JDK1.1
GetStringUTFChars
ReleaseStringUTFChars
获取或释放一个指向UTF-8格式字符串内容的指针。
可能返回该字符串的副本。
JDK1.1
GetStringLength返回字符串中Unicode字符的格式JDK1.1
GetStringUTFLength返回保持一个UTF-8格式的字符串所需要的字节数JDK1.1
NewString创建一个java.lang.String实例,它包含和给定Unicode C字符串相同的字符序列JDK1.1
NewStringUTF创建一个java.lang.String实例,它包含和给定UTF-8 C字符串相同的字符序列JDK1.1
GetStringCritical
ReleaseStringCritical
获取一个指向Unicode格式字符串内容的指针。可能返回一个字符串的副本。
native代码在Get/ReleaeStringCritical调用之间必须不能阻塞。
Java2
JDK1.2
GetStringRegion
setStringRegion
将一个字符串的内容拷贝至或者到一个预先分配好了的C缓冲区中。(以Unicode格式)Java2
JDK1.2
GetStringUTFRegion
setStringUTFRegion
将一个字符串的内容拷贝至或者到一个预先分配好了的C缓冲区中。(以UTF-8格式)Java2
JDK1.2


在JNI字符串函数中选择一个合适的函数

既然有这么多函数可以选择,那么应该选择哪些函数来访问字符串呢?且依照下图所示:




访问数组


JNI对待基础类型数组和对象数组是不同的。例如在java语言中:

int[] iarr;
float[] farr;
Object[] oarr;
int[][] arr2;
iarr和farr是基础数组,而oarr和arr2确实对象数组。


在一个native方法中访问基础数组,要求使用那些和访问字符串的函数类似的JNI函数。例如下例:

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");
    }
}


在native语言(C、C++)中访问数组


在JNI中,数组被jarrary引用类型以及它的子类型(如jintArray)表示。就如jstring不是一个C的字符串类型一样,jarray同样也不是一个C的数组类型。jarrary在Java_IntArray_sumArray native 方法的实现中,也不能直接访问jarray引用。如下代码,是非法的:
/* 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;
}


访问基础类型的数组


上一例中使用了GetIntArrayRegion函数来访问整型数组,并且把它的所有元素复制到一个C缓冲区中。第三个参数0,是指起始索引,第四个参数10是指需要被复制的元素的个数。只要这些元素被复制到了C缓冲区,就可以在native代码中直接访问它们了。

JNI支持一个对应的函数来运行native代码修改该数组中的元素(setIntArrayRegion)。其它基础类型的数组也同样支持该功能。

JNI支持Get<Type>ArraryElements,Release<Type>ArrayElement家族,它们运行native代码获取一个直接指向基础类型数组元素的指针。由于垃圾回收可能不支持寄存,所有VM可能返指向原数组副本的指针。这样,我们可以重写Java_IntArray_sumArray方法:
JNIEXPORT jint JNICALL
Java_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 JDK1.2引入了GetPrimitiveArrayCritical和ReleasePrimitiveArrayCritical函数。它们的用户和GetStringCritical、RelaseStringCritical方法一样。


JNI基础数组函数的汇总

JNI函数描述起始
Get<Type>ArrayRegion
Set<Type>ArrayRegion
复制基础数组的内容到预先分配 好了的C缓冲区,
或者从C缓冲区复制内容到基础数组。
JDK1.1
Get<Type>ArrayElements
Release<Type>ArrayElements
获取一个指向基础数组内容的指针,
该指针指向的可能是原数组的一个副本。
JDK1.1
GetArrayLength返回数组元素的个数JDK1.1
New<Type>Array创建一个给定长度的数组JDK1.1
GetPrimitiveArrayCritical
ReleasePrimitiveArratCritical
获取或者释放一个指向基础数组内容的指针,
该指针可能指向基础数组的一个副本。
JDK1.2


在各个JNI基础数组函数中选择一个合适的函数





访问对象数组


JNI提供了一对单独的函数来访问对象数组:GetObjectArrayElement返回一个位于给定索引的元素,而SetObjectArrayElement则是更新给定索引上的元素。和基础类型数组不一样的是,你不能一次获取所有的对象元素,或者一次复制多个对象元素。

Strings和数组都是引用类型。你可以使用上述两个函数来访问字符串的数组和数组的数组。如下面例子,创建了一个二维数组,并且打印其中的内容:
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方法initIntDArray创建一个给定大小的二维数组,该方法,分配并且初始化该二维数组:
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;
}

其中调用JNI函数:FindClass来获得一个二维int数组的元素的类的引用。“[I”参数是JNI累的描述符,它对应着java语言中的int[ ]类型。FindClass在加载失败时,会返回NULL,并且抛出一个异常。


NewObjectArray分配了一个数组,它的元素的类型由iniArrCls引用表示。到此,NewObjectArray只是分配了第一维,我们需要填满它的第二维。java VM对于多为数组,没有特定的数据结构。一个二维的数组,其实就是一个数组的数组(以此类推)。


创建第二维的代码非常直接、简单。函数分配独立的数组元素,并用SetIntArrayRegion复制tmp[ ]临时缓冲区的内容到新分配的一维数组中。这之后,数组i行j列的的元素的值被设置为i+j。于是将输出:
 0 1 2
 1 2 3
 2 3 4


DeleteLocalRef在循环的最后被调用,这保证了VM不会耗尽内存(用来保持JNI引用,例如iarr)。在以后的章节中会解释它。














  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值