JNI官方文档翻译3-基本数据类型 字符串 数组

        在使用JNI的时候,你问的最多的问题莫过于 Java的数据类型和C/C++的数据类型怎么一对一映射。在我们的HelloWord例子当中,我们并没有传入任何参数给我们的java层print方法,native方法也并没有返回任何数据而是void,本地方法只是简单的打印一个字符串,然后就返回了。实际开发中我们都需要传入参数,返回参数,本章就会讨论如何从java层向底层传数据,以及如何从底层向java层返回数据。我们从基本数据类型 字符串 数组开始, 下一章再介绍如何传任意类型的数据,以及如何访问他们的数据和方法。


我们来看一个不同于HelloWord的例子Prompt.java :等待用户输入,再把输入的字符串返回,源代码如下:

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: ");//getLine方法返回用户输入的字符串
        System.out.println("User typed: " + input);
    }
    static {
        System.loadLibrary("Prompt");//加载动态库
    }
}
生成的Prompt.h头文件如下:

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

JNIEXPORT 和JNICALL是定义在jni.h中的宏,能够保证这个方法能在不同平台上能够被导出, 并且生成的代码满足调用约定,在windows平台上麻烦一些,当然针对生成动态库而言,建议你去看一看这两个宏的定义。

*env 指向了一个函数表(JNI API),我们要和虚拟机交互,都需要调用JNI API才能完成, 并且env是一个二级指针

参数jobject this 有时候会不一样,如果你的native方法声明为static 则这个参数就代表Promt这个类,否则代表这个类的对象this


Java数据类型包括原生数据类型和引用数据类型:

原生数据类型int float double char 等

引用数据类型就是对象 String Array

JNI将这两类数据类型区别开来,原生数据类型-> C数据类型的映射是很直白的,很好理解,例如 int -> jint    float-> jfloat

JNI将一个对象当做一个”不透明引用“ 传给底层方法 ,这个不透明引用其实是一个指针,指向虚拟机中的某个数据结构,这个数据结构对开发人员来说是不透明的。也就是说,这个数据结构的定义,你看不到,你只能通过一些JNI提供的API来和他们进行交互, 这就需要JNIEnv这个函数表了。举个例子java.lang.String 对应的JNI类型为jstring,jstring引用的字符串和我们的C代码是不相关的,明白了吗?我再解释一下:你可以把jstring想象成一个访问java.lang.String 对象的一个id,我们通过这个id能够访问到这个字符串,但是单从这个id来看,我们不知道这个字符串是什么,必须通过JNIEnv的函数才能访问到这个字符串。例如一些方法:GetStringUTFChars()


jobject 类型有一些子类型 比如 jstring(java String) jobjectArray (java对象数组), 其实这个java层是一个道理,这里简单提一下。


下面看一下如何访问字符串:

这个方法接受一个字符串参数jstring 然后返回一个jstring , jstring 和 char*还是有区别的,所以你不能把他们混淆使用,下面的代码执行会出错

JNIEXPORT jstring JNICALL
Java_Prompt_getLine(JNIEnv *env, jobject obj, jstring prompt)
{
/* ERROR: incorrect use of jstring as a char* pointer */
printf("%s", prompt);//直接把jstring当成char*来使用是错误的
...
}

jstring转成char*   :

JNI支持Unicode UTF-8 和char* 之间来回转换的函数。Unicode是2个字节,UTF-8最高达到7个字节

GetStringUTFChars方法能够将Unicode串转换成UTF-8编码的C串,如果你确定你传入的是7bit的ASCII串,那么printf会正确输出,后面讲解怎么输出非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 的返回值,如果返回NULL,则表明虚拟机分配内存失败,会抛出OutOfMemory异常。从本地方法中抛出异常和java层抛异常还是有很大区别的,java层抛异常会改变代码执行路径,发生异常的代码处后面的语句不会执行,但是在本地方法中,我们需要手动return,返回后异常才会抛出,本地方法并不会因为异常发生而改变执行路径。暂时先有个概念,后面章节会介绍。

