我在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
指针不会超出范围。但是,我怀疑问题在于将成员分配给函数范围内的变量。
这里真正发生了什么,如何解决此问题?
谢谢。
答案 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蒙骗,但对您的情况没有帮助。
那该怎么办呢?哪种解决方案合适,取决于整体情况,但是可用的策略是:
A.s
的内存。即手动保留内存,从临时对象复制,并在完成后立即释放内存。PyObject *
结构中添加A
。为其分配临时对象(不要忘记手动增加参考计数器),完成后立即减小参考计数器。选项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
。