Java回调到C++的回调函数

3
有无数关于如何使用JNI从C++调用Java代码的文章和问题,我可以做到这一点,我可以从C++调用一些Java函数。
现在我找不到任何关于以下内容的信息:
假设我有一个需要传递回调函数的Java函数。该回调函数在稍后的时间点从不同的线程中调用。
现在我想从C++程序调用此函数,并且一旦调用回调函数,我想要调用一个C++回调。有人能指向一个包含如何执行此操作的信息源吗?
背景是我想在现有的C++项目中使用Java库(都在Linux上,尽管我怀疑这并不相关)。通过JNI调用Java函数的开销在这里不是问题。

不确定您是否已经涵盖了这个问题,但如果没有,您可以研究Javacpp:https://github.com/bytedeco/javacpp/wiki/Basic-Concepts - Koenigsberg
请为事物命名。在上面的问题中,您有1到4个名为“callback”的事物。我可以猜测哪个“callback”指的是哪个实际的事物,但我只会猜测,无法确定,这是您问题的核心 - Yakk - Adam Nevraumont
Yakk,当我写回调函数时,我的意思是指一个通常定义的回调函数。由于我在不同的线程中编写,因此显然是异步的。 - Demosthenes
2个回答

3

好的,为了给未来的读者提供帮助,以下是我如何完成这项任务的方法。有几个点似乎不是很清楚,如果有人知道更好的方法,请告诉我。

所以,我写了一个简单的Java类Bar,放在包foo中,从C++中调用它时需要传递一个函数引用(下面会详细说明),并使用一些硬编码参数调用该函数。

package foo;
import foo.Functor;

//this is just what we want to call from C++
//for demonstration, expect return type int
public class Bar
{
    public static void run(long addr) {
        Functor F = new Functor(addr);

        //synchronously here, just to prove the concept
        F.run(1,2);
    }
}

正如您所看到的,我还编写了一个名为Functor的类,这个类也很简单明了。
包 foo;
//we need to write this for every signature of a callback function
//we'll do this as a void foo(int,int), just to demonstrate
//if someone knows how to write this in a general (yet JNI-compatible) way,
//keeping in mind what we are doing in the non-Java part, feel free to tell me
public class Functor
{
    static {
        System.loadLibrary("functors");
    }
    public native void runFunctor(long addr,int a,int b);

    long address;

    public Functor(long addr)
    {
    address = addr;
    }

    public void run(int a, int b) {
        runFunctor(address,a,b);
    }
}

这取决于一个我称之为"functors"的共享库。它的实现非常直接。想法是将实际逻辑分开,仅在共享对象中提供接口。正如之前提到的,主要缺点是我必须为每个签名编写它,我看不到模板化的方式。

只是为了完整性,这是共享对象的实现:

#include <functional>
#include "include/foo_Functor.h"

JNIEXPORT void JNICALL Java_foo_Functor_runFunctor
  (JNIEnv *env, jobject obj, jlong address, jint a, jint b)
{
    //make sure long is the right size
    static_assert(sizeof(jlong)==sizeof(std::function<void(int,int)>*),"Pointer size doesn't match");

    //this is ugly, if someone has a better idea...
    (*reinterpret_cast<std::function<void(int,int)>*>(address))(static_cast<int>(a),static_cast<int>(b));
}

最后,这是我在C++中如何调用它,在运行时定义回调函数,位于共享对象之外:
#include <iostream>
#include <string>
#include <jni.h>
#include <functional>

int main()
{
    //this is from some tutorial, nothing special
    JavaVM *jvm;
    JNIEnv *env;
    JavaVMInitArgs vm_args;
   JavaVMOption* options = new JavaVMOption[1];  
    options[0].optionString = "-Djava.class.path=."; //this is actually annoying, JNI has this as char* without const, resulting in a warning since this is illegal in C++ (from C++11)
    vm_args.version = JNI_VERSION_1_6;   
    vm_args.nOptions = 1;      
    vm_args.options = options;
    vm_args.ignoreUnrecognized = false;  

    jint rc = JNI_CreateJavaVM(&jvm, (void**)&env, &vm_args);
    delete[] options; 

    if (rc != JNI_OK)
        return EXIT_FAILURE;


    jclass cls = env->FindClass("foo/Bar");
    jmethodID mid = env->GetStaticMethodID(cls, "run", "(J)V");

    //the main drawback of this approach is that this std::function object must never go out of scope as long as the callback could be fired
    std::function<void(int,int)> F([](int a, int b){std::cout << a+b << std::endl;});

    //this is a brutal cast, is there any better option?
    long address = reinterpret_cast<long>(&F);
    env->CallStaticVoidMethod(cls,mid,static_cast<jlong>(address));


    if (env->ExceptionOccurred())
        env->ExceptionDescribe();
    jvm->DestroyJavaVM();
    return EXIT_SUCCESS;
}

