现在我找不到任何关于以下内容的信息:
假设我有一个需要传递回调函数的Java函数。该回调函数在稍后的时间点从不同的线程中调用。
现在我想从C++程序调用此函数,并且一旦调用回调函数,我想要调用一个C++回调。有人能指向一个包含如何执行此操作的信息源吗?
背景是我想在现有的C++项目中使用Java库(都在Linux上,尽管我怀疑这并不相关)。通过JNI调用Java函数的开销在这里不是问题。
好的,为了给未来的读者提供帮助,以下是我如何完成这项任务的方法。有几个点似乎不是很清楚,如果有人知道更好的方法,请告诉我。
所以,我写了一个简单的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);
}
}
//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));
}
#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;
}
这个代码可以正常工作,我可以使用它。但是,有几件事情仍在困扰着我:
reinterpret_cast
用于将指向std :: function
的指针转换为整数并返回,这不是我喜欢做的事情。但这是我能想到的将对函数的引用(可能仅在运行时存在)传递给Java的最佳方法...char *
(这里应该有一个const
)。这看起来像一行非常无害的代码,但会产生编译器警告,因为在C ++中这是非法的(在C中是合法的,这就是JNI开发人员可能没有关注的原因)。我找不到优雅的方法解决这个问题。我知道可以使其合法,但我真的不想仅为此编写多行代码(或者抛出const_cast
),所以我决定,对于这个问题,只好忍受这个警告。创建您的Java视图的本地接口。也就是创建一个Java类并声明本地方法。有一个关键字native
可供使用。您不提供任何实现,只需声明即可。
使用javac -h
生成本机头文件。请阅读该工具的文档。在Java 7中,有一个单独的工具叫做javah
。但是,在当前的Java 11中,您应该使用javac
。
使用C或C++为生成的头文件中声明的函数提供实现。编译并将它们链接到共享对象(*.so
或*.dll
)。
在Java应用程序运行时通过调用以下代码从新库加载本机代码:
System.load("path-to-lib");
如果您的本地函数已经在当前进程中加载,那么您不必执行最后一步骤4。这将是如果您正在将Java应用程序嵌入CPP应用程序中。在这种情况下,您可能需要查看RegisterNatives。
Java关键字native
的文档:
JNI的文档在此处:
还要查看Java编译器的文档,了解如何生成本机头文件。查找选项-h
:
.
编辑
今天我比昨天更好地理解了你的问题:
好的,结合您已经知道的内容以及我上面给出的解释,这可以完成。 实际上,可以以不同的方式再次完成。 我将解释一种简单的方法:
您的C++应用程序已经知道在Java方法完成时需要调用哪个回调。
当您调用Java方法时,将其作为key
传递给它。
您已经知道如何从C++中调用Java方法。
该key
可以是任何东西。为了保持简单,key
是一个uintptr_t
,即指针大小的整数。
在这种情况下,我们只需将函数指针作为回调传递给Java方法即可。
extern "C"
函数,并将其作为参数给出key
。我已经解释了如何从Java调用本地函数。该本地函数现在只需要将该整数强制转换回指针:(reinterpret_cast<>()
)并调用您的回调。当然,如果有一些数据要传递给您的回调,则本地函数可以接受比key
更多的参数。我想现在这个想法非常清楚了。std::map
将该关键字映射到您的实际回调。但是,先从这个简单的例子开始。当它正常工作时,很容易改进它。大部分工作将是设置项目和工具一起工作。