使用 Swig 封装 Fluent 接口

Posted

技术标签:

【中文标题】使用 Swig 封装 Fluent 接口【英文标题】:Using Swig to Wrap Fluent Interfaces 【发布时间】:2016-04-13 08:32:14 【问题描述】:

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

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

add 方法将给定的字符串 s 添加到 state 并返回对对象的引用,允许链接多个 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 不会返回fc 的引用,而是返回一个新创建的对象的引用(我希望其他绑定也会这样做)实际上包装相同记忆:

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

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

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

【问题讨论】:

您有内存所有权问题。 (对构造函数的调用会导致一个代理“拥有”它所指向的东西,当创建新的、非拥有的代理时,该代理会被销毁)。我认为我们可以使用一个可爱的技巧来使这项工作变得更好,但我要到今晚晚些时候才能验证它。 【参考方案1】:

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

为了首先进行调查和修复,我创建了一个测试用例来正确重现问题。这是流利的。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 接口文件来构建模块“测试”:

%module test

%
#include "fluent.h"
%

%include <std_string.i>

%include "fluent.h"

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

您需要编写类型映射来处理“返回值 == this”的特殊情况。我最初想针对特殊“this”参数的 argout 类型映射,因为这感觉是进行此类工作的正确位置,但不幸的是,这也与析构函数调用相匹配,这会使正确编写类型映射比需要的更难所以我跳过了。

在我的输出类型图中,它只适用于流畅的类型,我检查我们确实满足“输入就是输出”的假设,而不是简单地返回其他东西。然后它会增加输入的引用计数,以便我可以按照预期的语义安全地返回它。

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

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

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

所以我最终做的是在 typemap 中编写一个自定义,它可靠地捕获 C++ this 指针和与之关联的 Python 对象。 (即使您启用了其他参数解包样式之一并且应该对其他变体具有健壮性,它的编写方式也有效。但是,如果您将其他参数命名为“self”,它将失败)。要将值放在可以从以后的“输出”类型映射中使用的位置,并且在跨越 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"

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

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

【讨论】:

到目前为止,您的解决方案看起来不错。但是,您真的认为我们需要增加引用计数吗 (Py_INCREF(_global_self);)。我的意思是,我们返回相同的实例 ($result = _global_self;),因此,我希望 f2 等于 f (f2 == f)。不过,我还没有测试它。 在 Python 中,您现在有两种方法来引用同一事物。所以它的引用计数应该是 2。我想我可以更清楚地证明这一点,所以我会在周末写更多的细节。 (简短的版本是输入参数只是借来的引用)。出于比较目的,即使您返回单例全局 Py_True ,您仍然需要增加引用计数才能正确。【参考方案2】:

以下代码适用于 ruby​​ 和 python。

%
typedef FluentClass FC_SELF;
%

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

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

“self”是 Ruby 和 Python C API 中用于引用 self 对象的 C 指针的变量名。所以如果一个方法的返回类型是FC_SELF,该方法将返回self对象。同样的技巧也适用于其他语言。但是使用智能指针绝对是更好的解决方案,这将在其他答案中。

【讨论】:

即使这个 typemap 可以编译(我还不能测试它,但预计它不会),它肯定不会在 Python 中得到正确的引用计数。 这不能与针对 Python 3 的 SWIG 3.0 一起编译 - SWIGUNUSED 的定义使得被忽略的函数参数实际上没有名称并且无法访问。你还需要一个Py_INCREF 来获得正确的引用计数,我认为我不建议使用 typedef 这样。 (%apply 将是获得相同结果的更好方法)。

以上是关于使用 Swig 封装 Fluent 接口的主要内容,如果未能解决你的问题,请参考以下文章

如何在 Laravel 的 fluent 中进行子查询?

如何检查是不是可以在 Fluent Assertion 中使用 ContainValue 验证类型类的字典

SWIG 之一:基础入门

python/c++接口库比较(SWIG,boost.python, pycxx, py++, sip, Weave, Pyrex )

如何使用swig为c++生成php接口so

QuantLib 金融计算——自己动手封装 Python 接口