这个代码可以正常工作,我可以使用它。但是,有几件事情仍在困扰着我:

  1. 我必须为我想要传递的函数签名编写接口(一个Java类和相应的C ++实现)。这是否可以以更通用的方式完成?我的感觉是Java(特别是JNI)不够灵活。
  2. reinterpret_cast用于将指向std :: function的指针转换为整数并返回,这不是我喜欢做的事情。但这是我能想到的将对函数的引用(可能仅在运行时存在)传递给Java的最佳方法...
  3. 在C ++中初始化Java VM时,我正在设置一个选项,在JVM接口中定义为char *(这里应该有一个const)。这看起来像一行非常无害的代码,但会产生编译器警告,因为在C ++中这是非法的(在C中是合法的,这就是JNI开发人员可能没有关注的原因)。我找不到优雅的方法解决这个问题。我知道可以使其合法,但我真的不想仅为此编写多行代码(或者抛出const_cast),所以我决定,对于这个问题,只好忍受这个警告。

3
您说得对,某种程度上来说,这方面的文档并不容易找到。 但我仍然记得我在以前的项目中是如何做到这一点的。 您需要阅读一些免费提供的在线文档,因为我可能会错过一些细节。我将在本帖子末尾给您提供链接。
所以如果我理解您的意思正确,您想从Java调用本地的C++函数。 首先要记住的是Java Native Interface不是C++而是C。 这就像大多数高级编程语言的本地接口一样(迄今为止我见过的所有语言都是如此)。
  1. 创建您的Java视图的本地接口。也就是创建一个Java类并声明本地方法。有一个关键字native可供使用。您不提供任何实现,只需声明即可。

  2. 使用javac -h生成本机头文件。请阅读该工具的文档。在Java 7中,有一个单独的工具叫做javah。但是,在当前的Java 11中,您应该使用javac

  3. 使用C或C++为生成的头文件中声明的函数提供实现。编译并将它们链接到共享对象(*.so*.dll)。

  4. 在Java应用程序运行时通过调用以下代码从新库加载本机代码:

    System.load("path-to-lib");

如果您的本地函数已经在当前进程中加载,那么您不必执行最后一步骤4。这将是如果您正在将Java应用程序嵌入CPP应用程序中。在这种情况下,您可能需要查看RegisterNatives

Java关键字native的文档:

JNI的文档在此处:

还要查看Java编译器的文档,了解如何生成本机头文件。查找选项-h

.

编辑

今天我比昨天更好地理解了你的问题:

  • 您有一个嵌入Java应用程序的C++应用程序。
  • 从C++中,您想调用Java方法。
  • 在调用时,您想传递回调方法。
  • 当Java方法完成时,它必须调用您之前传递的回调方法。

好的,结合您已经知道的内容以及我上面给出的解释,这可以完成。 实际上,可以以不同的方式再次完成。 我将解释一种简单的方法:

您的C++应用程序已经知道在Java方法完成时需要调用哪个回调。 当您调用Java方法时,将其作为key传递给它。 您已经知道如何从C++中调用Java方法。 该key可以是任何东西。为了保持简单,key是一个uintptr_t,即指针大小的整数。 在这种情况下,我们只需将函数指针作为回调传递给Java方法即可。

但是Java无法通过取消引用整数/指针来调用回调。现在,您可以调用一个本地的extern "C"函数,并将其作为参数给出key。我已经解释了如何从Java调用本地函数。该本地函数现在只需要将该整数强制转换回指针:(reinterpret_cast<>())并调用您的回调。当然,如果有一些数据要传递给您的回调,则本地函数可以接受比key更多的参数。我想现在这个想法非常清楚了。
如果您想拥有更具可移植性的代码,则不要使用回调的地址作为关键字。而是使用整数或甚至字符串,并使用std::map将该关键字映射到您的实际回调。但是,先从这个简单的例子开始。当它正常工作时,很容易改进它。大部分工作将是设置项目和工具一起工作。

我明天会研究这个问题。只有几点评论:首先,C和C++之间的桥接不是问题,我知道如何做。然后,实际上我是从C++调用Java。但是,根据我的理解,您的想法是回调函数应该是从Java调用C++的函数。我知道JNI可以做到这一点,并且可能有效。 - Demosthenes
我所看到的唯一缺点是,这个(加上依赖项)需要与Java代码链接,而不是C++代码。如果我这样做,我就会有两份所有内容(并且需要额外的开销来保持同步)。因此,你建议将所有内容放入共享对象中可能是一个好主意。我认为共享对象通常被过度使用,但我认为这可能是一个合理的用例。 - Demosthenes
我会给这个点个赞。根据你的回答,我想出了一个解决方案,我会自己发布作为答案。 - Demosthenes
1
现在我已经发布了我是如何做到的。这个想法和你的一样,只是我想提供一个完整的解决方案,也许对某些人有用。 - Demosthenes

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接