python c numpy ctypes dangling-pointer

python - ¿Una forma más segura de exponer un búfer de memoria asignado por C usando numpy/ctypes?



dangling-pointer (6)

Es una biblioteca propietaria escrita por un tercero y distribuida como un binario. Podría llamar a las mismas funciones de la biblioteca desde C en lugar de Python, pero eso no ayudaría mucho ya que todavía no tengo acceso al código que realmente asigna y libera los buffers. No puedo, por ejemplo, asignar yo mismo los buffers y luego pasarlos a la biblioteca como indicadores.

Sin embargo, podría envolver el búfer en un tipo de extensión de Python. De esa manera, puede exponer solo la interfaz que desea que esté disponible y dejar que el tipo de extensión maneje automáticamente la liberación del búfer. De esa forma no es posible que la API de Python realice una lectura / escritura de memoria libre.

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()

Ejemplo

$ 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

Estoy escribiendo enlaces de Python para una biblioteca de C que utiliza buffers de memoria compartida para almacenar su estado interno. La asignación y liberación de estos buffers se realiza fuera de Python por la propia biblioteca, pero puedo controlar indirectamente cuando esto sucede llamando a funciones de constructor / destructor envueltas desde Python. Me gustaría exponer algunos de los buffers a Python para poder leerlos y, en algunos casos, enviarles valores. El rendimiento y el uso de la memoria son preocupaciones importantes, por lo que me gustaría evitar copiar datos siempre que sea posible.

Mi enfoque actual es crear una matriz numpy que proporcione una vista directa sobre un puntero de 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)

Además de evitar copias, esto también significa que puedo usar la sintaxis de asignación e indexación de numpy y pasarla directamente a otras funciones 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]

Sin embargo, también es inherentemente peligroso:

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

Para hacer esto más seguro, necesito poder verificar si el puntero C subyacente se ha liberado antes de intentar leer / escribir en el contenido de la matriz. Tengo algunos pensamientos sobre cómo hacer esto:

  • Una forma sería generar una subclase de np.ndarray que contenga una referencia al atributo _cbuf de MyWrapper , verifique si es None antes de realizar cualquier lectura / escritura en su memoria subyacente, y genera una excepción si este es el caso.
  • Podría generar fácilmente múltiples vistas en el mismo búfer, por ejemplo, mediante la conversión o el corte de .view , por lo que cada uno de estos tendría que heredar la referencia a _cbuf y el método que realiza la comprobación. Sospecho que esto podría lograrse anulando __array_finalize__ , pero no estoy seguro de cómo.
  • El método de "comprobación de puntero" también debería llamarse antes de cualquier operación que lea y / o escriba en el contenido de la matriz. No sé lo suficiente sobre los aspectos internos de numpy para tener una lista exhaustiva de métodos para anular.

¿Cómo podría implementar una subclase de np.ndarray que realice esta comprobación? ¿Alguien puede sugerir un mejor enfoque?

Actualización: esta clase hace la mayor parte de lo que quiero:

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)

Por ejemplo:

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!

Estoy seguro de que hay otros casos de borde que he perdido.

Actualización 2: He tenido una weakref.proxy jugar con weakref.proxy , como sugiere @ivan_pozdeev . Es una buena idea, pero desafortunadamente no puedo ver cómo funcionaría con matrices numpy. Podría intentar crear una debilidad en la matriz numpy devuelta por .buffer :

wrap = MyWrapper() wr = weakref.proxy(wrap.buffer) print(wr) # ReferenceError: weakly-referenced object no longer exists # <weakproxy at 0x7f6fe715efc8 to NoneType at 0x91a870>

Creo que el problema aquí es que la instancia np.ndarray devuelta por wrap.buffer inmediatamente queda fuera del alcance. Una solución alternativa sería que la clase ejemplificara la matriz en la inicialización, mantuviera una fuerte referencia a ella y que el .buffer() devuelva un weakref.proxy a la matriz:

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)

Sin embargo, esto se interrumpe si creo una segunda vista en la misma matriz mientras el búfer aún está asignado:

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]

