【问题标题】:How to use SWIG to wrap std::function objects?如何使用 SWIG 包装 std::function 对象?
【发布时间】:2015-12-15 03:29:35
【问题描述】:

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

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

virtual std::shared_ptr<some::ns::TheThing> getTheThing(unsigned short thingID);

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

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

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

virtual std::vector<std::shared_ptr<some::ns::TheThing>> getAllTheThings() const = 0;

并使用这样的模板成功处理它们:

%template(ThingVector) std::vector<std::shared_ptr<some::ns::TheThing>>;

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

 void registerThingCallback(std::function<void(std::shared_ptr<some::ns::TheThing>) > func);

我无法让 SWIG 正确包装它。我尝试过使用 %callback、directors、%template 和 %inline 功能代码,因为我已经看到了所有这些东西的示例,但还没有得到任何似乎接近工作的东西。如果有帮助(已清理和减少),这里有更多关于函数调用的上下文:

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

    };

  }
}

更新

根据下面 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 */

这是我的 .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";
  }
%}

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

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

为了完整起见,更详细一点:我正在使用以下三个脚本来编译和运行代码。参数略有不同,但我认为这并不重要。就我而言,它被设置为 Eclipse maven 项目。这些脚本位于我项目的根目录中,头文件和 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

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

runThingTest.sh

pushd target/classes
java -Xmx512M -Xms512M -Djava.library.path=. some.ns.test.RunThingTest
popd

上次更新

修复了上面的代码以将正确的参数传递给 std_function。现在在问题和答案之间有一个我所追求的完整工作示例。

【问题讨论】:

  • 您的目标是什么语言? stackoverflow.com/a/11522655/168175 是一个起点,但也有其他选择。
  • D'oh 刚刚看到 Java 标记。我会尝试在周末找到答案。
  • 对不起,如果不清楚,是的,它是 Java。提前谢谢您,我会查看您在此期间发布的链接。

标签: java c++ c++11 swig std-function


【解决方案1】:

虽然 SWIG 本身不提供 std_function.i,但我们可以通过一些工作自己构建一个。我在这里的回答是我的previous of mine answer 的更通用版本,针对特定实例并针对 Python 来查看这个问题。我将对其进行多次迭代,为通用 std::function 包装定义一个 %std_function 宏。

我假设您希望通过包装 std::function 实现四件事,这成为我们的主要要求:

  1. 我们希望能够从 Java 代码中调用 std::function 对象。
  2. 包装后的 std::function 对象需要像任何其他对象一样被传递,包括在任一方向跨越语言边界。
  3. 应该可以在 Java 中编写 std::function 对象,可以将其传递回 C++,而无需修改适用于 std::function 对象的现有 C++ 代码(即维护 std::function 跨语言的类型擦除)
  4. 我们应该能够使用指向函数类型的 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, Ret, ...)

您要包装的std::function 模板的每个实例化调用一次,其中Name 是您要在Java 中调用的类型,Ret 是返回类型,然后剩余的(可变参数)参数是输入到您的功能。所以在我上面的测试界面中,我基本上是想包装std::function&lt;void(int,double)&gt;

编写“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

这会在生成的包装代码中包含一次 C++ 头文件,然后设置宏以在接口中使用。在这种使用场景中,SWIG 对C++11 variadic templates 的支持实际上对我们不是很有帮助,因此我编写的宏基本上使用 C99 可变参数宏参数(得到更好的支持)重新实现了the default template expansion functionality。巧合的是,这意味着我们正在编写的 SWIG 代码将适用于 2.x 甚至某些 1.3.x 版本。 (我用 2.x 测试过)。即使/当您的 SWIG 版本确实具有与 std::function 一起使用的 %template 支持时,保留此宏对于使其实际可调用的其余粘合剂仍然有用。

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

我使用的编译器:

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

它成功了。


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

// Conversion constructor from function pointer
function<Ret(__VA_ARGS__)>(Ret(*const)(__VA_ARGS__));

