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

10

我看到了很多类似的问题,但没有找到解决我的问题的方法。我试图将一些使用std::function的C++11代码转化为SWIG,以便在我的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传递正确的参数。现在,在问题和答案之间有一个完整的可工作示例,符合我所需要的。


你要使用哪种编程语言?https://dev59.com/9mbWa4cB1Zd3GeqPUDHZ#11522655 是一个起点,但还有其他选择。 - Flexo
我看到Java标签了,我会在周末尝试回答。 - Flexo
抱歉如果之前表述不清楚,是的,这是Java。非常感谢您的帮助,我会在此期间查看您提供的链接。 - Denial
1个回答

12

虽然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, Ret, ...)

您需要每个实例化的 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

这将在生成的包装代码中包含 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
%}

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

测试代码稍作修改以验证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特定功能来解决这些问题会更简单。

我想要改进两件事:

  1. 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测试失败了,我没有花费太多精力使其工作,因为这是一个已知的正在进行的工作区域)。

哇,这是一个(根据您的历史来看)非常棒的答案。我仍在努力理解您今天早些时候发布的前半部分,需要一点时间来消化所有内容。我会跟进问题或在感觉良好时接受。谢谢! - Denial
据我所知,SWIG 3.0中的可变参数模板支持仍然不足以正确处理std::function,但我编写的代码在可预见的未来仍应该能够正常工作。 - Flexo
@Denial,我刚刚进行的两次编辑中,第一次修复了许多 bug,其中一个是我昨天在答案中提到的垃圾回收问题,另一个是如果你将该类命名为任何除 Functor 以外的名称,就会导致完整解决方案无法工作。 - Flexo
好的,我已经有机会处理这个问题,并在问题中发布了新细节的更新。 - Denial
@Denial 我已经使用shared_ptr使其工作 - 这是C++11支持的已知限制(http://www.swig.org/Doc3.0/Library.html#Library_std_shared_ptr),但幸运的是,在这种情况下,我们可以在我们的模块中解决它(而不是在std_function.i中)以获得一些工作。我还添加了一条关于SWIGTYPE_p_f类型某些来源的注释。 - Flexo
显示剩余11条评论

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