java nativearray_JNI学习笔记(四)——基础类型、Strings和数组 | 学步园

由于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

/* 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接口指针:

0_1313642530ypgp.gif

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类型

描述说明

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

此外还有一个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引用类型和它的子类型关系的完整列表:

0_131364783965ii.gif

在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字符串函数中选择一个合适的函数

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

0_1313739546819V.gif

访问数组

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支持GetArraryElements,ReleaseArrayElement家族,它们运行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函数

描述

起始

GetArrayRegion

SetArrayRegion

复制基础数组的内容到预先分配 好了的C缓冲区,

或者从C缓冲区复制内容到基础数组。

JDK1.1

GetArrayElements

ReleaseArrayElements

获取一个指向基础数组内容的指针,

该指针指向的可能是原数组的一个副本。

JDK1.1

GetArrayLength

返回数组元素的个数

JDK1.1

NewArray

创建一个给定长度的数组

JDK1.1

GetPrimitiveArrayCritical

ReleasePrimitiveArratCritical

获取或者释放一个指向基础数组内容的指针,

该指针可能指向基础数组的一个副本。

JDK1.2

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

0_1313744259O9o3.gif

访问对象数组

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、付费专栏及课程。

余额充值