【问题标题】:How to wrap a C pointer and length in a new-style buffer object in Cython?如何在 Cython 的新型缓冲区对象中包装 C 指针和长度?
【发布时间】:2015-01-26 23:02:21
【问题描述】:

我正在 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:我更新了我的问题,指出即使我转换为char * 而不是const char *,也会发生同样的异常。感谢您指出这一点。
  • 在更详细地研究了这个问题之后,我想指出 memcpy 是非法的。您将 tmp 声明为 const,然后对其进行了修改。这是 C 标准未定义的行为。既然你还说你试图避免复制内存,我在这一点上有点困惑。
  • @Kevin:感谢您的调查。抛弃const 与我遇到的问题无关,但我还是更新了问题以消除const 演员。关于复制,这只是帮助设置问题代码的虚拟代码。请参阅修改后的问题;希望现在更清楚了。

标签: python python-2.7 cython python-c-extension memoryview


【解决方案1】:

您可以通过定义__getbuffer____releasebuffer__ special methods 来定义实现缓冲区协议的extension type。例如:

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 编写一个 __dealloc__ 函数,如果它拥有 C 函数返回的内存,则调用 free
  • @Dunes:是的,你是对的。我更新了答案以释放 __dealloc__ 中的内存。在我的实际代码中,C 函数保留了内存块的所有权,因此我没有考虑在此示例代码中释放内存。
【解决方案2】:

Python 3.3 有 PyMemoryView_FromMemory C-API 函数,它从提供的 C 缓冲区创建一个 memoryview Python 对象。 memoryview 对象确实实现了新型缓冲区接口。

如果您查看its sources,您会发现它们相当简单。 它和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 如下所示:&lt;MemoryView of 'memoryview' at 0x7f216fc70ad0&gt;。这是因为 Cython 似乎将 memoryview 包裹在 _memoryviewslice 内。由于memoryview 对象已经实现了缓冲区接口,所以您可能应该简单地返回PyMemoryView_FromBuffer 调用的结果。

此外,您还负责管理缓冲区的生命周期。 memoryview 以这种方式创建的对象不会自动释放内存。您必须自己做,确保只有在没有 memorybuffer 引用它时才这样做。在这方面,Richard Hansen 的回答是更好的选择。

【讨论】:

    【解决方案3】:

    正如@RichardHansen 在他的自我回答中正确观察到的那样,您需要的是一个实现缓冲区协议的类,并且有一个合适的析构函数来管理内存。

    Cython 实际上以cython.view.array 的形式提供了一个相当轻量级的内置类,因此您无需创建自己的类。它实际上是documented in the page you linked,但为了提供适合您情况的快速示例:

    # 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 = &lt;const char[:l]&gt;cstr(以前肯定没用过)。
    • 我比较确定你忽略了这一点,但要找到这些东西总是出奇的难。我不确定简化的演员阵容 - 这很可能是新的。
    • 当我说“我不确定简化演员表”时,我的意思是我不确定它是什么时候推出的。如果您不需要free,它绝对有效
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2010-11-09
    • 2012-02-07
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多