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.

Antworten auf die Frage(12)

Ihre Antwort auf die Frage