使用 Swig 包装 Fluent 接口

Using Swig to Wrap Fluent Interfaces

我正在使用 Swig 包装一个用 C++ 实现的 class。 class 使用流畅的界面来允许方法链接。也就是说,修改对象状态的方法 return 对对象的引用,因此允许调用下一个状态修改方法。例如:

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

方法 add 将给定的字符串 s 添加到 state 和 return 对象的引用允许一个链接多个调用 add:

FluentClass fc;
c.add(std::string("hello ")).add(std::string("world!"));

您可以在以下位置找到更全面的示例: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 并不是 return fc 的引用,而是一个引用(我希望其他绑定会执行相同)到一个新创建的对象,实际上包装了相同的内存:

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

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

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

以下代码适用于 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中使用的C指针的变量名,用来引用一个self对象。所以如果一个方法的 return 类型是 FC_SELF,该方法将 return 自身对象。同样的技巧也适用于其他语言。但是使用智能指针肯定是更好的解决方案,这将在其他答案中。

问题是,当您构造实例时创建的 Python 代理被销毁时,底层 C++ 对象将被删除。由于 SWIG 不知道 returned 值是对同一对象的引用,因此它会在您调用 add 时构造一个新代理。因此,在您观察到错误的情况下,原始对象在链式方法完成之前的引用计数为 0。

为了首先调查和解决问题,我创建了一个测试用例来正确重现问题。这是 fluent.h:

#include <string>

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

在 Python:

中的测试中有足够的代码可靠地命中 SEGFAULT/SIGABRT
import test

def test_fun():
    f=test.FluentClass()
    f=f.add("hello").add("world")

    return f

for i in range(1000):
    f2=test_fun()
    f2.add("moo")

以及用于构建模块的 SWIG 接口文件 'test':

%module test

%{
#include "fluent.h"
%}

%include <std_string.i>

%include "fluent.h"

完成这项额外工作后,我能够重现您报告的问题。 (注意:在整个过程中,我的目标是 Python 3.4 的 SWIG 3.0)。

您需要编写类型映射来处理 'returned value == this' 的特殊情况。我最初想针对特殊 'this' 参数的 argout 类型映射,因为感觉这是做这种工作的正确位置,但不幸的是,它也匹配析构函数调用,这会使正确编写类型映射变得更加困难超出需要,所以我跳过了。

在我的输出类型图中,它只适用于流畅的类型,我检查我们是否确实满足 "input is output" 假设,而不是简单地 return 其他东西。然后它会增加输入的引用计数,以便我可以安全地 return 它具有预期的语义。

尽管我们需要做更多的工作来安全稳健地捕获输入 Python 对象,但为了在输出类型映射中实现这一点。这里的问题是 SWIG 生成了以下函数签名:

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

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

所以我最后做的是在类型映射中编写一个自定义,它可靠地捕获 C++ this 指针和与之关联的 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, _type)
  _global_self = $input;
  _global_in = &;
%}

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

%apply FLUENT& { FluentClass& };

%include "fluent.h"

在这里使用 %apply 可以简单而通用地控制 this 的使用位置。


顺便说一句,您还可以告诉 SWIG 您的 FluentClass::add 函数使用其第一个参数并创建一个新参数,使用:

%module test

%{
#include "fluent.h"
%}

%include <std_string.i>

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

%include "fluent.h"

通过将第一个代理的死亡与真正的删除调用分离,以更简单的方式生成更正确的代码。同样,尽管必须为每个方法编写此代码更加冗长,并且在所有情况下它仍然是正确的,即使在我的测试用例中它是正确的,例如

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