Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Creating a byte[][] in C++ and returning it to Java using JNI

I have a JNI function, written in C++, that takes a byte array as input, segments it, and returns an array of byte arrays to Java.

JNIEXPORT jobjectArray JNICALL Java_class_method(JNIEnv *env, jobject o, jbyteArray dataToSegment);

On the Java side, it's something as simple as:

byte[] arg = getRandomByteArray();
Object[] retVal = x.method(arg);

Now, I'm finding the JNI part to be a bit tricky. I intend to create an array of Objects, each of which is a byte array. This is because JNI only defines a limited number of Java types. There's a jbyteArray type, and a jobjectArray type, but no jarrayOfByteArrays type.

So I create my array of Objects, each Object being initialized as a new byte[1024]:

jobjectArray retVal = env->NewObjectArray(numSegs, env->FindClass("[Ljava/lang/Object;"), env->NewByteArray(1024));

I then iterate over all the indexes in this array, doing something like:

jbyteArray tmp = (jbyteArray) env->GetObjectArrayElement(retVal, i);
env->SetByteArrayRegion(tmp, 0, size, (jbyte*) sourceBuffer);
env->SetObjectArrayElement(retVal, i, (jobject) tmp); // <--- Questionable line

and everything works great, for the most part. However, what if I wanted each byte array to be of variable length? That is, I want the array of byte arrays to be "jagged". What do I pass as the last parameter to NewObjectArray() as the initial value? I've tried passing 0 as the initial value to prevent initialization at time of jobjectArray creation, and then allocating new jbyteArray objects to pass to SetObjectArrayElement(), but this would only end up throwing ArrayStoreException every time I tried to call SetObjectArrayElement. Indeed, even assigning a new jbyteArray to the tmp object (instead of one from GetObjectArrayElement()) results in that same exception being thrown when SetObjectArrayElement() is called. Is there a reason why that last line of code would be an issue? Is it not possible to call SetObjectArrayElement() with a jbyteArray as a parameter?

It seems, upon further inspection, that that "Questionable line" isn't doing anything at all. When I comment it out, any changes made to tmp are reflected in retVal. I understand this is because the SetByteArrayRegion call is dealing with the byte array that's "in" the jobjectArray, and not a copy. That would be sufficient for me if all rows (rather, all single-dimensional byte arrays) were the same length. But they're not. How do I assign a new byte array to one of the rows in this object array?

TL;DR: With JNI, I have a jobjectArray of jbyteArrays. How do I replace one of the jbyteArrays with a new one that was created with NewByteArray? Hint: env->SetObjectArrayElement(retVal, i, (jobject) env->NewByteArray(size)); // doesn't work.

like image 479
Szymon Smakolski Avatar asked Aug 06 '13 19:08

Szymon Smakolski


1 Answers

I just successfully returned array of byte array through JNI. The initial size doesn't matter of your byte array because you replace it with a new one as you populate the jobject array:

static jbyteArray NewJavaStringBytes(JNIEnv* env, const char *src) {
    jbyteArray retVal = (*env)->NewByteArray(env, strlen(src));
    jbyte *buf = (*env)->GetByteArrayElements(env, retVal, NULL);
    strcpy(buf, src);
    printf("    NewJavaStringBytes: Created java byte array: %s.\n", buf);
    (*env)->ReleaseByteArrayElements(env, retVal, buf, 0);

    return retVal;
}

JNIEXPORT jobjectArray JNICALL Java_TestJniString_printStringArray
  (JNIEnv *env, jobject thisObj, jobjectArray jObjArr) {
    int numStr = (*env)->GetArrayLength(env, jObjArr);
    int idx = 0;
    jobjectArray strArr = NULL;
    jbyte *curStr = NULL;
   jclass arrayElemType = (*env)->FindClass(env, "[B");

    const char *retStrs[] = {"one", "two", "three", "twenty-five", "TESTING!!"};
    const int RET_LEN = sizeof(retStrs) / sizeof(char *);

    printf("Creating java object array of %d items.\n", RET_LEN);
    //Create new array of byte array
    jobjectArray testArray = (*env)->NewObjectArray(env,
                                                    RET_LEN,
                                                    arrayElemType,
                                                    (*env)->NewByteArray(env, 1) );

    for (idx = 0; idx < RET_LEN; ++idx) {   
        printf("Creating java byte array %d from str: %s.\n", idx, retStrs[idx]);
        jbyteArray str = NewJavaStringBytes(env, retStrs[idx]);
        (*env)->SetObjectArrayElement(env, testArray, idx, str);
        (*env)->DeleteLocalRef(env, str);
    }

    printf("printStringArray: Printing %d strings:\n", numStr);
    for (idx = 0; idx < numStr; ++idx) {
        strArr = (*env)->GetObjectArrayElement(env, jObjArr, idx);
        curStr = (*env)->GetByteArrayElements(env, strArr, NULL);
        printf("    %s.\n", curStr);
        (*env)->ReleaseByteArrayElements(env, (jbyteArray)strArr, curStr, 0);
    }

    (*env)->DeleteGlobalRef(env, arrayElemType);

    return testArray;
}

This example takes an array of byte arrays and returns an array of byte arrays. Note this is in C (not C++) so the jni calls are (*env)->(env, ...);. In C++ the wrappers simplify the calls. Also, NOTE that this assumes the java code adds the null terminator on the byte array version of the string before sending to the native layer. If you cannot count on that, then you must manually add the null term in the C/C++ code as Java will not do this for byte[] when converting from String.

Hope that helps.

like image 140
bduhbya Avatar answered Sep 19 '22 04:09

bduhbya