Безопасный способ выставить C-выделенный буфер памяти, используя numpy/ctypes?
Я пишу привязки Python для библиотеки C, которая использует буферы разделяемой памяти для хранения своего внутреннего состояния. Выделение и освобождение этих буферов выполняется за пределами Python самой библиотекой, но я могу косвенно контролировать, когда это происходит, вызывая упакованные функции конструктора / деструктора из Python. Я хотел бы представить некоторые из буферов в Python, чтобы я мог читать из них, а в некоторых случаях выдавать им значения. Производительность и использование памяти являются важными проблемами, поэтому я хотел бы избежать копирования данных, где это возможно.
Мой текущий подход заключается в создании массива numpy, который обеспечивает прямое представление указателя ctypes:
import numpy as np
import ctypes as C
libc = C.CDLL('libc.so.6')
class MyWrapper(object):
def __init__(self, n=10):
# buffer allocated by external library
addr = libc.malloc(C.sizeof(C.c_int) * n)
self._cbuf = (C.c_int * n).from_address(addr)
def __del__(self):
# buffer freed by external library
libc.free(C.addressof(self._cbuf))
self._cbuf = None
@property
def buffer(self):
return np.ctypeslib.as_array(self._cbuf)
Помимо того, что я избегаю копий, это также означает, что я могу использовать синтаксис индексации и присваивания numpy и передавать его непосредственно другим функциям numpy:
wrap = MyWrapper()
buf = wrap.buffer # buf is now a writeable view of a C-allocated buffer
buf[:] = np.arange(10) # this is pretty cool!
buf[::2] += 10
print(wrap.buffer)
# [10 1 12 3 14 5 16 7 18 9]
Однако это также опасно по своей природе:
del wrap # free the pointer
print(buf) # this is bad!
# [1852404336 1969367156 538978662 538976288 538976288 538976288
# 1752440867 1763734377 1633820787 8548]
# buf[0] = 99 # uncomment this line if you <3 segfaults
Чтобы сделать это более безопасным, мне нужно иметь возможность проверить, освобожден ли основной указатель C, прежде чем я попытаюсь прочитать / записать содержимое массива. У меня есть несколько мыслей о том, как это сделать:
- Одним из способов будет создание подкласса
np.ndarray
который содержит ссылку на_cbuf
атрибутMyWrapper
проверяет, является ли этоNone
перед выполнением какого-либо чтения / записи в основную память, и вызывает исключение, если это так. - Я мог бы легко сгенерировать несколько просмотров одного и того же буфера, например,
.view
приведение или нарезка, поэтому каждый из них должен был бы наследовать ссылку на_cbuf
и метод, который выполняет проверку. Я подозреваю, что это может быть достигнуто путем переопределения__array_finalize__
, но я не уверен, как именно. - Метод "проверки указателя" также необходимо вызывать перед любой операцией, которая будет считывать и / или записывать содержимое массива. Я не знаю достаточно о внутренностях numpy, чтобы иметь исчерпывающий список методов для переопределения.
Как я могу реализовать подкласс np.ndarray
что выполняет эту проверку? Кто-нибудь может предложить лучший подход?
Обновление: этот класс делает большую часть того, что я хочу:
class SafeBufferView(np.ndarray):
def __new__(cls, get_buffer, shape=None, dtype=None):
obj = np.ctypeslib.as_array(get_buffer(), shape).view(cls)
if dtype is not None:
obj.dtype = dtype
obj._get_buffer = get_buffer
return obj
def __array_finalize__(self, obj):
if obj is None: return
self._get_buffer = getattr(obj, "_get_buffer", None)
def __array_prepare__(self, out_arr, context=None):
if not self._get_buffer(): raise Exception("Dangling pointer!")
return out_arr
# this seems very heavy-handed - surely there must be a better way?
def __getattribute__(self, name):
if name not in ["__new__", "__array_finalize__", "__array_prepare__",
"__getattribute__", "_get_buffer"]:
if not self._get_buffer(): raise Exception("Dangling pointer!")
return super(np.ndarray, self).__getattribute__(name)
Например:
wrap = MyWrapper()
sb = SafeBufferView(lambda: wrap._cbuf)
sb[:] = np.arange(10)
print(repr(sb))
# SafeBufferView([0, 1, 2, 3, 4, 5, 6, 7, 8, 9], dtype=int32)
print(repr(sb[::2]))
# SafeBufferView([0, 2, 4, 6, 8], dtype=int32)
sbv = sb.view(np.double)
print(repr(sbv))
# SafeBufferView([ 2.12199579e-314, 6.36598737e-314, 1.06099790e-313,
# 1.48539705e-313, 1.90979621e-313])
# we have to call the destructor method of `wrap` explicitly - `del wrap` won't
# do anything because `sb` and `sbv` both hold references to `wrap`
wrap.__del__()
print(sb) # Exception: Dangling pointer!
print(sb + 1) # Exception: Dangling pointer!
print(sbv) # Exception: Dangling pointer!
print(np.sum(sb)) # Exception: Dangling pointer!
print(sb.dot(sb)) # Exception: Dangling pointer!
print(np.dot(sb, sb)) # oops...
# -70104698
print(np.extract(np.ones(10), sb))
# array([251019024, 32522, 498870232, 32522, 4, 5,
# 6, 7, 48, 0], dtype=int32)
# np.copyto(sb, np.ones(10, np.int32)) # don't try this at home, kids!
Я уверен, что есть другие крайние случаи, которые я пропустил.
Обновление 2: я поиграл с weakref.proxy
, как предложено @ivan_pozdeev. Это хорошая идея, но, к сожалению, я не вижу, как это будет работать с массивами numpy. Я мог бы попытаться создать слабую ссылку на массив numpy, возвращаемый .buffer
:
wrap = MyWrapper()
wr = weakref.proxy(wrap.buffer)
print(wr)
# ReferenceError: weakly-referenced object no longer exists
# <weakproxy at 0x7f6fe715efc8 to NoneType at 0x91a870>
Я думаю, что проблема здесь в том, что np.ndarray
экземпляр возвращен wrap.buffer
немедленно выходит за рамки. Обходной путь для класса заключается в том, чтобы создать экземпляр массива при инициализации, содержать сильную ссылку на него и получить .buffer()
получатель вернуть weakref.proxy
в массив:
class MyWrapper2(object):
def __init__(self, n=10):
# buffer allocated by external library
addr = libc.malloc(C.sizeof(C.c_int) * n)
self._cbuf = (C.c_int * n).from_address(addr)
self._buffer = np.ctypeslib.as_array(self._cbuf)
def __del__(self):
# buffer freed by external library
libc.free(C.addressof(self._cbuf))
self._cbuf = None
self._buffer = None
@property
def buffer(self):
return weakref.proxy(self._buffer)
Однако, это нарушается, если я создаю второе представление для того же массива, в то время как буфер все еще распределен:
wrap2 = MyWrapper2()
buf = wrap2.buffer
buf[:] = np.arange(10)
buf2 = buf[:] # create a second view onto the contents of buf
print(repr(buf))
# <weakproxy at 0x7fec3e709b50 to numpy.ndarray at 0x210ac80>
print(repr(buf2))
# array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9], dtype=int32)
wrap2.__del__()
print(buf2[:]) # this is bad
# [1291716568 32748 1291716568 32748 0 0 0
# 0 48 0]
print(buf[:]) # WTF?!
# [34525664 0 0 0 0 0 0 0
# 0 0]
Это серьезно сломано - после звонка wrap2.__del__()
я не только могу читать и писать buf2
который был взглядом на массив wrap2._cbuf
, но я могу даже читать и писать buf
что не должно быть возможно, учитывая, что wrap2.__del__()
наборы wrap2._buffer
в None
,
6 ответов
Вы должны сохранить ссылку на ваш Wrapper, пока существует какой-либо массив numpy. Самый простой способ добиться этого - сохранить эту ссылку в атрибуте ctype-buffer:
class MyWrapper(object):
def __init__(self, n=10):
# buffer allocated by external library
self.size = n
self.addr = libc.malloc(C.sizeof(C.c_int) * n)
def __del__(self):
# buffer freed by external library
libc.free(self.addr)
@property
def buffer(self):
buf = (C.c_int * self.size).from_address(self.addr)
buf._wrapper = self
return np.ctypeslib.as_array(buf)
Таким образом, ваша обертка автоматически освобождается, когда последняя ссылка, например, последний массив numpy, собирается мусором.
Это закрытая библиотека, написанная третьей стороной и распространяемая в виде двоичного файла. Я мог бы вызывать те же библиотечные функции из C, а не из Python, но это не сильно помогло бы, так как у меня все еще нет доступа к коду, который фактически распределяет и освобождает буферы. Я не могу, например, выделить буферы самостоятельно, а затем передать их в библиотеку как указатели.
Вы можете, однако, обернуть буфер в тип расширения Python. Таким образом, вы можете предоставить только тот интерфейс, который хотите быть доступным, и позволить типу расширения автоматически обрабатывать освобождение буфера. Таким образом, для Python API невозможно выполнить чтение / запись в свободную память.
mybuffer.c
#include <python3.3/Python.h>
// Hardcoded values
// N.B. Most of these are only needed for defining the view in the Python
// buffer protocol
static long external_buffer_size = 32; // Size of buffer in bytes
static long external_buffer_shape[] = { 32 }; // Number of items for each dimension
static long external_buffer_strides[] = { 1 }; // Size of item for each dimension
//----------------------------------------------------------------------------
// Code to simulate the third-party library
//----------------------------------------------------------------------------
// Allocate a new buffer
static void* external_buffer_allocate()
{
// Allocate the memory
void* ptr = malloc(external_buffer_size);
// Debug
printf("external_buffer_allocate() = 0x%lx\n", (long) ptr);
// Fill buffer with a recognizable pattern
int i;
for (i = 0; i < external_buffer_size; ++i)
{
*((char*) ptr + i) = i;
}
// Done
return ptr;
}
// Free an existing buffer
static void external_buffer_free(void* ptr)
{
// Debug
printf("external_buffer_free(0x%lx)\n", (long) ptr);
// Release the memory
free(ptr);
}
//----------------------------------------------------------------------------
// Define a new Python instance object for the external buffer
// See: https://docs.python.org/3/extending/newtypes.html
//----------------------------------------------------------------------------
typedef struct
{
// Python macro to include standard members, like reference count
PyObject_HEAD
// Base address of allocated memory
void* ptr;
} BufferObject;
//----------------------------------------------------------------------------
// Define the instance methods for the new object
//----------------------------------------------------------------------------
// Called when there are no more references to the object
static void BufferObject_dealloc(BufferObject* self)
{
external_buffer_free(self->ptr);
}
// Called when we want a new view of the buffer, using the buffer protocol
// See: https://docs.python.org/3/c-api/buffer.html
static int BufferObject_getbuffer(BufferObject *self, Py_buffer *view, int flags)
{
// Set the view info
view->obj = (PyObject*) self;
view->buf = self->ptr; // Base pointer
view->len = external_buffer_size; // Length
view->readonly = 0;
view->itemsize = 1;
view->format = "B"; // unsigned byte
view->ndim = 1;
view->shape = external_buffer_shape;
view->strides = external_buffer_strides;
view->suboffsets = NULL;
view->internal = NULL;
// We need to increase the reference count of our buffer object here, but
// Python will automatically decrease it when the view goes out of scope
Py_INCREF(self);
// Done
return 0;
}
//----------------------------------------------------------------------------
// Define the struct required to implement the buffer protocol
//----------------------------------------------------------------------------
static PyBufferProcs BufferObject_as_buffer =
{
// Create new view
(getbufferproc) BufferObject_getbuffer,
// Release an existing view
(releasebufferproc) 0,
};
//----------------------------------------------------------------------------
// Define a new Python type object for the external buffer
//----------------------------------------------------------------------------
static PyTypeObject BufferType =
{
PyVarObject_HEAD_INIT(NULL, 0)
"external buffer", /* tp_name */
sizeof(BufferObject), /* tp_basicsize */
0, /* tp_itemsize */
(destructor) BufferObject_dealloc, /* tp_dealloc */
0, /* tp_print */
0, /* tp_getattr */
0, /* tp_setattr */
0, /* tp_reserved */
0, /* tp_repr */
0, /* tp_as_number */
0, /* tp_as_sequence */
0, /* tp_as_mapping */
0, /* tp_hash */
0, /* tp_call */
0, /* tp_str */
0, /* tp_getattro */
0, /* tp_setattro */
&BufferObject_as_buffer, /* tp_as_buffer */
Py_TPFLAGS_DEFAULT, /* tp_flags */
"External buffer", /* tp_doc */
0, /* tp_traverse */
0, /* tp_clear */
0, /* tp_richcompare */
0, /* tp_weaklistoffset */
0, /* tp_iter */
0, /* tp_iternext */
0, /* tp_methods */
0, /* tp_members */
0, /* tp_getset */
0, /* tp_base */
0, /* tp_dict */
0, /* tp_descr_get */
0, /* tp_descr_set */
0, /* tp_dictoffset */
(initproc) 0, /* tp_init */
0, /* tp_alloc */
0, /* tp_new */
};
//----------------------------------------------------------------------------
// Define a Python function to put in the module which creates a new buffer
//----------------------------------------------------------------------------
static PyObject* mybuffer_create(PyObject *self, PyObject *args)
{
BufferObject* buf = (BufferObject*)(&BufferType)->tp_alloc(&BufferType, 0);
buf->ptr = external_buffer_allocate();
return (PyObject*) buf;
}
//----------------------------------------------------------------------------
// Define the set of all methods which will be exposed in the module
//----------------------------------------------------------------------------
static PyMethodDef mybufferMethods[] =
{
{"create", mybuffer_create, METH_VARARGS, "Create a buffer"},
{NULL, NULL, 0, NULL} /* Sentinel */
};
//----------------------------------------------------------------------------
// Define the module
//----------------------------------------------------------------------------
static PyModuleDef mybuffermodule = {
PyModuleDef_HEAD_INIT,
"mybuffer",
"Example module that creates an extension type.",
-1,
mybufferMethods
//NULL, NULL, NULL, NULL, NULL
};
//----------------------------------------------------------------------------
// Define the module's entry point
//----------------------------------------------------------------------------
PyMODINIT_FUNC PyInit_mybuffer(void)
{
PyObject* m;
if (PyType_Ready(&BufferType) < 0)
return NULL;
m = PyModule_Create(&mybuffermodule);
if (m == NULL)
return NULL;
return m;
}
test.py
#!/usr/bin/env python3
import numpy as np
import mybuffer
def test():
print('Create buffer')
b = mybuffer.create()
print('Print buffer')
print(b)
print('Create memoryview')
m = memoryview(b)
print('Print memoryview shape')
print(m.shape)
print('Print memoryview format')
print(m.format)
print('Create numpy array')
a = np.asarray(b)
print('Print numpy array')
print(repr(a))
print('Change every other byte in numpy')
a[::2] += 10
print('Print numpy array')
print(repr(a))
print('Change first byte in memory view')
m[0] = 42
print('Print numpy array')
print(repr(a))
print('Delete buffer')
del b
print('Delete memoryview')
del m
print('Delete numpy array - this is the last ref, so should free memory')
del a
print('Memory should be free before this line')
if __name__ == '__main__':
test()
пример
$ gcc -fPIC -shared -o mybuffer.so mybuffer.c -lpython3.3m
$ ./test.py
Create buffer
external_buffer_allocate() = 0x290fae0
Print buffer
<external buffer object at 0x7f7231a2cc60>
Create memoryview
Print memoryview shape
(32,)
Print memoryview format
B
Create numpy array
Print numpy array
array([ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16,
17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31], dtype=uint8)
Change every other byte in numpy
Print numpy array
array([10, 1, 12, 3, 14, 5, 16, 7, 18, 9, 20, 11, 22, 13, 24, 15, 26,
17, 28, 19, 30, 21, 32, 23, 34, 25, 36, 27, 38, 29, 40, 31], dtype=uint8)
Change first byte in memory view
Print numpy array
array([42, 1, 12, 3, 14, 5, 16, 7, 18, 9, 20, 11, 22, 13, 24, 15, 26,
17, 28, 19, 30, 21, 32, 23, 34, 25, 36, 27, 38, 29, 40, 31], dtype=uint8)
Delete buffer
Delete memoryview
Delete numpy array - this is the last ref, so should free memory
external_buffer_free(0x290fae0)
Memory should be free before this line
Мне понравился подход @Vikas, но когда я попробовал его, я получил только массив объектов Numpy из одного FreeOnDel
объект. Следующее намного проще и работает:
class FreeOnDel(object):
def __init__(self, data, shape, dtype, readonly=False):
self.__array_interface__ = {"version": 3,
"typestr": numpy.dtype(dtype).str,
"data": (data, readonly),
"shape": shape}
def __del__(self):
data = self.__array_interface__["data"][0] # integer ptr
print("do what you want with the data at {}".format(data))
view = numpy.array(FreeOnDel(ptr, shape, dtype), copy=False)
где ptr
указатель на данные в виде целого числа (например, ctypesptr.addressof(...)
).
это __array_interface__
атрибута достаточно, чтобы сообщить Numpy, как преобразовать область памяти в массив, а затем FreeOnDel
объект становится этим массивом base
, Когда массив удаляется, удаление распространяется на FreeOnDel
объект, куда можно позвонить libc.free
,
Я мог бы даже назвать это FreeOnDel
учебный класс " BufferOwner
", потому что это его роль: отслеживать собственность.
Вам просто нужна обертка с дополнительным __del__
функция, прежде чем передать его numpy.ctypeslib.as_array
метод.
class FreeOnDel(object):
def __init__(self, ctypes_ptr):
# This is not needed if you are dealing with ctypes.POINTER() objects
# Start of hack for ctypes ARRAY type;
if not hasattr(ctypes_ptr, 'contents'):
# For static ctypes arrays, the length and type are stored
# in the type() rather than object. numpy queries these
# properties to find out the shape and type, hence needs to be
# copied. I wish type() properties could be automated by
# __getattr__ too
type(self)._length_ = type(ctypes_ptr)._length_
type(self)._type_ = type(ctypes_ptr)._type_
# End of hack for ctypes ARRAY type;
# cannot call self._ctypes_ptr = ctypes_ptr because of recursion
super(FreeOnDel, self).__setattr__('_ctypes_ptr', ctypes_ptr)
# numpy.ctypeslib.as_array function sets the __array_interface__
# on type(ctypes_ptr) which is not called by __getattr__ wrapper
# Hence this additional wrapper.
@property
def __array_interface__(self):
return self._ctypes_ptr.__array_interface__
@__array_interface__.setter
def __array_interface__(self, value):
self._ctypes_ptr.__array_interface__ = value
# This is the onlly additional function we need rest all is overhead
def __del__(self):
addr = ctypes.addressof(self._ctypes_ptr)
print("freeing address %x" % addr)
libc.free(addr)
# Need to be called on all object members
# object.__del__(self) does not work
del self._ctypes_ptr
def __getattr__(self, attr):
return getattr(self._ctypes_ptr, attr)
def __setattr__(self, attr, val):
setattr(self._ctypes_ptr, attr, val)
Тестировать
In [32]: import ctypes as C
In [33]: n = 10
In [34]: libc = C.CDLL("libc.so.6")
In [35]: addr = libc.malloc(C.sizeof(C.c_int) * n)
In [36]: cbuf = (C.c_int * n).from_address(addr)
In [37]: wrap = FreeOnDel(cbuf)
In [38]: sb = np.ctypeslib.as_array(wrap, (10,))
In [39]: sb[:] = np.arange(10)
In [40]: print(repr(sb))
array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9], dtype=int32)
In [41]: print(repr(sb[::2]))
array([0, 2, 4, 6, 8], dtype=int32)
In [42]: sbv = sb.view(np.double)
In [43]: print(repr(sbv))
array([ 2.12199579e-314, 6.36598737e-314, 1.06099790e-313,
1.48539705e-313, 1.90979621e-313])
In [45]: buf2 = sb[:8]
In [46]: sb[::2] += 10
In [47]: del cbuf # Memory not freed because this does not have __del__
In [48]: del wrap # Memory not freed because sb, sbv, buf2 have references
In [49]: del sb # Memory not freed because sbv, buf have references
In [50]: del buf2 # Memory not freed because sbv has reference
In [51]: del sbv # Memory freed because no more references
freeing address 2bc6bc0
На самом деле более простым решением является перезапись __del__
функция
In [7]: olddel = getattr(cbuf, '__del__', lambda: 0)
In [8]: cbuf.__del__ = lambda self : libc.free(C.addressof(self)), olddel
In [10]: import numpy as np
In [12]: sb = np.ctypeslib.as_array(cbuf, (10,))
In [13]: sb[:] = np.arange(10)
In [14]: print(repr(sb))
array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9], dtype=int32)
In [15]: print(repr(sb))
array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9], dtype=int32)
In [16]: print(repr(sb[::2]))
array([0, 2, 4, 6, 8], dtype=int32)
In [17]: sbv = sb.view(np.double)
In [18]: print(repr(sbv))
array([ 2.12199579e-314, 6.36598737e-314, 1.06099790e-313,
1.48539705e-313, 1.90979621e-313])
In [19]: buf2 = sb[:8]
In [20]: sb[::2] += 10
In [22]: del cbuf # Memory not freed
In [23]: del sb # Memory not freed because sbv, buf have references
In [24]: del buf2 # Memory not freed because sbv has reference
In [25]: del sbv # Memory freed because no more references
weakref
это встроенный механизм для функциональности, которую вы предлагаете. В частности, weakref.proxy
является объектом с тем же интерфейсом, что и указанный. После удаления объекта, на который есть ссылка, любая операция с прокси weakref.ReferenceError
, Тебе даже не нужно numpy
:
In [2]: buffer=(c.c_int*100)() #acts as an example for an externally allocated buffer
In [3]: voidp=c.addressof(buffer)
In [10]: a=(c.c_int*100).from_address(voidp) # python object accessing the buffer.
# Here it's created from raw address value. It's better to use function
# prototypes instead for some type safety.
In [14]: ra=weakref.proxy(a)
In [15]: a[1]=1
In [16]: ra[1]
Out[16]: 1
In [17]: del a
In [18]: ra[1]
ReferenceError: weakly-referenced object no longer exists
In [20]: buffer[1]
Out[20]: 1
Как видите, в любом случае вам нужен обычный объект Python поверх буфера C. Если внешней библиотеке принадлежит память, объект должен быть удален до освобождения буфера на уровне C. Если вы владеете памятью сами, вы просто создаете ctypes
возражать обычным способом, тогда он будет освобожден при удалении.
Таким образом, если ваша внешняя библиотека владеет памятью и может освободиться в любое время (ваша спецификация неясна в этом отношении), она должна как-то сказать вам, что собирается это сделать - в противном случае у вас нет возможности узнать об этом, чтобы предпринять необходимые действия.
Если вы можете полностью контролировать время жизни буфера C из Python, то, по сути, у вас есть объект буфера Python, который ndarray
следует использовать.
Таким образом,
- Есть 2 основных способа их подключения:
- буфер -> ndarray
- ndarray -> буфер
- есть также вопрос, как реализовать сам буфер
буфер -> ndarray
Небезопасно: нет ничего автоматически содержащего ссылку на buffer
на всю жизнь ndarray
, Представление третьего объекта для хранения ссылок на оба не лучше: тогда вам просто нужно отслеживать третий объект вместо buffer
,
ndarray -> буфер
"Сейчас ты разговариваешь!" Так как сама задача "буфера, который ndarray
следует использовать "? Это естественный путь.
По факту, numpy
имеет встроенный механизм: любой ndarray
который не владеет своей памятью, содержит ссылку на объект, который делает в его base
атрибут (таким образом предотвращая сбор мусора). Для представлений атрибут автоматически назначается соответствующим образом (родительскому объекту, если его base
является None
или родителям base
).
Уловка в том, что вы не можете просто разместить там какой-нибудь старый объект. Вместо этого атрибут заполняется конструктором, и предложенный объект сначала подвергается проверке.
Так что, если бы мы только могли создать какой-то пользовательский объект, который numpy.array
принимает и считает пригодным для повторного использования памяти (numpy.ctypeslib.as_array
на самом деле является оберткой для numpy.array(copy=False)
с несколькими проверками вменяемости)...
<...>