Markovian8261
Markovian8261

Reputation: 899

Call NewObject method jni with params in jobjectarray

I am working in JNI with c++ and I have created a method where a series of parameters are passed to my native method as a jobjectarray. I would like to call a constructor in JNI using those parameters. However the NewObject method does not accept a jobject array instead using an ellipsis. How would I accomplish this task? I do not know how many parameters the constructor will be taking before the method is called and the signature string is passed along as well from java. The constructor that I am calling does not take an array as an argument, instead different versions of the same class can be passed to the c++ function with each one containing a different method signature. I need my c++ method to generically be able to create any object with its passed arguments. I am using visual studio as my IDE. I understand I might need an array of jvalues but I don't understand how to get that from a jobjectarray.

Upvotes: 4

Views: 6570

Answers (3)

nneonneo
nneonneo

Reputation: 179422

Thanks to @LukeHutchinson's prompting, I went looking for a better solution, and am happy to report that there is in fact a simpler approach using the Reflection API. You can use the JNI function ToReflectedMethod to convert a methodID to java.lang.reflect.Method or java.lang.reflect.Constructor, after which you can call invoke or newInstance respectively, which will handle all of the necessary unboxing conversions.

Here's a proof of concept, with error checking omitted for clarity.

test.java:

public class test {
    static {
        System.loadLibrary("native");
    }
    
    public static void main(String[] args) {
        /* This is the constructor String(byte[], int, int).
           This call will print out BCD - the result of creating
           a string from bytes 1-3 of the array */
        System.out.println(new test().makeObject("java/lang/String", "([BII)V", new byte[] { 0x41, 0x42, 0x43, 0x44, 0x45 }, 1, 3));
    }

    private native Object makeObject(String clazz, String signature, Object... args);
}

libnative.c:

#include <jni.h>

JNIEXPORT jobject JNICALL Java_test_makeObject(JNIEnv *env, jobject this, jstring clazzName, jstring signature, jobjectArray args) {
    const char *clazzNameStr = (*env)->GetStringUTFChars(env, clazzName, NULL);
    const char *signatureStr = (*env)->GetStringUTFChars(env, signature, NULL);

    jclass clazz = (*env)->FindClass(env, clazzNameStr);
    jmethodID methodID = (*env)->GetMethodID(env, clazz, "<init>", signatureStr);
    jobject reflectedMethod = (*env)->ToReflectedMethod(env, clazz, methodID, JNI_FALSE);

    jclass constructorClazz = (*env)->FindClass(env, "java/lang/reflect/Constructor");
    jmethodID newInstanceMethod = (*env)->GetMethodID(env, constructorClazz, "newInstance", "([Ljava/lang/Object;)Ljava/lang/Object;");

    jobject result = (*env)->CallObjectMethod(env, reflectedMethod, newInstanceMethod, args);

    (*env)->ReleaseStringUTFChars(env, clazzName, clazzNameStr);
    (*env)->ReleaseStringUTFChars(env, signature, signatureStr);
    return result;
}

Upvotes: 1

Ruben O. Chiavone
Ruben O. Chiavone

Reputation: 354

EDIT:

Sorry I misunderstood your question. You can achieve this by using the other two ways that JNI API provides for creating objects (from docs):

jobject NewObjectA(JNIEnv *env, jclass clazz,
jmethodID methodID, const jvalue *args);

jobject NewObjectV(JNIEnv *env, jclass clazz,
jmethodID methodID, va_list args);

NewObjectA

Programmers place all arguments that are to be passed to the constructor in an args array of jvalues that immediately follows the methodID argument. NewObjectA() accepts the arguments in this array, and, in turn, passes them to the Java method that the programmer wishes to invoke.

NewObjectV

Programmers place all arguments that are to be passed to the constructor in an args argument of type va_list that immediately follows the methodID argument. NewObjectV() accepts these arguments, and, in turn, passes them to the Java method that the programmer wishes to invoke.

So, I made a sample program that shows how to use it.

Foo.java

public class Foo {

    private int bar;
    private String baaz;

    public Foo(int bar) {
        this(bar, "");
    }

    public Foo(int bar, String baaz) {
        this.bar = bar;
        this.baaz = baaz;
    }

