使用Swig包装Fluent接口

2024-06-07 14:41:35 发布

您现在位置:Python中文网/ 问答频道 /正文

我使用SWIG来封装C++中实现的类。此类使用fluent接口来允许方法链接。也就是说,修改对象状态的方法返回对对象的引用,从而允许调用下一个状态修改方法。例如:

class FluentClass {
public:
    ...
    FluentClass & add(std::string s)
    {
        state += s;
        return *this;
    }
    ...
private:
    std::string state;
};

方法add将给定的字符串s添加到state中,并返回对对象的引用,该对象允许链接add的多个调用:

^{pr2}$

您可以在:https://en.wikipedia.org/wiki/Fluent_interface上找到更全面的示例

我编写了几个swing文件(没有什么特别的)来为多种语言创建绑定,特别是C#、Java、Python和Ruby。以下示例(Python)按预期工作:

fc = FluentClass()
fc.add("hello").add("world!")

但是,以下内容不包括:

fc = FluentClass()
fc = fc.add("hello").add("world!")

我发现对fc调用add并没有返回{}的引用,而是对一个新创建的对象的引用(我希望其他绑定也会这样做),该对象实际上包装了相同的内存:

fc = FluentClass()
nfc = fc.add("hello world!")
fc != nfc, though fc and nfc wrap the same memory :(

因此,将add的结果赋给同一个变量会导致作为垃圾回收一部分的原始对象被破坏。结果是fc现在指向无效内存。在

所以我的问题是:您知道如何正确地包装FluentClass,让add返回相同的引用以防止垃圾回收吗?在


Tags: 对象方法内存add示例helloworldstring
2条回答

问题是,当构建Python代理时,你的实例被破坏,底层C++对象会被删除。由于SWIG不知道返回的值是对同一对象的引用,所以当您调用add时,SWIG将构造一个新的代理。因此,在您观察到错误的情况下,原始对象的ref count在链接方法完成之前达到0。在

为了首先调查和修复,我创建了一个测试用例来正确地再现这个问题。很流利。h:

#include <string>

class FluentClass {
public:
    FluentClass & add(std::string s)
    {
        state += s;
        return *this;
    }
private:
    std::string state;
};

足够的代码在Python测试中可靠地命中SEGFAULT/SIGABRT:

^{pr2}$

以及一个SWIG接口文件来构建“test”模块:

%module test

%{
#include "fluent.h"
%}

%include <std_string.i>

%include "fluent.h"

有了这些额外的工作,我得以重现你所报告的问题。(注意:在本文中,我的目标是使用python3.4实现swig3.0)。在

您需要编写类型映射来处理“returned value==this”的特殊情况。我最初想以特殊的'this'参数的argout类型映射为目标,因为这感觉是进行此类工作的正确位置,但不幸的是,这也匹配了析构函数调用,这会使正确编写类型映射比需要的困难,所以我跳过了这一步。在

在我的outtypemap中,它只适用于流畅的类型,我检查我们是否真的符合“输入即输出”的假设,而不是简单地返回其他东西。然后它会碰撞输入的引用计数,这样我就可以用预期的语义安全地返回它。在

为了在outtypemap中工作,我们需要做更多的工作来安全可靠地捕获输入Python对象。这里的问题是SWIG生成了以下函数签名:

SWIGINTERN PyObject *_wrap_FluentClass_add(PyObject *SWIGUNUSEDPARM(self), PyObject *args) {

其中SWIGUNUSEDPARAM marco扩展到根本不命名第一个参数。(这在宏定义中看起来像是一个bug,因为它是GCC的次要版本,它决定了哪个选项在C++模式中被选中,但是我们仍然希望它仍然有效。)在

<> p>所以我最后做的是在Type映射中编写一个自定义的自定义函数,它可以捕获C++指针和与之关联的Python对象。(即使您启用了其他参数解包样式之一,它的编写方式仍然有效,并且应该对其他变体保持健壮。但是,如果您将其他参数命名为“self”),则它将失败。为了将值放在可以从后面的'out'类型映射中使用的地方,并且不存在交叉goto语句的问题,我们需要在declaring local variables时使用_global_前缀。在

最后,我们需要在不流畅的情况下做一些理智的事情。因此,生成的文件如下所示:

%module test

%{
#include "fluent.h"
%}

%include <std_string.i>
%typemap(in) SWIGTYPE *self (PyObject *_global_self=0, $&1_type _global_in=0) %{
  $typemap(in, $1_type)
  _global_self = $input;
  _global_in = &$1;
%}

%typemap(out) FLUENT& %{
  if ($1 == *_global_in) {
    Py_INCREF(_global_self);
    $result = _global_self;
  }
  else {
    // Looks like it wasn't really fluent here!
    $result = SWIG_NewPointerObj($1, $descriptor, $owner);
  }
%}

%apply FLUENT& { FluentClass& };

%include "fluent.h"

在这里使用%apply可以使控制在哪里使用它变得简单和通用。在


另外,您还可以告诉SWIG,FluentClass::add函数使用其第一个参数并创建一个新参数,使用:

%module test

%{
#include "fluent.h"
%}

%include <std_string.i>

%delobject FluentClass::add;
%newobject FluentClass::add;

%include "fluent.h"

它通过将第一个代理的死亡与实际的delete调用分离,以更简单的方式生成更正确的代码。同样地,尽管为每个方法编写这个代码会更加冗长,而且它也不会在所有情况下都是正确的,即使在我的测试用例中它是正确的,例如

f1=test.FluentClass()
f2=f.add("hello").add("world") # f2 is another proxy object, which now owns
f3=f1.add("again") # badness starts here, two proxies own it now....

下面的代码适用于ruby和python。在

%{
typedef FluentClass FC_SELF;
%}

%typemap(out) FC_SELF& { $result = self; }

class FluentClass {
public:
  FC_SELF& add(const std::string& s);
};

“self”是Ruby和pythoncapi中用来引用self对象的C指针的变量名。因此,如果方法的返回类型是FC_SELF,则该方法将返回SELF对象。其他语言也一样。但使用智能指针绝对是一个更好的解决方案,这将是其他答案。在

相关问题 更多 >

    热门问题