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