如何使用SWIG包装std :: function对象?

Den*_*ial 8 c++ java swig c++11 std-function

我看过很多类似的问题,但还没有找到解决我特定问题的方法.我试图SWIGify一些使用std :: function的C++ 11代码,所以我可以在我的Java应用程序中使用它.

我遇到过像这样的共享指针:

virtual std::shared_ptr<some::ns::TheThing> getTheThing(unsigned short thingID);
Run Code Online (Sandbox Code Playgroud)

并使用shared_ptr指令成功处理它们,如下所示:

%shared_ptr(some::ns::TheThing);
Run Code Online (Sandbox Code Playgroud)

我遇到过这样的共享指针向量:

virtual std::vector<std::shared_ptr<some::ns::TheThing>> getAllTheThings() const = 0;
Run Code Online (Sandbox Code Playgroud)

并使用如下模板成功处理它们:

%template(ThingVector) std::vector<std::shared_ptr<some::ns::TheThing>>;
Run Code Online (Sandbox Code Playgroud)

现在我有一个像这样的方法:

 void registerThingCallback(std::function<void(std::shared_ptr<some::ns::TheThing>) > func);
Run Code Online (Sandbox Code Playgroud)

我无法让SWIG正确包装它.我已经尝试使用%回调,导演,%模板和%内联功能代码,因为我已经看到所有这些事情的例子,但是还没有能够得到任何似乎接近工作的东西.如果有帮助(清理和减少),这里有一个关于函数调用的更多上下文:

thing_callback.h

#include <functional>

namespace some {
  namespace ns {

    /**
     * Hold some callbacks.
     */
    class ThingCallbacks {
    public:

        /**
         * Registers a callback 
         * @param func The callback function
         */
        void registerThingCallback(std::function<void(std::shared_ptr<some::ns::TheThing>) > func);

    };

  }
}
Run Code Online (Sandbox Code Playgroud)

更新

根据Flexo下面的答案,我更接近解决方案.我能够让下面的例子完全像宣传的那样工作.我尝试将它合并到我的实际代码中,但遇到了问题.为了扩展我之前的简化示例,这里是我对TheThing的定义:

test_thing.h

#ifndef THE_THING_H
#define THE_THING_H

#include <string>

namespace some {
  namespace ns {

    class TheThing {
    public:

        virtual ~TheThing() {};

        virtual unsigned long longThing() const = 0;

        virtual std::string stringThing() const = 0;
    };
  }
}

#endif  /* THE_THING_H */
Run Code Online (Sandbox Code Playgroud)

这是我的.i文件.为了让尽可能少的移动部件,我基本上只是从下面的答案中提供的代码中获取int和double,并用一个指向我的对象的共享指针替换它们.

func_thing_test.i

%module(directors="1") Thing
%include "stl.i"
%include "std_function.i"
%include "std_shared_ptr.i"

%shared_ptr(some::ns::TheThing);


%typemap(javadirectorin) std::shared_ptr<some::ns::TheThing> "new $typemap(jstype, some::ns::TheThing)($1,false)";
%typemap(directorin,descriptor="Lsome.ns.typemap(jstype, some::ns::TheThing);") std::shared_ptr<some::ns::TheThing> %{
     *($&1_type*)&j$1 = &$1;
%}


%include "test_thing.h"
%include "thing_callback.h"

%{
#include <memory>

#include "test_thing.h"
#include "thing_callback.h"

%}

%std_function(Functor, void, std::shared_ptr<some::ns::TheThing>);

%{
#include <iostream>
void add_and_print(std::shared_ptr<some::ns::TheThing> thing) {
  std::cout << "here\n";
}
%}

%callback("%s_cb");
void add_and_print(std::shared_ptr<some::ns::TheThing>);
%nocallback;

%inline %{
  std::function<void(std::shared_ptr<some::ns::TheThing>)> make_functor() {
    return [](std::shared_ptr<some::ns::TheThing>){
      std::cout << "make functor\n";
    };
  }

  void do_things(std::function<void(std::shared_ptr<some::ns::TheThing>)> in) {
      std::cout << "inside do things\n";
  }
%}
Run Code Online (Sandbox Code Playgroud)

test_thing.h就是我上面发布的内容,而thing_callback.h是我在原始问题中发布的代码.这一切都通过swig,c ++和Java链进行编译而没有错误,但看起来swig在连接c ++和Java之间的点时遇到了一些麻烦.它创建了这三个类:

SWIGTYPE_p_f_std__function__f_std__shared_ptr__some__ns__TheThing____void____void
SWIGTYPE_p_f_std__shared_ptr__some__ns__TheThing____void
SWIGTYPE_p_std__functionT_void_fstd__shared_ptrT_some__ns__TheThing_tF_t
Run Code Online (Sandbox Code Playgroud)

不幸的是,简单的Java主代码中的大多数方法现在都接受或返回这些对象,这使得它们相当不可用.知道如何解决这个问题吗?谢谢!

