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
deMyWrapper
, verifique si esNone
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.