虽然SWIG本身没有提供std_function.i,但我们可以自己构建一个,需要一些工作。我的回答是我之前回答的链接的更普遍版本,着眼于Python的特定实例来解决这个问题。我将会介绍几个迭代版本,定义了一个用于通用std::function包装的%std_function宏。
我假设您想从std::function的封装中实现四件事情,这些成为我们的主要要求:
我们希望能够在Java代码中调用
std::function
对象。
包装的
std::function
对象需要像任何其他对象一样传递,包括在两个语言之间跨越语言边界。
应该可以在Java中编写
std::function
对象,并将其传回C++,而无需修改现有的在
std::function
对象上工作的C++代码(即保持
std::function
类型擦除跨语言)。
我们应该能够使用C++函数指针类型在Java中构造
std::function
对象。
我将逐步解决这些问题,并展示如何实现。在可能的情况下,我将保持解决方案不依赖于特定语言。
为了讨论方便,我会忽略您问题中的
shared_ptr
部分,因为如果您已经让
shared_ptr
正常工作,那么在这种情况下也足以使用它,只会使我的示例过于冗长。
我正在努力实现的解决方案实际上是基于 SWIG 中现有的 shared_ptr 支持模型。我已经编写了一个测试接口,以说明它将如何使用:
%module test
%include "std_function.i"
%std_function(Functor, void, int, double);
%{
#include <iostream>
%}
%inline %{
std::function<void(int,double)> make_functor() {
return [](int x, double y){
std::cout << x << ", " << y << "\n";
};
}
%}
基本上,要使用这个功能,你只需要包含文件"std_function.i",然后使用宏
%std_function
,该宏接受以下参数:
您需要每个实例化的
std::function
模板调用一次此函数,其中
Name
是您想在Java中调用的类型名称,Ret 是返回类型,其余(可变参数)为您的函数输入。因此,在上面的测试接口中,我基本上想要包装
std::function<void(int,double)>
。
编写“std_function.i”的第一个版本实际上并不太棘手。要获得基本的工作要求#1和#2,您所需要的是:
%{
#include <functional>
%}
%define %std_function(Name, Ret, ...)
%rename(Name) std::function<Ret(__VA_ARGS__)>;
%rename(call) std::function<Ret(__VA_ARGS__)>::operator();
namespace std {
struct function<Ret(__VA_ARGS__)> {
function<Ret(__VA_ARGS__)>(const std::function<Ret(__VA_ARGS__)>&);
Ret operator()(__VA_ARGS__) const;
};
}
%enddef
这将在生成的包装代码中包含 C++ 头文件,然后设置宏以供接口使用。SWIG 对
C++11 可变参数模板 的支持对于我们在此使用场景中并不实用,因此我编写的宏基本上是使用 C99 可变宏参数重新实现
默认模板扩展功能(C99 可变宏参数得到了更好的支持)。巧合的是,这意味着我们编写的 SWIG 代码将适用于 2.x 甚至一些 1.3.x 版本。(我已经测试过 2.x 版本)。即使/当你的 SWIG 版本具有与
std::function
兼容的
%template
支持时,保留此宏仍然对使其实际可调用的其他粘合剂非常有用。
std:function
模板的手动扩展仅限于我们关心的部分:实际的
operator()
和可能有用的复制构造函数。
唯一需要做的是将
operator()
重命名为与目标语言匹配的名称,例如将其重命名为普通函数“call”(如针对Java),或者如果针对Python,则重命名为
__call__
或使用tp_slots(如果需要)。
现在这已足以让我们的接口工作,为了演示它,我写了一点Java代码:
public class run {
public static void main(String[] argv) {
System.loadLibrary("test");
test.make_functor().call(1,2.5);
}
}
我使用以下内容进行编译:
swig2.0 -Wall -c++ -java test.i
g++ -Wall -Wextra -std=c++11 test_wrap.cxx -o libtest.so -I/usr/lib/jvm/default-java/include/ -I/usr/lib/jvm/default-java/include/linux -shared -fPIC
javac run.java
LD_LIBRARY_PATH=. java run
它起作用了。
现在,第四个要求很容易就能完成了。我们只需要告诉SWIG,在std::function
中有另一个接受兼容函数指针的构造函数:
// Conversion constructor from function pointer
function<Ret(__VA_ARGS__)>(Ret(*const)(__VA_ARGS__));
然后我们可以在 SWIG 中使用 %callback
机制,这是我们的测试接口文件:
%module test
%include "std_function.i"
%std_function(Functor, void, int, double);
%{
#include <iostream>
void add_and_print(int a, double b) {
std::cout << a+b << "\n";
}
%}
%callback("%s_cb");
void add_and_print(int a, double b);
%nocallback;
%inline %{
std::function<void(int,double)> make_functor() {
return [](int x, double y){
std::cout << x << ", " << y << "\n";
};
}
%}
我们用来调用这个的Java代码是:
public class run {
public static void main(String[] argv) {
System.loadLibrary("test");
test.make_functor().call(1,2.5);
new Functor(test.add_and_print_cb).call(3,4.5);
}
}
我们此时已经成功地编译和运行了相同的内容。
(需要注意的是,此时看到一些以名称“SWIGTYPE_p_f_…”开头的Java类是正常且理想的——它们包装了由指向函数构造函数和回调常量使用的“指向函数”的类型)
第三个需求是开始变得棘手的地方。本质上,我们遇到了与我之前回答过的关于如何让SWIG在Java中生成接口相同的问题,只不过现在我们希望以更通用的宏方式完成。
事实证明,在这种情况下,由于我们想要生成的接口相当简单,我们可以在宏中使用一些技巧来使SWIG为我们生成它。
为了使它起作用,我们需要做的主要事情是设置SWIG directors以提供跨语言多态性,并允许使用Java编写的内容实现C++接口。这是我的代码中带有后缀“Impl”的类。
为了让Java开发人员感觉“正确”,我们希望仍然使用相同的类型来实现C++和Java的
std::function
对象。即使
std::function::operator()
是虚拟的,我们仍不希望SWIG directors直接使用该类型,因为通常会通过值传递
std::function
,这将导致类型
slicing problems。因此,当Java开发人员扩展我们的
std::function
对象并覆盖
call
时,我们需要做一些额外的工作,以使C++实际调用该对象的Java实现,鉴于我们无法使用directors自动处理这个问题。
因此,我们所做的看起来有点奇怪。如果您构造一个旨在实现std::function
的Java对象,则有一个特殊的受保护构造函数可供使用。此构造函数将swigCPtr
成员变量(通常指向实际的C++对象)置为0,而是创建一个匿名包装器对象,该对象实现了“Impl”接口,并简单地将所有内容代理回Java对象的call
成员。
我们还有另一个类型映射,适用于在Java中将
std::function
对象传递给C++的任何地方。它的作用是检测我们所拥有的情况 - 一个由C++实现的
std::function
对象或者一个Java对象。在C++的情况下,它不会做任何特殊处理,一切都像平常一样进行。在Java的情况下,它会获取代理对象并请求C++将其转换回另一个独立的
std::function
实例,然后替换原来的对象。
这足以让我们在两种语言中获得所需的行为,而没有任何让人感到奇怪的东西(除了大量自动化的机械操作)。
问题在于自动构建代理对象并不是一件简单的事情。Java有动态代理类,作为反射API的一部分,但这些类只实现接口,而不是扩展抽象类。我尝试使用的一种可能性是在Java端使用void call(Object ...args)
,这是一个可变参数函数参数。虽然合法,但似乎并没有覆盖超类中所需的任何情况。
我最终做的是改编
一些宏,使其按照我想要的方式迭代可变参数宏。考虑到我们已经决定出于其他原因使用可变的C99宏参数,这是一个相当明智的解决方案。在我的解决方案中,该机制总共被使用了四次,一次在函数声明中,一次在Java和C++的委托调用中。(C++保留完美转发属性,而Java需要执行类型映射查找,因此在每种情况下都是不同的)。
还有一个自定义类型映射,简化了一些Java代码 - 在void函数中,写
return other_void_function();
是不合法的,所以如果没有它,我们需要特殊处理void函数。
那么让我们看看实际的情况。首先是我用于测试的run.java,它只是稍微修改了之前的示例,添加了一个
std::function
对象的Java实现。
public class run extends Functor {
public static void main(String[] argv) {
System.loadLibrary("test");
test.make_functor().call(1,2.5);
new Functor(test.add_and_print_cb).call(3,4.5);
Functor f = new run();
test.do_things(f);
}
@Override
public void call(int a, double b) {
System.out.println("Java: " + a + ", " + b);
}
}
std_function.i现在随着上述所有更改而显着增大:
%{
#include <functional>
#include <iostream>
#ifndef SWIG_DIRECTORS
#error "Directors must be enabled in your SWIG module for std_function.i to work correctly"
#endif
%}
#define param(num,type) $typemap(jstype,type) arg ## num
#define unpack(num,type) arg##num
#define lvalref(num,type) type&& arg##num
#define forward(num,type) std::forward<type>(arg##num)
#define FE_0(...)
#define FE_1(action,a1) action(0,a1)
#define FE_2(action,a1,a2) action(0,a1), action(1,a2)
#define FE_3(action,a1,a2,a3) action(0,a1), action(1,a2), action(2,a3)
#define FE_4(action,a1,a2,a3,a4) action(0,a1), action(1,a2), action(2,a3), action(3,a4)
#define FE_5(action,a1,a2,a3,a4,a5) action(0,a1), action(1,a2), action(2,a3), action(3,a4), action(4,a5)
#define GET_MACRO(_1,_2,_3,_4,_5,NAME,...) NAME
%define FOR_EACH(action,...)
GET_MACRO(__VA_ARGS__, FE_5, FE_4, FE_3, FE_2, FE_1, FE_0)(action,__VA_ARGS__)
%enddef
%define %std_function(Name, Ret, ...)
%feature("director") Name##Impl;
%typemap(javaclassmodifiers) Name##Impl "abstract class";
%{
struct Name##Impl {
virtual ~Name##Impl() {}
virtual Ret call(__VA_ARGS__) = 0;
};
%}
%javamethodmodifiers Name##Impl::call "abstract protected";
%typemap(javaout) Ret Name##Impl::call ";"
struct Name##Impl {
virtual ~Name##Impl();
protected:
virtual Ret call(__VA_ARGS__) = 0;
};
%typemap(maybereturn) SWIGTYPE "return ";
%typemap(maybereturn) void "";
%typemap(javain) std::function<Ret(__VA_ARGS__)> "$javaclassname.getCPtr($javaclassname.makeNative($javainput))"
%typemap(javacode) std::function<Ret(__VA_ARGS__)> %{
protected Name() {
wrapper = new Name##Impl(){
public $typemap(jstype, Ret) call(FOR_EACH(param, __VA_ARGS__)) {
$typemap(maybereturn, Ret)Name.this.call(FOR_EACH(unpack, __VA_ARGS__));
}
};
proxy = new $javaclassname(wrapper);
}
static $javaclassname makeNative($javaclassname in) {
if (null == in.wrapper) return in;
return in.proxy;
}
private Name##Impl wrapper;
private $javaclassname proxy;
%}
%rename(Name) std::function<Ret(__VA_ARGS__)>;
%rename(call) std::function<Ret(__VA_ARGS__)>::operator();
namespace std {
struct function<Ret(__VA_ARGS__)> {
function<Ret(__VA_ARGS__)>(const std::function<Ret(__VA_ARGS__)>&);
Ret operator()(__VA_ARGS__) const;
function<Ret(__VA_ARGS__)>(Ret(*const)(__VA_ARGS__));
%extend {
function<Ret(__VA_ARGS__)>(Name##Impl *in) {
return new std::function<Ret(__VA_ARGS__)>([=](FOR_EACH(lvalref,__VA_ARGS__)){
return in->call(FOR_EACH(forward,__VA_ARGS__));
});
}
}
};
}
%enddef
测试代码稍作修改以验证std::function
对象的Java -> C++传递并启用指导程序:
%module(directors="1") test
%include "std_function.i"
%std_function(Functor, void, int, double);
%{
#include <iostream>
void add_and_print(int a, double b) {
std::cout << a+b << "\n";
}
%}
%callback("%s_cb");
void add_and_print(int a, double b);
%nocallback;
%inline %{
std::function<void(int,double)> make_functor() {
return [](int x, double y){
std::cout << x << ", " << y << "\n";
};
}
void do_things(std::function<void(int,double)> in) {
in(-1,666.6);
}
%}
这段代码可以像之前的例子一样编译和运行。值得注意的是,我们已经开始写很多特定于Java的代码了 - 虽然如果你的目标是Python,使用Python特定功能来解决这些问题会更简单。
我想要改进两件事:
Use C++14 variadic lambdas to avoid the macro preprocessor magic I've used to keep them compatible with C++11. If you have C++ 14 the %extend
constructor becomes:
%extend {
function<Ret(__VA_ARGS__)>(Name##Impl *in) {
return new std::function<Ret(__VA_ARGS__)>([=](auto&& ...param){
return in->call(std::forward<decltype(param)>(param)...);
});
}
}
关于在使用
std::shared_ptr
的宏时,该宏本身不需要更改。然而,实现javadirectorin和directorin类型映射时存在问题,这会阻止事情的顺利进行。即使使用从“trunk”构建的SWIG版本也是如此。(在
combining directors and shared_ptr上有一个未解决的问题)
我们可以通过在模块的主.i文件中在调用
%shared_ptr
后添加两个额外的类型映射来解决这个问题。
%shared_ptr(some::ns::TheThing);
%typemap(javadirectorin) std::shared_ptr<some::ns::TheThing> "new $typemap(jstype, some::ns::TheThing)($1,false)";
%typemap(directorin,descriptor="L$typemap(jstype, some::ns::TheThing);") std::shared_ptr<some::ns::TheThing> %{
*($&1_type*)&j$1 = &$1;
%}
这两个typemap中的第一个实际上是死代码,因为我们在抽象类中强制“调用”方法是抽象的,但修复这个方法的编译比抑制它要容易。第二个typemap很重要。它与普通的“out”typemap非常相似,因为它创建了一个jlong,它实际上只是一个C++指针的表示,即它准备好一个对象从C++到Java。
请注意,如果您在模块中使用包,则可能需要修改directorin typemap的描述符属性,要么为“L$packagepath/$typemap(...);”,要么手动编写它。
这也应该删除现在生成的虚假“SWIGTYPE_p_sstd__shared_ptr...”类型。如果您有返回shared_ptr对象的虚函数,那么您也需要为它们编写directorout和javadirectorout typemaps。它们可以基于普通的“in”typemap。
这足以满足我的简单测试,我修改了Functor,至少在今天从trunk签出的SWIG版本中工作。(我的2.0.x测试失败了,我没有花费太多精力使其工作,因为这是一个已知的正在进行的工作区域)。