然后我们可以将它与 SWIG 中的%callback mechanism 一起使用,我们的测试接口文件变为:

%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 类是正常​​且可取的 - 这些包装了指向函数构造函数的指针使用的“指向函数”类型和回调常量)


要求 #3 是事情开始变得棘手的地方。本质上,我们遇到了与我之前回答 on making SWIG generate an interface in Java 相同的问题,但现在我们希望在更通用的宏中执行此操作。

事实证明,在这种情况下,因为我们想要生成的界面相当简单,我们可以在宏中使用一些技巧来让 SWIG 为我们生成它。

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

为了让 Java 开发人员“感觉正确”,我们希望仍然对 C++ 和 Java 实现的 std::function 对象使用相同的类型。即使 std::function::operator() 是虚拟的,我们仍然不希望 SWIG 主管直接使用该类型,因为通过值传递 std::function 很常见,这将导致类型 slicing problems。因此,当 Java 开发人员扩展我们的 std::function 对象并覆盖 call 时,我们需要做一些额外的工作,以便使用该对象的 C++ 实际上调用 Java 实现,因为我们不能只使用导向器来处理它自动。

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

我们也有另一个类型映射,在 Java 中,我们将 std::function 对象传递给 C++ 的任何地方。它的作用是检测我们有哪种情况——一个 C++ 实现的 std::function 对象,还是一个 Java 对象。在 C++ 的情况下,它没有什么特别的,一切都照常进行。在 Java 案例中,它接受代理对象并要求 C++ 将其转换回另一个单独的 std::function 实例,而该实例被替换。

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

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

我最终做的是调整 some macros 以按照我想要的方式迭代可变参数宏参数。这是一个相当明智的解决方案,因为我们已经出于其他原因决定使用可变参数 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
%}

// 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

并且 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);
  }
%}

这与前面的示例一样编译和运行。值得注意的是,我们已经开始编写大量 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)...);
        });
      }
    }
    

如预期的那样将此宏与std::shared_ptr 一起使用时,宏本身不需要更改。然而,应用的 javadirectorin 和 directorin 类型映射的实现存在问题,这确实会阻止事情“正常工作”。即使从“主干”构建 SWIG 也是如此。 (combining directors and shared_ptr有一个悬而未决的问题)

不过,我们可以通过在调用 %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="L$typemap(jstype, some::ns::TheThing);") std::shared_ptr<some::ns::TheThing> %{ 
  *($&1_type*)&j$1 = &$1;
%}

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

请注意,如果您在模块中使用包,则可能需要修改directorin 类型映射的描述符属性,修改为"L$packagepath/$typemap(...);" 或简单地手动编写。

这也应该删除现在生成的虚假“SWIGTYPE_p_sstd__shared_ptr...”类型。如果您有返回 shared_ptr 对象的虚函数,您还需要为它们编写directorout 和javadirectorout 类型映射。这些可以基于正常的“in”类型映射。

这足以让我自己使用修改后的Functor 进行简单测试,至少在我今天从主干中检出的 SWIG 版本中。 (我对 2.0.x 的测试失败了,因为这是一个已知的正在进行的工作领域,所以我没有花太多精力让它工作)。

【讨论】:

  • 哇,这是一个(典型的你看你的历史)奇妙的答案。我还在完成你今天早些时候发布的前半部分,我需要花一点时间来完成并消化所有这些。我要么跟进问题,要么在我感觉良好时接受。谢谢!
  • 我马上就有一个问题是这是否都适用于 swig 3.0?
  • @Denial 据我所知,SWIG 3.0 中的可变参数模板支持仍然不足以正确处理 std::function,但我所写的内容在可预见的未来应该仍然有效。
  • @Denial 我刚刚进行的两个编辑中的第一个修复了一堆错误,一个是我昨天在答案中提到的垃圾收集问题,另一个是如果您打电话,则停止完整解决方案的工作除了Functor 之外的任何其他类。
  • 好的,我有机会解决这个问题,并在问题中发布了更新的详细信息。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多