Как обернуть указатель C и длину в буферный объект нового стиля в Cython?

Я пишу модуль расширения Python 2.7 на Cython. Как мне создать объект Python, реализующий интерфейс буфера нового стиля, который оборачивает кусок памяти, данный мне библиотекой C? Кусок памяти - это просто строка байтов, а не структура или многомерный массив. Мне дали const void * указатель и длина, а также некоторые подробности о том, как долго указатель остается в силе.

Я не могу скопировать память - это снизит производительность моего приложения.

С объектами буфера старого стиля я мог бы просто использовать PyBuffer_FromMemory(), но я не могу найти такой же простой способ для создания буферного объекта нового стиля.

Должен ли я создать свой собственный класс, который реализует интерфейс буфера? Или Cython предоставляет простой способ сделать это?

Я прочитал страницы Unicode и Passing Strings и Typed Memoryviews из документации Cython, но документация неточная и не очень полная, и нет примеров, которые выглядели бы так, как я хочу.

Вот что я пробовал (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

2 ответа

Решение

Вы можете определить тип расширения, который реализует буферный протокол, определив __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'

Я не знаю, есть ли более простой способ.

Python 3.3 имеет PyMemoryView_FromMemory Функция C-API, которая создает memoryview Python-объект из предоставленного C-буфера. memoryview объекты действительно реализуют интерфейс буфера нового стиля.

Если вы посмотрите на его источники, вы заметите, что они довольно просты. Это делает то же самое, что и PyMemoryView_FromBuffer кроме прежних заливок Py_buffer с PyBuffer_FillInfo сам.

Так как последний существует в 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, кажется, обнажается memoryview внутри _memoryviewslice, поскольку memoryview объекты уже реализуют интерфейс буфера, вы, вероятно, должны просто вернуть результат PyMemoryView_FromBuffer позвони вместо.

Кроме того, вы несете ответственность за управление временем жизни вашего буфера. memoryview объекты, созданные таким образом, не освобождают память автоматически. Вы должны сделать это самостоятельно, гарантируя, что вы делаете это только один раз, когда нет memorybuffer ссылается на это. В связи с этим ответ Ричарда Хансена является гораздо лучшей альтернативой.

Как правильно заметил в своем ответе @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 функция используется.

Другие вопросы по тегам