    public void method1() {
        this.bar++;

        System.out.println(bar);
        System.out.println(baaz);
    }
}

Bar.java

public class Bar {

    public Bar() {
    }

    public static native Foo createFoo(String signature, Object ... params);
}

Bar.h

/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class Bar */

#ifndef _Included_Bar
#define _Included_Bar
#ifdef __cplusplus
extern "C" {
#endif
/*
 * Class:     Bar
 * Method:    createFoo
 * Signature: (Ljava/lang/String;[Ljava/lang/Object;)LFoo;
 */
JNIEXPORT jobject JNICALL Java_Bar_createFoo
  (JNIEnv *, jclass, jstring, jobjectArray);

#ifdef __cplusplus
}
#endif
#endif

Bar.c

#include "Bar.h"

#include <stdlib.h>

jobject JNICALL Java_Bar_createFoo
  (JNIEnv * env, jclass class, jstring signature, jobjectArray params) {

    // method signature in char *
    const char * signatureChar = (*env)->GetStringUTFChars(env, signature, 0);

    jvalue * args;
    int i, size;

    // retrieve foo class
    jclass fooClass = (*env)->FindClass(env, "LFoo;");

    // retrieve foo construtor
    jmethodID fooConstructor = (*env)->GetMethodID(env, fooClass, "<init>", signatureChar);

    // operate over params
    // ...

    // TODO: find out a way to retrieve size from constructor
    size = 2;

    args = malloc(size * sizeof(jvalue));

    for (i = 0; i < size; i++) {
        args[i].l = (*env)->GetObjectArrayElement(env, params, i);
    }

    return (*env)->NewObjectA(env, fooClass, fooConstructor, args);
}

Main.java

public class Main {

    static {
        System.loadLibrary("YOUR_LIBRARY_NAME_HERE");
    }

    public static void main(String[] args) {
        Foo foo = Bar.createFoo("(ILjava/lang/String;)V", -12312141, "foo");

        System.out.println(foo);

        foo.method1();

        foo = Bar.createFoo("(I)V", -12312141, "foo");

        System.out.println(foo);

        foo.method1();

        foo = Bar.createFoo("(I)V", -12312141);

        System.out.println(foo);

        foo.method1();
    }
}

Warning: it is still not 100% funciontal because I couldn't figure out how to retrieve constructor argument size based on constructor signature.

Upvotes: 2

nneonneo
nneonneo

Reputation: 179422

This is a bit tricky because you've been passed a jobjectArray. This means that primitive types have been boxed (e.g. ints are java.lang.Integer instances in your array), and you have to unbox them before passing them onto a constructor.

What you're going to have to do is parse the method signature string (it's not nearly as bad as you might think), walk over every jobject in your array and convert it to the corresponding type (using an unboxing conversion if necessary).

Sadly, there's no built-in way in JNI to perform unboxing, and thus you're going to have to do it manually by invoking the appropriate methods of the boxed values (e.g. Integer.intValue to get ints).

The basic idea:

jobject createObject(JNIEnv *env, jclass clazz, jmethodID constructor, const char *argstr, jobjectArray *args) {
    int n = env->GetArrayLength(args);
    jvalue *values = new jvalue[n];
    const char *argptr = argstr;
    for(int i=0; i<n; i++) {
        jobject arg = env->GetObjectArrayElement(args, i);
        if(*argptr == 'B') { /* byte */
            values[i].b = object_to_byte(arg);
        }
        /* cases for all of the other primitive types...*/
        else if(*argptr == '[') { /* array */
            while(*argptr == '[') argptr++;
            if(*argptr == 'L')
                while(*argptr != ';') argptr++;
            values[i].l = arg;
        } else if(*argptr == 'L') { /* object */
            while(*argptr != ';') argptr++;
            values[i].l = arg;
        }
        argptr++;
        env->DeleteLocalRef(arg);
    }
    return env->NewObjectA(clazz, methodID, values);
}

object_to_byte and other conversion functions will be defined as functions which unbox the relevant type (e.g. object_to_byte will use the JNI to call java.lang.Byte.byteValue on the given object).

Upvotes: 4

Related Questions