别忘记调用ReleaseStringUTFChars,因为GetStringUTFChars 返回的指针是申请来的内存,保存我们的char*串,如果你不调用这个方法,就会产生内存泄漏。

Tip: 如果JNI方法返回的是指针,你就要考虑,是否有对应的方法来释放这个指针的内存,这或许是个好习惯。


new一个字符串:

你可以使用NewStringUTF方法来new一个java字符串,如果返回NULL说明没内存,会抛OutOfMemory异常,同上。如果成功则返回一个jstring类型的数据

你不需要担心jstring的释放问题,因为这是一个java层的String,释放的工作交给虚拟机GC吧。


其他JNI字符串函数:

GetStringChars 和 ReleaseStringChars 返回Unicode编码的C字符串,注意,是C串类型是UTF-8还是Unicode编码。

GetStringChars 有个参数isCopy ,如果设置成JNI_TRUE   那么会返回一份拷贝 JNI_FALSE的话就直接指向原始内存地址。当设置成JNI_FALSE时,你最好别去修改内存中的内容,这违背了java.lang.String 的设计原则:不能修改字符串。一般我们都传NULL,即拷贝字符串,如果传JNI_TRUE,也需要调用ReleaseStringChars,这就和GC相关了。


为了能够直接返回java层String的char*而不拷贝,java2 sdk release1.2引入了一对新的JNI API:  Get/ReleaseStringCritical,表面看来和Get/ReleaseStringChars 很像,但是实际上它的使用限制还是很多的。你可以把get release之间的代码当成一个critical reigion,特殊的区域,在这个区域里,你不许再调用JNI函数,或者能够引起当前线程阻塞的函数,比如等待IO,这个限制使得虚拟机在这个区域里禁止垃圾回收机制,也就是说,当你拿到了一个指向java层string的native char* 时,垃圾回收不会运行,会被阻塞。

native code 在这个区域不能调用block阻塞函数,也不能new java对象,否则虚拟机可能就会死锁。考虑如下情形:另一个线程触发了垃圾回收,不能向下执行(直到当前线程不阻塞,并且跳出Get/ReleaseStringCritical之间的区域启动垃圾回收), 也就是说垃圾回收被阻塞了,与此同时,当前线程因为阻塞也不能向下执行了,且垃圾回收线程也被阻塞了,它需要等待另一个线程释放锁,且这个线程也等待执行垃圾回收,这样可能就会造成死锁。

但是你可以在这个特殊区域调用Get/ReleaseStringCritical, overlap,嵌套调用,比如如下代码片段:


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 和Get/ReleasePrimitiveArrayCritical ,JNI不支持GetStringUTFCritical 和 ReleaseStringUTFCritical


Java 2 SDK release 1.2其他的函数GetStringRegion 和 GetStringUTFRegion ,这些函数会接受一个预分配的buffer作为参数,我们的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 可能会抛出StringIndexOutOfBoundsException , 如果你不检查越界情况的话, 当然我们的例子没有检查 len<=128,自己加上吧

GetStringUTFRegion比GetStringUTFChars 要简单很多,因为GetStringUTFRegion并不会分配内存,我们也不用检查out-of-memory。


JNI 有关字符串的函数概要:



这么多函数,我们如何选择呢?

也就是说:如果你的目标版本是1.1或者同时1.1,1.2,你别无选择,只有Get/ReleaseStringChars and Get/ReleaseStringUTFChars

如果你的目标版本是1.2或1.2以上,并且你希望copy字符串到你预分配的数组里,请用GetStringRegion or GetStringUTFRegion.对于固定长度的或者比较短小的字符串,这两个函数是再合适不过的了,他们的优势是他们不需要分配内存,也就不会导致out-of-memory异常,访问子串使用本方法也是合适的选择。

GetStringCritical的使用就得小心了,你不能在特殊区域内阻塞,或者调用除了上面提到的方法外的任何JNI函数,否则,就会导致系统死锁。我们用一个例子演示一下:

