由于java编程语言和C、C++的数据类型不一致,所以在JNI和native代码直接数据类型的映射就成了问题。这里将学习java编程语言和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
它们保证了该方法从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方法在从java转成C、C++格式的方法时,会增加额外的两个参数:
1)第一个参数JNIEnv *:它指向一个包含了函数表的指针的地址,每个指针都指向了一个JNI函数。native方法经常通过JNI方法来访问一个在java VM中的数据结构。下图是JNIEnv接口指针:
2)第二个参数,根据native方法是一个static方法还是一个实例方法而不同的。如果是一个实例方法,它就是调用该方法的对象的引用,和C++中的this指针类似(此时类型为jobject)。如果是一个static方法,那它就对应着包含该方法的类(此时类型为jclass)。在本例中,Java_Prompt_getLine方法,是一个实例方法,所以这个参数是对象的自己的引用。
java语言类型 | native类型 | 描述说明 |
boolean | jboolean | unsigned 8 bits |
byte | jbyte | signed 8 bits |
char | jchar | unsigned 16 bits |
short | jshort | signed 16 bits |
int | jint | signed 32 bits |
long | jlong | signed 64 bits |
float | jfloat | 32bits |
double | jdouble | 64 bits |
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;
JNIEXPORT jstring JNICALL Java_Prompt_getLine(JNIEnv *penv, jobject obj, jstring strPrompt) { /*error: incorrect use of jstring as a char* pointer */ printf("%s", strPrompt); ... }
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); }
UTF-8字符串,通常以‘\0’字符结尾,而Unicode字符串却不是。为了获得一个jstring引用中的Unicode字符的个数,JNI开发人员可以调用JNI函数GetStringLength。为了得知需要多少字节来保持一个UTF-8格式的jstring,开发人员可以在GetStringUTFChars的返回中调用strlen, 或者直接调用JNI函数GetStringUTFLength。
注意:
不管如何,当不再继续使用通过GetStringChars、GetStringUTFChars获取到的字符串时,需要调用ReleaseStringChars、ReleaseStringUTFChars来释放分配的内存资源。
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); }
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 |
既然有这么多函数可以选择,那么应该选择哪些函数来访问字符串呢?且依照下图所示:
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"); } }
/* 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]; } }
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; }
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; }
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 |
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"); } }
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; }
NewObjectArray分配了一个数组,它的元素的类型由iniArrCls引用表示。到此,NewObjectArray只是分配了第一维,我们需要填满它的第二维。java VM对于多为数组,没有特定的数据结构。一个二维的数组,其实就是一个数组的数组(以此类推)。
创建第二维的代码非常直接、简单。函数分配独立的数组元素,并用SetIntArrayRegion复制tmp[ ]临时缓冲区的内容到新分配的一维数组中。这之后,数组i行j列的的元素的值被设置为i+j。于是将输出:
0 1 2
1 2 3
2 3 4
DeleteLocalRef在循环的最后被调用,这保证了VM不会耗尽内存(用来保持JNI引用,例如iarr)。在以后的章节中会解释它。