【问题标题】:Return function pointer from C++ behavior to Java object with JNI使用 JNI 将函数指针从 C++ 行为返回到 Java 对象
【发布时间】:2018-11-14 13:59:10
【问题描述】:

这超出了我的想象,所以我们开始吧。

我在 C++ 中使用接口 Java 对象设置函数指针,该对象在 Java 端看起来像这样:

    Info_t info_t = new Info_t();
    info_t.setCallbackTest(new CallbackTest() {
        @Override
        public void onCallback(int num) {
            System.out.println("callback: " + num);
        }
    });

其中CallbackTest() 是只有一种方法的接口:

public interface CallbackTest {
    void onCallback(int num);
}

setCallbackTest()指的是Java原生方法,它反映在C++端是这样的:

JNIEnv *gbl_env;
jmethodID gbl_method;
jobject gbl_callback;

void WrapperFunction(int a) {
    gbl_env->ExceptionClear();
    gbl_env->CallVoidMethod(gbl_callback, gbl_method, a);
    if(gbl_env->ExceptionOccurred()) {
        gbl_env->ExceptionClear();
    }
    return;
}

JNIEXPORT void JNICALL Java_apiJNI_Info_1t_1callbackTest_1set(JNIEnv *jenv, jclass jcls, jlong jarg1, jobject jarg1_, jobject jarg2) {
    Info *arg1 = (Info *) 0;

    (void) jenv;
    (void) jcls;
    (void) jarg1_;
    arg1 = *(Info **)&jarg1;
    {
        jclass clazz;
        jmethodID mid;

        clazz = jenv->GetObjectClass(jarg2);
        mid = jenv->GetMethodID(clazz, "onCallback", "(I)V");
        if(mid == 0) {
            std::cout << "could not get method id" << std::endl;
            return;
        }
        gbl_env = jenv;
        gbl_method = mid;
        gbl_callback = jarg2;
    }
    // here I am setting function pointer to the helper method which invokes the java code
    if(arg1) (arg1)->completionCB = WrapperFunction;
}

所有这些都是基于Implement callback function in JNI using Interface写的

我尝试在 C++ 端调用 Java 中定义的 onCallback() 方法,它可以工作(就像上面的链接中一样),但现在我试图在 C++ 下的 struct 上设置值

typedef void (*callbackFunction)(int);
typedef struct Info
{
    callbackFunction    completionCB;
    void                *caller;
} Info_t;

所以稍后在某个时候,我想将callbackFunction completionCB 作为一个对象返回给 Java,并可能用它做各种事情。

我的问题是如何将函数指针行为从 C++ 返回/映射回 Java 对象?基本上,我想做同样的事情,但顺序相反。我现在可以将 Java 接口映射到函数指针上,我想将函数指针行为映射到 Java 对象上。

编辑:到目前为止,我已经想出了这样的吸气剂,但我不确定如何继续

JNIEXPORT jobject JNICALL Java_apiJNI_Info_1t_1callbackTest_1get(JNIEnv *jenv, jclass jcls, jlong jarg1, jobject jarg1_) {
    jobject jresult = 0;
    Info *arg1 = (Info *) 0;
    callbackFunction result;

    (void)jenv;
    (void)jcls;
    (void)jarg1_;
    arg1 = *(Info **)&jarg1;
    result = ((arg1)->completionCB);

    {
        jclass clazz = jenv->FindClass("com/CallbackTest");
        ???
    }
    ???
    return ;
}

