Python 包含复杂类型的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不知道我添加了对该数据的引用 我创建了一个简单的案例来演示。

我遇到了一个有趣的发现,它涉及到SWIG如何处理包含其他结构作为成员的C结构的引用计数

我注意到,在我将数据从结构子成员存储到其他python对象(列表/dict)中的情况下,在使用python SWIG对象之前,我的python SWIG对象被垃圾收集。经过相当多的挖掘,我发现SWIG-ed结构成员似乎没有自己的独立引用计数,即使解释器指出它们是“SWIG对象”。因此,当我将structure子元素中的数据添加到列表中时,python不知道我添加了对该数据的引用

我创建了一个简单的案例来演示。我使用了以下3种结构:

SWIG-ed-C结构:

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的角度来看,如果要确保永远不会有悬空指针,有两种方法可以修复它:

  • getter返回
    /
    副本的代理,该代理拥有它指向的内存
  • getter返回一个代理,该代理保存对
    消息本身的引用,因此即使
    消息被释放,当代理对象引用部分消息时,其refcount也不能达到0
  • 我总结了一个用SWIG做#2的例子。头文件保持不变,但接口变为:

    %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的行为。在本例中,如果您选择将指针保持在比它指向的内存区域更长的位置,则行为是未定义的。