/* 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,我们进一步假定fprintf会等待线程T从fd中读完数据。这种情形就会死锁:如果线程T没有申请到足够的内存,那么会触发垃圾回收,但是垃圾回收被禁用了,换句话说垃圾回收被阻塞了直到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 是一个系统调用,不会阻塞,直接向屏幕输出字符串

总之,在使用Get/ReleaseStringCritical的时候你一定要格外的小心,确保这个特殊区域的代码永远不会阻塞。(少做调用即可,除非你确定,你的调用一定不阻塞)

以上就是关于JNI操作字符串的一切,下面我们讨论下一个主题,访问数组


JNI对于原生数据类型的数组,和引用类型的数组的对待方式是不一样的,原生数据类型的数组包含原生类型的数据,例如int boolean, 引用类型的数组包含引用类型的数据,比如对象的实例和其他的数组(二维数组)  例如 如下java代码片段:

int[] iarr;
float[] farr;//原生数组
Object[] oarr;//引用数组
int[][] arr2;//二维数组

通过JNI访问数组类似于通过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];
    }
}

jintArray 是jarray的子类型,我们不能通过这种方式来访问一个数组,JNI访问数组有专门的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);//拷贝到一个jint buffer里进行操作
    for (i = 0; i < 10; i++) {
        sum += buf[i];
    }
    return sum;
}

下面介绍如何访问原生数据类型数组的方法:

上个例子,我们用GetIntArrayRegion 将数据拷贝到一个C buffer里, 10是数组的长度,因为我们知道数组的长度是10,所以不会越界。JNI支持SetIntArrayRegion函数,来操作改变Int型数组中的某些数据,其他类型 float boolean 也支持,SetFloatArrayRegion.

JNI支持一族函数Get/Release<Type>ArrayElements, 重写上面的例子:

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 SDK release 1.2以后引入了Get/ReleasePrimitiveArrayCritical, 用法同字符串一样,你需要格外小心,别在这个特殊区域内做阻塞的动作,也别调用不允许调用的函数。

JNI操作数组的函数:



我们如何选择这些函数呢?



如果你需要拷贝数组到一个预分配的buffer Get/Set<Type>ArrayRegion是最好的选择,越界会抛ArrayIndexOutOfBoundsException,适合小数组,因为这涉及到在栈上分配数组,小数组代价小,并且这个函数可以拷贝子数组,subarray

如果数组大小未知,可以尝试Get/ReleasePrimitiveArrayCritical 前提是Java 2 SDK release 1.2., 并且要多加小心。

使用Get/Release<type>ArrayElements 总是安全的,并且1.1, 1.2 版本通用,要么直接返回一个指针给你,要么拷贝一份数据,然后将拷贝的数据的指针给你。


访问引用类型数组:另一对JNI函数 GetObjectArrayElement 根据给定的index返回数据 SetObjectArrayElement 同理,和原生数据类型的数组不同,你一次拿不到那么多的数据,Get/Set<Type>ArrayRegion 是不支持的。

字符串和数组都是引用类型,你可以用Get/SetObjectArrayElement 来操作字符串数组和数组的数组(二维数组).

如下代码访问一个二维数组,然后打印数组中的内容:

class ObjectArrayTest {
    private static native int[][] initInt2DArray(int size);//初始化二维数组
    public static void main(String[] args) {
        int[][] i2arr = initInt2DArray(3);//3x3大小
        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,你可能会觉得是jintArray
{
    jobjectArray result;
    int i;
    jclass intArrCls = (*env)->FindClass(env, "[I");//[I代表int[]
    if (intArrCls == NULL) {
        return NULL; /* exception thrown */
    }
<pre name="code" class="cpp">    
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;}

 

java的二维数组就是数组的数组,上面的例子很简单,初始化结束后:数组应该是这个样子

0 1 2
1 2 3
2 3 4

DeleteLocalRef调用保证了不会因为申请内存而发生out-of-memory ,后面详细介绍为什么要调用这个方法。



上一篇:JNI官方文档翻译2-Getting Started

下一篇:JNI官方文档翻译4-属性和方法的访问




评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值