更完整的细节:我使用以下三个脚本来编译和运行代码.参数略有不同,但我认为不重要.在我的最后,它被设置为Eclipse maven项目.这些脚本位于我的项目的根目录中,header和swig文件驻留在src/main/resources中,java源文件驻留在src/main/java中,java编译的类驻留在target/classes中.Eclipse执行java编译.

swigthing.sh

MODULE_NAME=Thing
PACKAGE=some.ns
OUTDIR=./src/main/java/some/ns
I_FILE=./src/main/resources/func_thing_test.i

mvn clean

rm $OUTDIR/*.*
mkdir -p $OUTDIR

swig -java -c++ -module $MODULE_NAME -package $PACKAGE -outdir $OUTDIR $I_FILE

./compileThingSwigTest.sh
Run Code Online (Sandbox Code Playgroud)

compileThingSwigTest.sh

#!/bin/bash

pushd src/main/resources
g++ -c -std=gnu++11 -fpic \
func_thing_test_wrap.cxx \
-I/usr/lib/jvm/java/include \
-I/usr/lib/jvm/java/include/linux

g++ -shared func_thing_test_wrap.o -o libFunc.so
popd
Run Code Online (Sandbox Code Playgroud)

runThingTest.sh

pushd target/classes
java -Xmx512M -Xms512M -Djava.library.path=. some.ns.test.RunThingTest
popd
Run Code Online (Sandbox Code Playgroud)

最后更新

修复了上面的代码,将正确的参数传递给std_function.现在,问题和答案之间有一个完整的工作实例,说明我所追求的是什么.

Fle*_*exo 9

我们可以通过一些工作来做到这一点.我在这里的答案是我的一个答案的更通用的版本,针对特定实例查看此问题并针对Python.

我假设你想通过以下方式实现四件事std::function:

  1. 我们希望能够std::function从Java代码中调用对象.
  2. 包装的std::function对象需要像任何其他对象一样被传递,包括跨越任一方向的语言边界.
  3. 应该可以std::function在Java内部编写对象,这些对象可以传递回C++,而无需修改在std::function对象上工作的现有C++代码(即维护std::function交叉语言的类型擦除)
  4. 我们应该能够std::function使用C++指向函数类型的指针在Java中构造对象.

我将研究这些并展示我们如何实现这一目标.在可能的情况下,我将保持解决方案语言不可知.

出于讨论的目的,我shared_ptr对你问题的部分进行了修饰,它实际上并没有改变一些事情,因为当你已经开始shared_ptr工作时,实际上也足以在这个场景中使用它,这只会使我的例子更加冗长不必要.

我正在努力的解决方案实际上是shared_ptr在SWIG 的现有支持之后建模的.我已经整理了一个测试界面来说明它将如何使用:

%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";
    };
  }
%}
Run Code Online (Sandbox Code Playgroud)

基本上使用这个你需要做的就是包含文件"std_function.i",然后使用%std_function带参数的宏:

%std_function(Name, Ret, ...)
Run Code Online (Sandbox Code Playgroud)

在每个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__)> {
    // Copy constructor
    function<Ret(__VA_ARGS__)>(const std::function<Ret(__VA_ARGS__)>&);

    // Call operator
    Ret operator()(__VA_ARGS__) const;
  };
}

%enddef
Run Code Online (Sandbox Code Playgroud)

这包括生成的包装器代码中的C++头文件,然后设置宏以便在接口中使用.在这种使用场景中,SWIG对C++ 11可变参数模板的支持实际上对我们没什么帮助,因此我编写的宏基本上使用C99可变参数宏参数重新实现了默认模板扩展功能(支持得更好).巧合的是,这意味着我们编写的SWIG代码将适用于2.x甚至一些1.3.x版本.(我用2.x测试过).即使/当您的SWIG版本确实具有保留此宏的%template支持时,std::function对于使其实际可调用的其余粘合剂仍然有用.

std:function模板的手动扩展仅限于我们关注的用途:实际operator()和复制构造函数可能会派上用场.

唯一要做的事情就是重命名operator()为与目标语言匹配的东西,例如,Java将其重命名为仅称为"call"的常规函数​​,或者如果您将Python定位到__call__或使用tp_slots(如果需要).

现在这足以让我们的界面工作,为了演示它我写了一点Java:

public class run {
    public static void main(String[] argv) {
        System.loadLibrary("test");
        test.make_functor().call(1,2.5);
    }
}
Run Code Online (Sandbox Code Playgroud)

我编译的是:

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
Run Code Online (Sandbox Code Playgroud)

它起作用了.


此时要求#4很容易从列表中删除.我们需要做的就是告诉SWIG还有另一个构造函数,std::function它接受兼容的函数指针:

// Conversion constructor from function pointer
function<Ret(__VA_ARGS__)>(Ret(*const)(__VA_ARGS__));
Run Code Online (Sandbox Code Playgroud)

然后我们可以使用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";
    };
  }
%}
Run Code Online (Sandbox Code Playgroud)

然后我们用来调用它的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);
    }
}
Run Code Online (Sandbox Code Playgroud)

我们在这一点上成功地编译和运行.

(注意,在此时创建一​​些以"SWIGTYPE_p_f _..."开头的Java类是正常的,也是可取的 - 这些Java包装了指向函数构造函数和回调常量的"指向函数的指针"类型)


要求#3是事情开始变得棘手的地方.基本上我们遇到了同样的问题,因为我之前使用SWIG生成Java接口,但现在我们希望在宏中更普遍地进行.

事实证明,在这个例子中,因为我们想要生成的接口非常简单,我们可以在宏中使用一些技巧让SWIG为我们生成它.

为了使这项工作,我们需要做的主要事情是设置SWIG导向器以提供跨语言多态性并允许用Java编写的东西来实现C++接口.这是我的代码中使用后缀"Impl"生成的类.

为了让Java开发人员"感觉正确",我们仍然希望对C++和Java实现的std::function对象使用相同的类型.即使std::function::operator()是虚拟的,我们仍然不希望SWIG导演直接使用该类型,因为std::function通过值传递会导致类型切片问题是很常见的.因此,当Java开发人员扩展我们的std::function对象和覆盖时,call我们需要做一些额外的工作来使它使得使用该对象的C++实际上调用Java实现,因为我们不能仅使用director来自动处理它.

所以我们所做的看起来有点奇怪.如果构造一个旨在实现的Java对象,std::function那么就有一个特殊的受保护构造函数.此构造函数保留swigCPtr成员变量,该变量通常指向真正的C++对象为0,而是创建一个匿名包装器对象,该对象实现"Impl"接口并简单地将所有内容代理回callJava对象的成员.

我们还有另一个类型映射,可以在Java中将std::function对象传递给C++.它的作用是检测我们拥有哪种情况 - 一个C++实现的std::function对象,或一个Java对象.在C++的情况下,它没有什么特别的,一切都正常进行.在Java情况下,它接受代理对象并要求C++将其转换回另一个std::function替代的单独实例.

这足以让我们在两种语言中都能获得我们想要的行为,而不会在任何一方感到奇怪(除了透明地发生的大量机械提升).

这里的问题是自动构造代理对象并非易事.Java将动态代理类作为反射API的一部分,但这些只实现接口,而不是扩展抽象类.我尝试使用的一种可能性是void call(Object ...args)在Java端,这是一个可变参数函数参数.虽然合法,但这似乎并没有真正覆盖超级类中的任何案例.

我最终做的是调整一些宏以我想要的方式迭代可变参数宏参数.这是一个相当明智的解决方案,因为我们已经决定出于其他原因使用可变参数C99宏参数.这个机制在我的解决方案中总共使用了四次,一次在函数声明中,一次在Java和C++的delgated调用中.(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);
    }
}
Run Code Online (Sandbox Code Playgroud)

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

// These are the things we actually use
#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)

// This is the mechanics
#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 ";" // Suppress the body of the abstract method

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

  // Bot of these are retained to prevent garbage collection from happenign to early
  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__)> {
    // Copy constructor
    function<Ret(__VA_ARGS__)>(const std::function<Ret(__VA_ARGS__)>&);

    // Call operator
    Ret operator()(__VA_ARGS__) const;

    // Conversion constructor from function pointer
    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
Run Code Online (Sandbox Code Playgroud)

并且test.i稍微扩展以验证Java - > C++传递std::function对象并启用导向器:

%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);
  }
%}
Run Code Online (Sandbox Code Playgroud)

这与前面的示例一样编译和运行.值得注意的是,我们已经编写了大量Java特定代码 - 尽管如果您针对Python设计其他语言,但使用Python特定功能修复其中一些问题要简单得多.

我想改进两件事:

  1. 使用C++ 14可变参数lambda来避免我用来保持它们与C++ 11兼容的宏预处理器魔法.如果您有C++ 14,%extend构造函数将变为:

    %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)...);
        });
      }
    }
    
    Run Code Online (Sandbox Code Playgroud)

std::shared_ptr按预期使用此宏时,宏本身不需要更改.然而,应用的javadirectorin和directorin类型映射的实现存在问题,这确实阻止了"正常工作".即使从"主干"构建SWIG也是如此.(关于组合导演和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;
%}
Run Code Online (Sandbox Code Playgroud)

这两个类型映射中的第一个实际上是死代码,因为我们强制"调用"方法在我们的抽象类中是抽象的,但是修复此方法的编译比修改它更容易.第二个类型图很重要.它与普通的"out"类型映射基本类似,它创建了一个jlong实际上只是C++指针的表示,即它准备一个对象从C++转到Java.

请注意,如果在模块中使用软件包,"L$packagepath/$typemap(...);"或者只是手动编写软件包,则可能需要修改directorin typemap的descriptor属性.

这应该删除现在生成的虚假"SWIGTYPE_p_sstd__shared_ptr ..."类型.如果你有返回shared_ptr对象的虚函数,你也需要为它们编写directorout和javadirectorout类型图.这些可以基于普通的"in"类型映射.

这对于我自己的简单测试来说已经足够了,经过修改Functor可以工作,至少我今天从主干检出了我的SWIG版本.(我对2.0.x的测试失败,我没有花太多精力使其工作,因为这是一个已知的工作进展区域).