Search code examples
javajava-native-interfacenative-code

Unboxing a `jobjectArray` into a `jvalue[]` array in JNI code


I have the following Java native method declaration:

static native Object nativeCallObjectMethod
        (Object object, Method method, Object... params);

This produces the following C function declaration (note that Object... params is mapped to jobjectArray params):

JNIEXPORT jobject JNICALL Java_pkg_Cls_nativeCallObjectMethod
        (JNIEnv *env, jclass ignored, jobject obj, jobject method,
                jobjectArray params) { }

I want to call method with params as arguments:

    jmethodID methodID = (*env)->FromReflectedMethod(env, method);
    return (*env)->CallObjectMethod(env, obj, methodID, params);

However, params is of type jobjectArray, whereas the JNI methods callObjectMethod* can only take one of the following three forms:

  1. CallObjectMethod(JNIEnv *env, jobject obj, jmethodID methodID, ...)
  2. CallObjectMethodA(JNIEnv *env, jobject obj, jmethodID methodID, const jvalue *args)
  3. CallObjectMethodV(JNIEnv *env, jobject obj, jmethodID methodID, va_list args)

There is no form of this method that takes jobjectArray in the last argument position. I assume that method 2 is the one I want to call, and I realize jobjectArray elements may need unboxing to produce an array of jvalue values. I found this suggestion of how to implement this: https://stackoverflow.com/a/30961708/3950982

However, I can't believe there is no standard way to unbox a jobjectArray into a jvalue array using the JNI API. Surely there must be a built-in way to achieve this, since Java does this internally when using reflection?

How can I solve this, preferably without reinventing the wheel?


