如何在Cython中将C指针和长度封装在新式缓冲区对象中?

8
我正在使用Cython编写Python 2.7扩展模块。如何创建实现新式缓冲区接口的Python对象,以包装由C库提供给我的一块内存?该内存块只是一串字节,而不是结构或多维数组。我被给予一个const void *指针和一个长度,以及有关指针有效期的一些详细信息。
我无法复制内存-这会损害我的应用程序性能。
对于旧式缓冲区对象,我可以简单地使用PyBuffer_FromMemory(),但我似乎找不到类似地产生新式缓冲区对象的简单方法。
我必须创建自己实现缓冲区接口的类吗?还是Cython提供了一种简单的方法来做到这一点?
我已经阅读了Cython文档中的Unicode and Passing StringsTyped Memoryviews页面,但文档不够准确、不够完整,也没有看起来与我想要做的相似的示例。
以下是我尝试过的内容(test.pyx):
from libc.stdlib cimport malloc
from libc.string cimport memcpy

## pretend that this function is in some C library and that it does
## something interesting.  (this function is unrelated to the problem
## I'm experiencing -- this is just an example function that returns a
## chunk of memory that I want to wrap in an object that follows the
## new buffer protocol.)
cdef void dummy_function(const void **p, size_t *l):
    cdef void *tmp = malloc(17)
    memcpy(tmp, "some test\0 bytes", 17)
    p[0] = tmp
    l[0] = 17

cpdef getbuf():
    cdef const void *cstr
    cdef size_t l
    dummy_function(&cstr, &l)

    ## error: test.pyx:21:20: Invalid base type for memoryview slice: void
    #cdef const void[:] ret = cstr[:l]

    ## error: test.pyx:24:9: Assignment to const 'ret'
    #cdef const char[:] ret = cstr[:l]

    ## error: test.pyx:27:27: Cannot convert 'void const *' to memoryviewslice
    #cdef char[:] ret = cstr[:l]

    ## this next attempt cythonizes, but raises an exception:
    ## $ python -c 'import test; test.getbuf()'
    ## Traceback (most recent call last):
    ##   File "<string>", line 1, in <module>
    ##   File "test.pyx", line 15, in test.getbuf (test.c:1411)
    ##   File "test.pyx", line 38, in test.getbuf (test.c:1350)
    ##   File "stringsource", line 614, in View.MemoryView.memoryview_cwrapper (test.c:6763)
    ##   File "stringsource", line 321, in View.MemoryView.memoryview.__cinit__ (test.c:3309)
    ## BufferError: Object is not writable.
    cdef char[:] ret = (<const char *>cstr)[:l]

    ## this raises the same exception as above
    #cdef char[:] ret = (<char *>cstr)[:l]

    return ret

也许它失败是因为你将类型转换为 const char * 而不是 char * - Kevin
@Kevin:我更新了我的问题,说明即使我将其转换为char *而不是const char *,仍会发生相同的异常。感谢您指出这一点。 - Richard Hansen
1
经过更详细的研究,我想指出memcpy是非法的。您将tmp声明为const,然后又修改了它。这是C标准中未定义的行为。既然您还说要避免复制内存,我对此有点困惑。 - Kevin
@Kevin:谢谢你的调查。去掉 const 强制转换与我遇到的问题无关,但我已经更新了问题以消除 const 强制转换。关于复制,那只是一些虚假代码,用来帮助设置问题代码。请查看修正后的问题;希望现在更清楚了。 - Richard Hansen
3个回答

5
你可以定义一个扩展类型,通过定义__getbuffer____releasebuffer__特殊方法来实现缓冲区协议。例如:
from cpython.buffer cimport PyBuffer_FillInfo
from libc.stdlib cimport free, malloc
from libc.string cimport memcpy

cdef void dummy_function(const void **p, size_t *l):
    cdef void *tmp = malloc(17)
    memcpy(tmp, "some test\0 bytes", 17)
    p[0] = tmp
    l[0] = 17

cdef void free_dummy_data(const void *p, size_t l, void *arg):
    free(<void *>p)

cpdef getbuf():
    cdef const void *p
    cdef size_t l
    dummy_function(&p, &l)
    return MemBuf_init(p, l, &free_dummy_data, NULL)

ctypedef void dealloc_callback(const void *p, size_t l, void *arg)

cdef class MemBuf:
    cdef const void *p
    cdef size_t l
    cdef dealloc_callback *dealloc_cb_p
    cdef void *dealloc_cb_arg

    def __getbuffer__(self, Py_buffer *view, int flags):
        PyBuffer_FillInfo(view, self, <void *>self.p, self.l, 1, flags)
    def __releasebuffer__(self, Py_buffer *view):
        pass

    def __dealloc__(self):
        if self.dealloc_cb_p != NULL:
            self.dealloc_cb_p(self.p, self.l, self.dealloc_cb_arg)

