J'écris des liens Python pour une bibliothèque C qui utilise des tampons de mémoire partagée pour stocker son état interne. L'allocation et la libération de ces tampons sont effectuées en dehors de Python par la bibliothèque elle-même, mais je peux indirectement contrôler le moment où cela se produit en appelant les fonctions constructrices/destructrices enveloppées depuis Python. J'aimerais exposer certains de ces tampons à Python afin de pouvoir les lire et, dans certains cas, leur transmettre des valeurs. Les performances et l'utilisation de la mémoire étant des préoccupations importantes, j'aimerais éviter de copier des données dans la mesure du possible.
Mon approche actuelle consiste à créer un tableau numpy qui fournit une vue directe sur un pointeur 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)
En plus d'éviter les copies, cela signifie également que je peux utiliser la syntaxe d'indexation et d'affectation de numpy et la passer directement à d'autres fonctions 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]
Cependant, elle est aussi intrinsèquement dangereuse :
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
Pour rendre cela plus sûr, je dois pouvoir vérifier si le pointeur C sous-jacent a été libéré avant d'essayer de lire/écrire le contenu du tableau. J'ai quelques idées sur la façon de le faire :
- Une façon de faire serait de générer une sous-classe de
np.ndarray
qui contient une référence à la_cbuf
l'attribut deMyWrapper
vérifie s'il estNone
avant d'effectuer toute lecture/écriture dans sa mémoire sous-jacente, et lève une exception si c'est le cas. - Je pourrais facilement générer plusieurs vues sur le même tampon, par exemple en
.view
casting ou slicing, donc chacun d'entre eux devra hériter de la référence à_cbuf
et la méthode qui effectue le contrôle. Je pense que l'on pourrait y parvenir en surchargeant la méthode__array_finalize__
mais je ne sais pas exactement comment. - La méthode de "pointer-checking" devrait également être appelée avant toute opération de lecture et/ou d'écriture du contenu du tableau. Je ne connais pas assez les internes de numpy pour avoir une liste exhaustive des méthodes à surcharger.
Comment pourrais-je implémenter une sous-classe de np.ndarray
qui effectue cette vérification ? Quelqu'un peut-il suggérer une meilleure approche ?
Mise à jour : Cette classe fait la plupart de ce que je veux :
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)
Par exemple :
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!
Je suis sûr qu'il y a d'autres cas limites que j'ai manqués.
Mise à jour 2 : Je me suis amusé avec weakref.proxy
comme le suggère @ivan_pozdeev . C'est une bonne idée, mais malheureusement je ne vois pas comment cela pourrait fonctionner avec les tableaux numpy. Je pourrais essayer de créer une référence faible vers le tableau numpy retourné par .buffer
:
wrap = MyWrapper()
wr = weakref.proxy(wrap.buffer)
print(wr)
# ReferenceError: weakly-referenced object no longer exists
# <weakproxy at 0x7f6fe715efc8 to NoneType at 0x91a870>
Je pense que le problème ici est que le np.ndarray
retournée par wrap.buffer
sort immédiatement du champ d'application. Une solution de contournement serait que la classe instancie le tableau lors de l'initialisation, qu'elle conserve une référence forte à ce tableau et que la fonction .buffer()
getter retourne un weakref.proxy
au tableau :
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)
Cependant, cela s'arrête si je crée une deuxième vue sur le même tableau alors que le tampon est toujours alloué :
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 es sérieusement cassé - après avoir appelé wrap2.__del__()
non seulement je peux lire et écrire sur buf2
qui était une vue d'un tableau numpy sur wrap2._cbuf
mais je peux même lire et écrire à buf
ce qui ne devrait pas être possible étant donné que wrap2.__del__()
fixe wrap2._buffer
a None
.