Cython结构成员范围

时间:2019-01-06 03:19:19

标签: pointers scope cython

我在Cython中有一个使用char *成员的数据结构。

正在发生的事情是,成员值似乎在将值分配给成员的函数之外失去了作用域。参见以下示例(使用IPython):

[nav] In [26]: %%cython -f 
      ...: ctypedef struct A: 
      ...:     char *s 
      ...:      
      ...: cdef char *_s 
      ...:      
      ...: cdef void fn(A *a, msg): 
      ...:     s = msg.encode() 
      ...:     a[0].s = s 
      ...:  
      ...: cdef A _a 
      ...: _a.s = _s 
      ...: fn(&_a, 'hello') 
      ...: print(_a.s)          
      ...: print(b'hola') 
      ...: print(_a.s)          
b'hello'
b'hola'
b"b'hola'"

似乎_a.s被释放到fn之外,并被分配了适合该插槽的内存中的所有垃圾。

仅在某些情况下会发生这种情况。例如,如果我为{分配了b'hello'而不是fn()内的编码字符串,则正确的字符串将被打印到函数外部。

如您所见,我还为char变量添加了一个额外的声明,并在执行fn之前将其分配给结构,以确保_a.s指针不会超出范围。但是,我怀疑问题在于将成员分配给函数范围内的变量。

这里真正发生了什么,如何解决此问题?

谢谢。

1 个答案:

答案 0 :(得分:1)

您的问题是,指针a.s一旦创建,就会在fn函数中悬空。

调用msg.encode()时,将创建临时字节对象s并将其缓冲区的地址保存到a.s。但是,紧接着之后(即在函数的退出处),临时字节对象被破坏,指针悬空了。

由于字节对象很小,Python's memory manager在舞台上管理其内存-这可以确保访问地址时不会出现段错误(幸运的是)。

在临时对象被销毁的同时,内存不会被覆盖/清理,因此从A.s的角度来看,临时对象似乎仍然存在。

无论何时创建大小与临时对象相似的新字节对象,舞台上的旧内存都可能会被重用,因此指针a.s可以指向新分配的字节对象的缓冲区

顺便说一句,您是否会直接使用a[0].s = msg.encode()(我猜您是否使用过),Cython不会构建并告诉您,您试图说的是对临时Python对象的引用。添加一个明确的引用使Cython蒙骗,但对您的情况没有帮助。

那该怎么办呢?哪种解决方案合适,取决于整体情况,但是可用的策略是:

  1. 管理A.s的内存。即手动保留内存,从临时对象复制,并在完成后立即释放内存。
  2. 管理引用计数:在PyObject *结构中添加A。为其分配临时对象(不要忘记手动增加参考计数器),完成后立即减小参考计数器。
  3. 将临时对象的引用收集到一个池中(例如一个列表),这将使它们保持活动状态。不需要对象后立即清除池。

选项3不一定总是最好的,但最简单的是-您不必管理内存也不必管理引用计数:

%%cython
...
pool=[]   
cdef void fn(A *a, msg):    
    s = msg.encode()
    pool.append(s) 
    a[0].s = s

虽然这不能解决主要问题,但在这种情况下,使用PyUnicode_AsUTF8(启发式by this answer)可能是令人满意的解决方案:

%%cython

# it is not wrapped by `cpython.pxd`:
cdef extern from "Python.h":
    const char* PyUnicode_AsUTF8(object unicode)
...

cdef void fn(A *a, msg): 
 a[0].s = PyUnicode_AsUTF8(msg) # msg.encode() uses utf-8 as default.

这至少具有两个优点:

  • 只要a[0].s有效,指针msg就是有效的
  • 调用PyUnicode_AsUTF8(msg)msg.encode()快,因为它会重用缓存的缓冲区,因此基本上是在第一次调用后O(1),而msg.encode()至少需要复制内存,并且O(n),字符数为n