# Call this instead of constructing a MemBuf directly.  The __cinit__
# and __init__ methods can only take Python objects, so the real
# constructor is here.  See:
# https://mail.python.org/pipermail/cython-devel/2012-June/002734.html
cdef MemBuf MemBuf_init(const void *p, size_t l,
                        dealloc_callback *dealloc_cb_p,
                        void *dealloc_cb_arg):
    cdef MemBuf ret = MemBuf()
    ret.p = p
    ret.l = l
    ret.dealloc_cb_p = dealloc_cb_p
    ret.dealloc_cb_arg = dealloc_cb_arg
    return ret

使用上述(名为test.pyx)代码,您将获得以下行为:

$ python -c 'import test; print repr(memoryview(test.getbuf()).tobytes())'
'some test\x00 bytes\x00'

我不知道是否有更简单的方法。


MemBuf 正在创建内存泄漏。__releasebuffer__ 应该调用 PyBuffer_Release(view)。如果 MemBuf 拥有由 C 函数返回的内存,则应编写一个 __dealloc__ 函数,以调用 free - Dunes
@Dunes:是的,你说得对。我更新了我的答案,在__dealloc__中释放内存。在我的实际代码中,C函数保留了内存块的所有权,所以我没有想到在这个示例代码中释放内存。 - Richard Hansen

4
Python 3.3拥有PyMemoryView_FromMemory C-API函数,该函数从提供的C缓冲区创建一个memoryview Python对象。 memoryview对象确实实现了新型缓冲区接口。
如果您查看其源代码,您会发现它们非常简单。它做的事情与PyMemoryView_FromBuffer相同,只是前者会使用PyBuffer_FillInfo填充Py_buffer本身。
由于后者存在于Python 2.7中,那么为什么我们不能自己调用PyBuffer_FillInfo呢?
from libc.stdlib cimport malloc
from libc.string cimport memcpy

cdef extern from "Python.h":
    ctypedef struct PyObject
    object PyMemoryView_FromBuffer(Py_buffer *view)
    int PyBuffer_FillInfo(Py_buffer *view, PyObject *obj, void *buf, Py_ssize_t len, int readonly, int infoflags)
    enum:
        PyBUF_FULL_RO

cdef void dummy_function(const void **p, size_t *l):
    cdef void *tmp = malloc(17)
    memcpy(tmp, "some test\0 bytes", 17)
    p[0] = tmp
    l[0] = 17

cpdef getbuf():
    cdef const void *cstr
    cdef size_t l
    cdef Py_buffer buf_info
    cdef char[:] ret
    cdef int readonly

    dummy_function(&cstr, &l)

    readonly = 1
    PyBuffer_FillInfo(&buf_info, NULL, <void*>cstr, l, readonly, PyBUF_FULL_RO)
    ret = PyMemoryView_FromBuffer(&buf_info)

    return ret

请注意,返回的值将具有类似于这样的 repr: <MemoryView of 'memoryview' at 0x7f216fc70ad0>。这是因为Cython似乎在_memoryviewslice中包装了裸memoryview。由于memoryview对象已经实现了缓冲区接口,因此你可能只需返回PyMemoryView_FromBuffer调用的结果。
此外,您负责管理缓冲区的生存期。以这种方式创建的memoryview对象不会自动释放内存。您必须自行完成它,确保没有memorybuffer引用它时再进行。在这方面,Richard Hansen的回答是更好的选择。

3
正如@RichardHansen在他的自我回答中正确地观察到的那样,你需要一个实现缓冲区协议并具有适当的析构函数来管理内存的类。
实际上,Cython在其中提供了一个相当轻量级的类cython.view.array,因此无需创建自己的类。 实际上,它在您链接的页面文档中已经记录下来,但是为了提供一个适合您情况的快速示例:
# at the top of your file
from cython.view cimport array

# ...

# after the call to dummy_function
my_array = array(shape=(l,), itemsize=sizeof(char), format='b',  # or capital B depending on if it's signed
                 allocate_buffer=False)
my_array.data = cstr
my_array.callback_free_data = free

cdef char[:] ret = my_array

提醒注意两个关键点:allocate_buffer 设置为 False,因为你在 cstr 中自行分配了缓冲区。设置 callback_free_data 确保使用标准库的 free 函数。


哇,我真的忽略了那个吗?还是Cython自从我最初提问以来就改变了?此外,如果不需要调用free,这可以简化为cdef const char[:] ret = <const char[:l]>cstr(这肯定以前是行不通的)。 - Richard Hansen
1
我相当肯定你忽略了这一点,但总是很难找到这些东西。我不确定简化转换 - 可能是新的。 - DavidW
当我说“我不确定简化强制转换”时,我的意思是我不确定它是何时引入的。如果你不需要 free,它绝对是有效的。 - DavidW

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接