【问题讨论】:

    标签: java c++ java-native-interface function-pointers swig


    【解决方案1】:

    您不能缓存 JNIEnv 值。

    根据 JNI 规范的Chapter 5: The Invocation API(粗体字):

    附加到虚拟机

    JNI接口指针(JNIEnv)只在当前有效 线程。如果另一个线程需要访问 Java VM,它必须 首先调用 AttachCurrentThread() 将自己附加到 VM 和 获取一个 JNI 接口指针。连接到 VM 后,本机 线程的工作方式就像在本机内部运行的普通 Java 线程 方法。本机线程一直连接到 VM,直到它调用 DetachCurrentThread() 分离自身。

    对于方法id和你的回调对象,你需要创建一个全局引用:

        gbl_method = jenv->NewGlobalReference( mid );
        gbl_callback = jenv->NewGlobalReverend( jarg2 );
    

    不过,这确实会产生一个严重的问题 - gbl_callback 中保存的对象引用将导致 Java 对象永远不会被垃圾回收。

    要返回 C++ 回调函数,您首先需要将函数指针转换为有效的 Java 类型。严格遵守 C 标准(JNI 是真正的 C 而不是 C++,所以当你在 C++ 中混用时来回传递对象有点模糊)意味着你不能将函数指针视为一系列字节之外的任何东西,这意味着您需要将函数指针转换为 Java 字节数组并将其返回给 Java。然后,当您想在本机代码中使用它时,将该 Java 字节数组转换回函数指针。这是很多代码,而在 JNI 中,您编写的代码越多,您就越有可能破坏某些东西 - JNI 脆弱

    但是在 Windows 和 POSIX 上,函数指针都适合 jlong,而 jlong 可以准确地表示函数指针的值,所以这是一个有效的 hack(在 POSIX 下它可能甚至不是一个 hack ,但我不会尝试找到允许这种转换的实际 POSIX 函数指针规范)。

    最简单的方法是在您的 Java 对象中创建一个long 字段,然后返回将函数指针转换为jlong

    JNIEXPORT jlong JNICALL Java_apiJNI_Info_1t_1callbackTest_1set(
        JNIEnv *jenv, jclass jcls, jlong jarg1, jobject jarg1_, jobject jarg2)
    {
        ...
    
        return( ( jlong ) WrapperFunction );
    }
    

    这将被称为:

    // Info_t has a long field called callback
    Info_t info_t = new Info_t();
    info_t.callback = info_t.setCallbackTest(new CallbackTest() {
        @Override
        public void onCallback(int num) {
            System.out.println("callback: " + num);
        }
    });
    

    根据我的经验,JNI 调用越少越好。因此,如果您可以通过参数传递值或从函数调用中返回值,这比尝试使用 JNI 调用获取/设置对象字段值更简单、更安全、更可靠。

    但是你需要将回调值作为另一个参数传递:

    // function pointer, returns void, takes int arg
    typedef void (*funcPtr)( int );
    
    JNIEXPORT jobject JNICALL Java_apiJNI_Info_1t_1callbackTest_1get(
    JNIEnv *jenv, jclass jcls, jlong callback, jlong jarg1, jobject jarg1_)
    {
        funcPtr f = ( funcPtr ) callback;
    
        f( 4 );
    
        ...
    }
    

    【讨论】:

    • 你是指我定义全局JEnv*gbl_env = jenv;的代码部分吗?如果你这样做了,那么如何将 Java 接口映射到 C++ 中的函数指针?
    • @Lisek 答案中的引用已经概述了需要做的事情。有关详细信息,请参阅this answer
    • 谢谢迈克尔。这指出了我的代码的缺陷,但仍在寻找答案,我如何将函数指针映射到 Java 对象。
    • @Lisek 这指出了我的代码的缺陷,但仍在寻找答案,我如何将函数指针映射到 Java 对象。 没有办法调用非JNI 类型函数,例如您的 void WrapperFunction(int a) 直接来自 Java 代码。但是,您可以将函数指针保存为 jlong 值。有关详细信息,请参阅修改。
    猜你喜欢
    • 2022-11-07
    • 2010-12-25
    • 1970-01-01
    • 1970-01-01
    • 2014-03-30
    • 1970-01-01
    • 1970-01-01
    • 2018-01-19
    • 2020-02-11
    相关资源
    最近更新 更多