Sicherer Weg, einen C-zugewiesenen Speicherpuffer mit numpy / ctypes verfügbar zu machen?
Ich schreibe Python-Bindungen für eine C-Bibliothek, die gemeinsam genutzte Speicherpuffer zum Speichern ihres internen Zustands verwendet. Die Zuweisung und Freigabe dieser Puffer erfolgt außerhalb von Python durch die Bibliothek selbst, aber ich kann indirekt steuern, wann dies geschieht, indem ich umschlossene Konstruktor- / Destruktorfunktionen aus Python heraus aufrufe. Ich möchte einige der Puffer für Python freigeben, damit ich sie lesen und in einigen Fällen Werte an sie senden kann. Die Leistung und die Speichernutzung sind wichtige Faktoren. Daher möchte ich das Kopieren von Daten nach Möglichkeit vermeiden.
ein aktueller Ansatz besteht darin, ein Numpy-Array zu erstellen, das eine direkte Ansicht auf einen ctypes-Zeiger biete
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)
as bedeutet, dass ich nicht nur Kopien vermeiden, sondern auch die Indizierungs- und Zuweisungssyntax von numpy verwenden und direkt an andere numpy-Funktionen übergeben kann:
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]
Es ist jedoch auch von Natur aus gefährlich:
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
Um dies sicherer zu machen, muss ich überprüfen können, ob der zugrunde liegende C-Zeiger freigegeben wurde, bevor ich versuche, in den Array-Inhalt zu lesen / schreiben. Ich habe ein paar Gedanken darüber, wie das geht:
in Weg wäre, eine Unterklasse von @ zu generierenp.ndarray
das enthält einen Verweis auf das_cbuf
Attribut vonMyWrapper
, prüft ob es @ iNone
vor dem Lesen / Schreiben in den zugrunde liegenden Speicher und löst in diesem Fall eine Ausnahme aus.Ich könnte leicht mehrere Ansichten auf den gleichen Puffer erzeugen, z. durch.view
Casting oder Slicing, daher müsste jeder von diesen den Verweis auf @ erb_cbuf
und die Methode, die die Prüfung durchführt. Ich vermute, dass dies durch Überschreiben von @ erreicht werden könnt__array_finalize__
, aber ich weiß nicht genau wie.Die "Pointer-Checking" -Methode müsste auch vor jeder Operation aufgerufen werden, die in den Inhalt des Arrays lesen und / oder schreiben würde. Ich weiß nicht genug über die Interna von numpy, um eine vollständige Liste von Methoden zum Überschreiben zu haben.Wie kann ich eine Unterklasse von @ implementierenp.ndarray
das führt diese Prüfung durch? Kann mir jemand einen besseren Ansatz vorschlagen?
Aktualisieren Diese Klasse macht das meiste was ich will:
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)
Beispielsweise
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!
Ich bin sicher, es gibt andere Randfälle, die ich verpasst habe.
Update 2: Ich habe ein wenig mit @ rumgespieweakref.proxy
, wie von @ vorgeschlag@ ivan_pozdeev. Es ist eine nette Idee, aber ich kann leider nicht sehen, wie es mit numpy Arrays funktionieren würde. Ich könnte versuchen, eine Schwachstelle für das von @ zurückgegebene Numpy-Array zu erstelle.buffer
:
wrap = MyWrapper()
wr = weakref.proxy(wrap.buffer)
print(wr)
# ReferenceError: weakly-referenced object no longer exists
# <weakproxy at 0x7f6fe715efc8 to NoneType at 0x91a870>
Ich denke, das Problem hier ist, dass dienp.ndarray
Instanz zurückgegeben vonwrap.buffer
verlässt sofort den Gültigkeitsbereich. Eine Problemumgehung wäre, dass die Klasse das Array bei der Initialisierung instanziiert, einen starken Verweis darauf hält und das @ ha.buffer()
getter return aweakref.proxy
zum Array:
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)
Dies bricht jedoch ab, wenn ich eine zweite Ansicht auf dasselbe Array erstelle, während der Puffer noch zugewiesen ist:
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]
Das istErnsthaf gebrochen - nach dem Aufruf vonwrap2.__del__()
kann ich nicht nur lesen und schreibenbuf2
das war eine numpy Array-Ansicht aufwrap2._cbuf
, aber ich kann sogar lesen und schreibenbuf
, was angesichts des @ nicht möglich sein sollwrap2.__del__()
setztwrap2._buffer
zuNone
.