Python 包含复杂类型的SWIG-ed C结构的引用计数不';似乎没有如预期的那样工作
我遇到了一个有趣的发现,它涉及到SWIG如何处理包含其他结构作为成员的C结构的引用计数 我注意到,在我将数据从结构子成员存储到其他python对象(列表/dict)中的情况下,在使用python SWIG对象之前,我的python SWIG对象被垃圾收集。经过相当多的挖掘,我发现SWIG-ed结构成员似乎没有自己的独立引用计数,即使解释器指出它们是“SWIG对象”。因此,当我将structure子元素中的数据添加到列表中时,python不知道我添加了对该数据的引用 我创建了一个简单的案例来演示。我使用了以下3种结构: SWIG-ed-C结构:Python 包含复杂类型的SWIG-ed C结构的引用计数不';似乎没有如预期的那样工作,python,struct,swig,Python,Struct,Swig,我遇到了一个有趣的发现,它涉及到SWIG如何处理包含其他结构作为成员的C结构的引用计数 我注意到,在我将数据从结构子成员存储到其他python对象(列表/dict)中的情况下,在使用python SWIG对象之前,我的python SWIG对象被垃圾收集。经过相当多的挖掘,我发现SWIG-ed结构成员似乎没有自己的独立引用计数,即使解释器指出它们是“SWIG对象”。因此,当我将structure子元素中的数据添加到列表中时,python不知道我添加了对该数据的引用 我创建了一个简单的案例来演示。
typedef struct
{
unsigned long source;
unsigned long destination;
} message_header;
typedef struct
{
unsigned long data[120];
} message_large_body;
typedef struct
{
message_header header;
message_large_body body;
} large_message;
然后,我创建了一个相当于python的类,将该行为与纯SWIG-ed解决方案进行比较
相当于Python类
class pyLargeMessage(object):
def __init__(self):
self.header = bar.message_header()
self.body = bar.message_large_body()
>>> y = pyLargeMessage()
>>> y
<__main__.pyLargeMessage object at 0x06C5E6B0>
>>> y.header
<Swig Object of type 'message_header *' at 0x06C5E700>
>>> sys.getrefcount(y.header)
3
>>> z = [y.header]
>>> sys.getrefcount(y.header)
3
>>> z += [y.header]
>>> sys.getrefcount(y.header)
4
>>>
>>> y = bar.large_message()
>>> y
<Swig Object of type 'large_message *' at 0x06C668E0>
>>> y.header
<Swig Object of type 'message_header *' at 0x06C66B60>
>>> sys.getrefcount(y.header)
1
>>> z = [y.header]
>>> sys.getrefcount(y.header)
1
>>> z += [y.header]
>>> sys.getrefcount(y.header)
1
>>>
然后,我在解释器中运行了以下测试
Python解释器结果
class pyLargeMessage(object):
def __init__(self):
self.header = bar.message_header()
self.body = bar.message_large_body()
>>> y = pyLargeMessage()
>>> y
<__main__.pyLargeMessage object at 0x06C5E6B0>
>>> y.header
<Swig Object of type 'message_header *' at 0x06C5E700>
>>> sys.getrefcount(y.header)
3
>>> z = [y.header]
>>> sys.getrefcount(y.header)
3
>>> z += [y.header]
>>> sys.getrefcount(y.header)
4
>>>
>>> y = bar.large_message()
>>> y
<Swig Object of type 'large_message *' at 0x06C668E0>
>>> y.header
<Swig Object of type 'message_header *' at 0x06C66B60>
>>> sys.getrefcount(y.header)
1
>>> z = [y.header]
>>> sys.getrefcount(y.header)
1
>>> z += [y.header]
>>> sys.getrefcount(y.header)
1
>>>
正如已经指出的那样,
头
和体
的getter返回的对象基本上是一个轻量级代理对象,它在结构
中为头
/体
保存一个指向内存的指针。它不拥有该内存(它仍然由消息
对象本身或C库“拥有”,具体取决于您创建它的方式),并且它不是副本
即使它是一个副本,您对sys.getrefcount
的调用始终会返回1,不管怎样-每次对getter的调用都会返回一个新副本
从Python的角度来看,如果要确保永远不会有悬空指针,有两种方法可以修复它:
头
/体
副本的代理,该代理拥有它指向的内存消息本身的引用,因此即使消息被释放,当代理对象引用部分消息时,其refcount也不能达到0
%module test
%{
#include "test.h"
%}
%typemap(out) message_header * header %{
// This expands to resultobj = SWIG_NewPointerObj(...) exactly as before:
$result = SWIG_NewPointerObj(SWIG_as_voidptr($1), $1_descriptor, 0);
// This sets a reference to the parent object inside the child
PyObject_SetAttrString($result, "_parent", obj0);
%}
%include "test.h"
这等于说:
z = y.header
z._parent = y
在Python中
有了这个,我们现在可以运行:
y = test.large_message()
print(sys.getrefcount(y))
print(y.header)
z = [y.header]
print(sys.getrefcount(y))
z += [y.header]
print(sys.getrefcount(y))
正如预期的那样,它显示了y
的引用计数随着创建的每个子对象代理的增加而增加。因此,它们引用的内存不能过早释放(至少不能通过SWIG释放)
您可以使用%apply
,使其更通用,并将其应用于多个类型/成员:
%module test
%{
#include "test.h"
%}
%typemap(out) SWIGTYPE * SUBOBJECT %{
$result = SWIG_NewPointerObj(SWIG_as_voidptr($1), $1_descriptor, 0);
PyObject_SetAttrString($result, "_parent", obj0);
assert(obj0);
// hello world
%}
%apply SWIGTYPE * SUBOBJECT { message_header * header };
%apply SWIGTYPE * SUBOBJECT { message_large_body * body };
%include "test.h"
Fwiw,
sys.getrefcount(y.header)
始终返回1,因为每次读取y.header
都会返回一个新对象。尝试只读取y.header
一次,并将其保存在某个变量中。我对SWIG了解不够,所以这可能是错误的,但我可以想象Python对象是位于C对象本身之外的包装器:当调用bar.large_message()
时,它都会为large_message
结构分配原始C内存,和一个包装器Python对象。当这个特定的包装器对象消失时,它会释放原始C内存。y.header
表达式返回指向同一原始C内存的新Python对象,但不保持任何活动状态。(我可能在某个地方错了,但这就是它在CFFI中的工作原理)两个伟大的见解@ArminRigo。你的第二个评论是将引用返回到同一块内存中,这也是我的猜测。我继续写了一个简单的helper函数,它以无符号int的形式返回底层对象的地址,对于y.header的所有实例都是一样的(尽管它们有不同的Python对象地址)。看起来,一个人应该能够将记忆的副本复制到一个新的对象中,而不必跳过太多的环(类型映射),但也许我错了。也许我应该回答我自己的问题,再问一个新的问题。@ArminRigo-这看起来是我的答案。也许你想写下来。不过,在SWIG中有一些方法可以解决这个问题。@Flexo您能详细说明一下如何在SWIG中解决这个问题吗?这是一个很好的解决方案。但我可以想象,如果包含大量复杂结构,使用此解决方案将变得很麻烦。这让我想知道为什么这不是SWIG中的默认行为。Swig在封装所有子元素时都知道它们,所以为什么不更好地跟踪它们并保护它们免受无意中的垃圾收集。@tnerb123 Swig尽可能地反映C的行为。在本例中,如果您选择将指针保持在比它指向的内存区域更长的位置,则行为是未定义的。