Solution

  • I wrote a method for unboxing a jobjectArray into a jvalue[] array:

    // Unbox a jobjectArray of method invocation args into a jvalue array. Returns 0 if an exception was thrown, or 1 if unboxing succeeded.
    int unbox(JNIEnv *env, jobject method, jobjectArray args, jsize num_args, jvalue* arg_jvalues) {
        // Get parameter types of method
        jclass methodClass = (*env)->GetObjectClass(env, method);
        jmethodID getParameterTypesMethodID = (*env)->GetMethodID(env, methodClass, "getParameterTypes", "()[Ljava/lang/Class;");
        jobject parameterTypes = (*env)->CallObjectMethod(env, method, getParameterTypesMethodID);
        jsize num_params = (*env)->GetArrayLength(env, parameterTypes);
    
        // Check arg arity
        if (num_args != num_params) {
            throwIllegalArgumentException(env, "Tried to invoke method with wrong number of arguments");
            return 0;
        }
        
        // Unbox args
        for (jsize i = 0; i < num_args; i++) {
            jobject paramType = (*env)->GetObjectArrayElement(env, parameterTypes, i);
            jobject arg = (*env)->GetObjectArrayElement(env, args, i);
            jclass argType = arg == NULL ? (jclass) NULL : (*env)->GetObjectClass(env, arg);
    
            if ((*env)->IsSameObject(env, paramType, int_class)) {
                if (arg == NULL) {
                    throwIllegalArgumentException(env, "Tried to unbox a null argument; expected Integer");
                    return 0;
                } else if (!(*env)->IsSameObject(env, argType, Integer_class)) {
                    throwIllegalArgumentException(env, "Tried to unbox arg of wrong type; expected Integer");
                    return 0;
                } else {
                    arg_jvalues[i].i = (*env)->CallIntMethod(env, arg, int_value_methodID);
                }
            } else if ((*env)->IsSameObject(env, paramType, long_class)) {
                if (arg == NULL) {
                    throwIllegalArgumentException(env, "Tried to unbox a null argument; expected Long");
                    return 0;
                } else if (!(*env)->IsSameObject(env, argType, Long_class)) {
                    throwIllegalArgumentException(env, "Tried to unbox arg of wrong type; expected Long");
                    return 0;
                } else {
                    arg_jvalues[i].j = (*env)->CallLongMethod(env, arg, long_value_methodID);
                }
            } else if ((*env)->IsSameObject(env, paramType, short_class)) {
                if (arg == NULL) {
                    throwIllegalArgumentException(env, "Tried to unbox a null argument; expected Short");
                    return 0;
                } else if (!(*env)->IsSameObject(env, argType, Short_class)) {
                    throwIllegalArgumentException(env, "Tried to unbox arg of wrong type; expected Short");
                    return 0;
                } else {
                    arg_jvalues[i].s = (*env)->CallShortMethod(env, arg, short_value_methodID);
                }
            } else if ((*env)->IsSameObject(env, paramType, char_class)) {
                if (arg == NULL) {
                    throwIllegalArgumentException(env, "Tried to unbox a null argument; expected Character");
                    return 0;
                } else if (!(*env)->IsSameObject(env, argType, Character_class)) {
                    throwIllegalArgumentException(env, "Tried to unbox arg of wrong type; expected Character");
                    return 0;
                } else {
                    arg_jvalues[i].c = (*env)->CallCharMethod(env, arg, char_value_methodID);
                }
            } else if ((*env)->IsSameObject(env, paramType, boolean_class)) {
                if (arg == NULL) {
                    throwIllegalArgumentException(env, "Tried to unbox a null argument; expected Boolean");
                    return 0;
                } else if (!(*env)->IsSameObject(env, argType, Boolean_class)) {
                    throwIllegalArgumentException(env, "Tried to unbox arg of wrong type; expected Boolean");
                    return 0;
                } else {
                    arg_jvalues[i].z = (*env)->CallBooleanMethod(env, arg, boolean_value_methodID);
                }
            } else if ((*env)->IsSameObject(env, paramType, byte_class)) {
                if (arg == NULL) {
                    throwIllegalArgumentException(env, "Tried to unbox a null argument; expected Byte");
                    return 0;
                } else if (!(*env)->IsSameObject(env, argType, Byte_class)) {
                    throwIllegalArgumentException(env, "Tried to unbox arg of wrong type; expected Byte");
                    return 0;
                } else {
                    arg_jvalues[i].b = (*env)->CallByteMethod(env, arg, byte_value_methodID);
                }
            } else if ((*env)->IsSameObject(env, paramType, float_class)) {
                if (arg == NULL) {
                    throwIllegalArgumentException(env, "Tried to unbox a null argument; expected Float");
                    return 0;
                } else if (!(*env)->IsSameObject(env, argType, Float_class)) {
                    throwIllegalArgumentException(env, "Tried to unbox arg of wrong type; expected Float");
                    return 0;
                } else {
                    arg_jvalues[i].f = (*env)->CallFloatMethod(env, arg, float_value_methodID);
                }
            } else if ((*env)->IsSameObject(env, paramType, double_class)) {
                if (arg == NULL) {
                    throwIllegalArgumentException(env, "Tried to unbox a null argument; expected Double");
                    return 0;
                } else if (!(*env)->IsSameObject(env, argType, Double_class)) {
                    throwIllegalArgumentException(env, "Tried to unbox arg of wrong type; expected Double");
                    return 0;
                } else {
                    arg_jvalues[i].d = (*env)->CallDoubleMethod(env, arg, double_value_methodID);
                }
            } else {
                // Arg does not need unboxing, but we need to check if it is assignable from the parameter type
                if (arg != NULL && !(*env)->IsAssignableFrom(env, argType, paramType)) {
                    throwIllegalArgumentException(env, "Tried to invoke function with arg of incompatible type");
                    return 0;
                } else {
                    arg_jvalues[i].l = arg;
                }
            }
        }
        return 1;
    }
    

    For speed, global refs to classes like Integer.class and int.class are obtained as follows on initialization:

    jclass Integer_class = (*env)->NewGlobalRef(env, (*env)->FindClass(env, "java/lang/Integer"));
    jclass int_class = (*env)->NewGlobalRef(env, (*env)->GetStaticObjectField(env, Integer_class, (*env)->GetStaticFieldID(env, Integer_class, "TYPE", "Ljava/lang/Class;")));
    jmethodID int_value_methodID = (*env)->GetMethodID(env, Integer_class, "intValue", "()I");
    

    Call the unboxing method like this:

    JNIEXPORT jint JNICALL Java_narcissus_Narcissus_callIntMethod(JNIEnv *env, jclass ignored, jobject obj, jobject method, jobjectArray args) {
        jmethodID methodID = (*env)->FromReflectedMethod(env, method);
        jsize num_args = (*env)->GetArrayLength(env, args);
        if (num_args == 0) {
            return (*env)->CallIntMethod(env, obj, methodID);
        }
        jvalue arg_jvalues[num_args];
        return unbox(env, method, args, num_args, arg_jvalues) ? (*env)->CallIntMethodA(env, obj, methodID, arg_jvalues) : (jint) 0;
    }
    

    The above code does not handle varargs. The full version that handles varargs can be seen in the Narcissus library (I am the author).