Esto se ha roto seriamente : después de llamar wrap2.__del__() no solo puedo leer y escribir en buf2 que era una vista de matriz wrap2._cbuf en wrap2._cbuf , sino que incluso puedo leer y escribir en buf , lo cual no debería ser posible dado que wrap2.__del__() establece wrap2._buffer en None .


Debe mantener una referencia a su Wrapper mientras exista cualquier matriz numpy. La forma más sencilla de lograr esto, es guardar esta referencia en un atributo del 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)

De esta manera, su envoltorio se libera automáticamente, cuando la última referencia, por ejemplo, la última matriz numpy, se recolecta como basura.


Me gustó el enfoque de @Vikas, pero cuando lo probé, solo conseguí una matriz de objetos Numpy de un solo objeto FreeOnDel . Lo siguiente es mucho más simple y funciona:

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)

donde ptr es un puntero a los datos como un entero (por ejemplo, ctypesptr.addressof(...) ).

Este atributo __array_interface__ es suficiente para decirle a Numpy cómo crear una región de memoria como una matriz, y luego el objeto FreeOnDel convierte en la base esa matriz. Cuando se elimina la matriz, la eliminación se propaga al objeto FreeOnDel , donde puede llamar a libc.free .

Incluso podría llamar a esta clase de FreeOnDel " BufferOwner ", porque ese es su rol: hacer un seguimiento de la propiedad.


Si puede controlar completamente la vida útil del búfer de C desde Python, lo que esencialmente tiene es un objeto "búfer" de Python que debe utilizar un ndarray .

Así,

  • Hay 2 formas fundamentales de conectarlos:
    • búfer -> ndarray
    • ndarray -> buffer
  • También hay una pregunta sobre cómo implementar el búfer en sí.

búfer -> ndarray

No es seguro: no hay nada que ndarray automáticamente una referencia al buffer durante el tiempo de vida de ndarray . La introducción de un tercer objeto para contener referencias a ambos no es mejor: entonces solo hay que hacer un seguimiento del tercer objeto en lugar del buffer .

ndarray -> buffer

"¡Ahora estas hablando!" ¿Ya que la tarea en cuestión es "buffer que debe usar un ndarray "? Esta es la forma natural de ir.

De hecho, numpy tiene un mecanismo incorporado: cualquier ndarray que no sea propietaria de su memoria contiene una referencia al objeto que tiene en su atributo base (lo que evita que este último se recoja). Para las vistas, el atributo se asigna automáticamente en consecuencia (al objeto principal si su base es None o a la base ).

El problema es que no puedes colocar ningún objeto viejo allí. En su lugar, el atributo es llenado por un constructor y el objeto sugerido se pone primero a través de su escrutinio.

Entonces, si solo pudiéramos construir algún objeto personalizado que numpy.array acepte y considere elegible para la reutilización de la memoria ( numpy.ctypeslib.as_array es en realidad una envoltura para numpy.array(copy=False) con algunas comprobaciones de numpy.array(copy=False) ) ...

<...>


Solo necesita un contenedor con la función __del__ adicional antes de pasarlo al método 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)

Probar

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

De hecho, una solución más fácil es sobrescribir la función __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 es un mecanismo incorporado para la funcionalidad que está proponiendo. Específicamente, weakref.proxy es un objeto con la misma interfaz que el referido. Después de la eliminación del objeto al que se hace referencia, cualquier operación en el proxy genera un valor weakref.ReferenceError . Ni siquiera necesitas 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

Como puede ver, en cualquier caso, necesita un objeto de Python normal sobre el búfer de C. Si una biblioteca externa posee la memoria, el objeto debe eliminarse antes de que el búfer se libere en el nivel C. Si usted es dueño de la memoria, simplemente crea un objeto ctypes la manera normal, luego se liberará cuando se elimine.

Por lo tanto, si su biblioteca externa posee la memoria y puede liberarse en cualquier momento (su especificación es vaga al respecto), debe indicarle de alguna manera que está a punto de hacerlo; de lo contrario, no tiene forma de saberlo para tomar las medidas necesarias.