【问题标题】:Call NewObject method jni with params in jobjectarray使用 jobjectarray 中的参数调用 NewObject 方法 jni
【发布时间】:2015-09-03 05:09:54
【问题描述】:

我正在使用 C++ 在 JNI 中工作,并且我创建了一个方法,其中一系列参数作为 jobjectarray 传递给我的本机方法。我想使用这些参数在 JNI 中调用构造函数。但是,NewObject 方法不接受 jobject 数组,而是使用省略号。我将如何完成这项任务?我不知道在调用该方法之前构造函数将采用多少个参数,并且签名字符串也从 java.util 传递。我正在调用的构造函数不将数组作为参数,而是可以将同一类的不同版本传递给 c++ 函数,每个函数都包含不同的方法签名。我需要我的 c++ 方法通常能够使用其传递的参数创建任何对象。我正在使用 Visual Studio 作为我的 IDE。我知道我可能需要一个 jvalues 数组,但我不明白如何从 jobjectarray 中获取它。

【问题讨论】:

  • 您是否考虑过使用多个参数,而不仅仅是一个数组?这样会更容易。
  • “调用构造函数”:你的意思是java构造函数?以及如何找到合适的构造函数,寻找合适的签名?
  • 我不知道构造函数在调用方法之前会接受多少个参数。
  • 抱歉坚持,但要调用构造函数,您首先需要使用env->GetMethodID(cls, "<init>", signature-string) 找到它。因此,您需要知道数组中参数的数量及其类型,以便能够找到正确的构造函数。
  • 我从 java 传递签名字符串,它在 java 方法中生成

标签: java c++ java-native-interface


【解决方案1】:

这有点棘手,因为您已收到jobjectArray。这意味着原始类型已被装箱(例如,ints 是数组中的 java.lang.Integer 实例),您必须在将它们传递给构造函数之前将它们拆箱。

您要做的是解析方法签名字符串(它并不像您想象的那么糟糕),遍历数组中的每个 jobject 并将其转换为相应的类型(使用拆箱必要时进行转换)。

遗憾的是,JNI 中没有内置的方法来执行拆箱,因此您将不得不通过调用装箱值的适当方法来手动完成(例如,Integer.intValue 以获取 ints) .

基本思路:

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 和其他转换函数将被定义为将相关类型拆箱的函数(例如,object_to_byte 将使用 JNI 调用给定对象上的 java.lang.Byte.byteValue)。

【讨论】:

  • 没有 JNI 方法如何自动执行此操作?如果您将本机方法设置为可变参数方法,则最后一个参数是jobjectArray,并且必须先取消装箱,然后才能使用这些参数调用另一个仅采用未装箱参数的方法。这是一个非常普遍的需求,我不明白为什么没有 JNI 调用这个。
  • @LukeHutchison:感谢您的评论!事实证明,实际上通过反射 API 有更好的方法来做到这一点——所以我用这种方法发布了一个新的答案。如果无法使用反射 API,并且如果人们对如何以艰难的方式做到这一点感到好奇,我将留下这个其他答案;)
【解决方案2】:

编辑:

抱歉,我误解了您的问题。您可以通过使用 JNI API 提供的其他两种创建对象的方式(来自docs)来实现这一点:

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

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

NewObjectA

程序员将所有要传递给构造函数的参数放在一个 jvalue 的 args 数组中,该数组紧跟在 methodID 参数之后。 NewObjectA() 接受此数组中的参数,然后将它们传递给程序员希望调用的 Java 方法。

NewObjectV

程序员将所有要传递给构造函数的参数放在紧跟在 methodID 参数之后的 va_list 类型的 args 参数中。 NewObjectV() 接受这些参数,然后将它们传递给程序员希望调用的 Java 方法。

所以,我制作了一个示例程序来展示如何使用它。

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();
    }
}

警告:它仍然不是 100% 功能性的,因为我不知道如何根据构造函数签名检索构造函数参数大小。

【讨论】:

  • 这不是我想要做的。我正在调用的构造函数不将数组作为参数。问题是同一类的不同版本可以传递给 c++ 函数,每个函数都包含不同的方法签名。我需要我的 c++ 方法通常能够使用其传递的参数创建任何对象。
【解决方案3】:

感谢@LukeHutchinson 的提示,我开始寻找更好的解决方案,并且很高兴地报告,实际上有一种使用反射 API 的更简单的方法。您可以使用 JNI 函数ToReflectedMethodmethodID 转换为java.lang.reflect.Methodjava.lang.reflect.Constructor,之后您可以分别调用invokenewInstance,这将处理所有必要的拆箱转换。

这是一个概念证明,为清楚起见省略了错误检查。

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;
}

【讨论】:

  • 谢谢,这是有道理的。现在,如果 JDK 团队从 JDK 16 开始不强制执行封装就好了! (这些反射调用强制执行访问检查。)我真的希望他们只向 JNI API 添加一个CallObjectMethodX 函数,该函数将在最后一个参数位置采用jobjectArray。当甚至本机方法也可以采用可变参数(Object[] 参数在最后一个位置)时,他们为什么不认为包括在内?
猜你喜欢
  • 2012-07-04
  • 2018-04-22
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2017-09-06
  • 1970-01-01
  • 2012-11-08
相关资源
最近更新 更多