SWIG的Python包装中临时对象的生命周期 (?)

7 投票
2 回答
2232 浏览
提问于 2025-04-16 11:40

编辑于2月12日

我最近遇到了一个奇怪的崩溃问题,涉及到一些用SWIG生成的Python包装器和C++类。看起来SWIG和Python的组合在处理临时值时非常积极,甚至积极到在这些值还在使用的时候就把它们清理掉了。简化后的情况大致是这样的:

/* Example.hpp */
struct Foo {
    int value;
    ~Foo();
};

struct Bar {
    Foo theFoo;
    Bar();
};

/* Example.cpp */
#include "Example.hpp"
Bar::Bar()  {theFoo.value=1;}
Foo::~Foo() {value=0;}

/* Example.i */
%module Example
%{
#include "Example.hpp"
%}
%include "Example.hpp"

我在.i文件上运行SWIG(版本1.3.37),然后在Python中有:

Python 2.4.3 (#1, Sept 17 2008, 16:07:08)
[GCC 4.1.2 20071124 (Red Hat 4.1.2-41)] on linux2
Type "help", "copyright", "credits", or "license" for more information.
>>> from Example import Bar
>>> b=Bar()
>>> print b.theFoo.value      # expect '1', since Bar's constructor sets this
1
>>> print Bar().theFoo.value  # expect '1', since we're still using the Foo object
26403424

在第二个例子中,临时的Bar对象在我们还没来得及读取theFoovalue字段时就被销毁了。通过在gdb中追踪,这一点非常明显。因此,当我们从Bar().theFoo读取.value时,C++已经把.theFoo销毁了(并且用其他内存分配覆盖了它)。在我的实际情况中,这导致了一个段错误(segfault)。

有没有什么SWIG的指令或者技巧可以让我在Example.i文件中添加,以便让Bar().theFoo.value在这里返回1呢?

2 个回答

1

解决办法是在你的 .i 文件中添加 %naturalvar,像这样:

%naturalvar Bar::theFoo;
%include "Example.hpp"

这样做会让 SWIG 返回 Foo 的一个副本,而不是它的引用,这样就解决了 Python 对临时对象进行过于积极的清理的问题。

2

第二次更新

我们知道基本问题是,Python 会立即销毁 Bar。当 Bar 在 Python 中实现时,Python 的垃圾回收机制知道还有对 theFoo 的引用,所以不会销毁它。但当 Bar 是用 C++ 实现时,Python 会调用 C++ 的析构函数,这样就会自动销毁 theFooBar

所以,显而易见的解决办法就是防止 Python 提前销毁 Bar。这里有一个稍微有点“黑科技”的解决方案,涉及到对 Bar 的子类化:

class PersistentBar(swigexample.Bar):
    lastpbar = None
    def __init__(self):
        super(PersistentBar, self).__init__()
        PersistentBar.lastpbar = self

这个方法保存了最后一个创建的 Bar 的引用,这样它就不会立刻被销毁。当创建一个新的 Bar 时,旧的就会被删除。(我之前的版本有点傻;其实没必要重写 __del__。)这是输出结果(在 Foo 的析构函数中有 cout << "deleting Foo "):

>>> from test import persistentBar
>>> persistentBar().theFoo.value
1
>>> persistentBar().theFoo.value
deleting Foo 1
>>> persistentBar().theFoo.value
deleting Foo 1

我还是不太喜欢这个方法。把“持久化”的行为放在一个装饰器里可能更好;我也试过这个,效果不错(如果你想看代码可以告诉我)。当然,最好是能让 Python 自己处理 theFoo 的销毁,但我还没找到办法。

第一次更新

包裹代码没有告诉我任何信息,所以我查看了 swigexample.py。结果也没什么收获。当我尝试在纯 Python 中复制 Bar 时,事情变得清晰了:

# pyfoobar.py
class Foo(object):
    def __init__(self):
        self.value = -1

class Bar(object):
    def __init__(self):
        self.theFoo = Foo()
        self.theFoo.value = 1
    def __del__(self):
        self.theFoo.value = 0

现在我们从 pyfoobar 导入 Bar

>>> from pyfoobar import Bar
>>> b = Bar()
>>> b.theFoo.value
1
>>> Bar().theFoo.value
0

这种行为是来自 Python 的!

原始回答

看起来这里确实有一些垃圾回收的斗争在进行中……这里有一些关于 SWIG 内存管理 的相关信息。根据这些信息,%newobject 指令可能是你需要的;但我尝试了几种变体,还是没能让 Python 控制 theFoo

>>> from swigexample import Bar
>>> b = Bar()
>>> b.theFoo.value
1
>>> b.theFoo.thisown
False
>>> Bar().theFoo.value
0
>>> Bar().theFoo.thisown
False

我开始怀疑这可能是故意的;似乎上面链接中的这一行与此相关:

C 现在持有对该对象的引用——你可能不希望 Python 销毁它。

但我不确定。我打算查看 swigexample_wrap 代码,看看能否弄清楚 ~Bar 何时被调用。

撰写回答