CTypes ungültige Speicherallokation

Python in C/C++ embedden, C-Module, ctypes, Cython, SWIG, SIP etc sind hier richtig.
Antworten
Plexian
User
Beiträge: 10
Registriert: Mittwoch 15. Juli 2020, 20:24

Hallo nochmal,

ich habe ein weiteres Problem mit CTypes und bin mir nicht mal sicher, ob ich beschreiben kann, was genau das Problem ist.
Meine Ausgangssituation ist folgende:
Ich habe eine .so in C zur Verfügung, wessen structs und Methoden ich in Python (in schöner Weise) zur Verfügung stellen will, und zwar mit CTypes. Wie in dem Thread schon mir erklärt wurde, sieht meine Struktur so aus, dass ich in Python zwei Klassen pro struct in C habe. Bspw. gibt es eine struct "vector" in C, dann habe ich eine Klasse "class LibVector(ctypes.Structure)" in Python, in welcher die Felder mittels _fields_ gesetzt werden. Genauso werden die C-Methoden dann in Python wie folgt erstellt, wobei get_func eine eigene Helperfunktion ist, welche nur die Funktion aus der Lib "holt" und dann die ".restype" und ".argtypes" Attribute setzt.

Code: Alles auswählen

# libvector.py
new_avector = get_func('new_avector', PTR(CStructAVector), [c_uint])
fill_avector = get_func('fill_avector', None, [PTR(CStructAVector), c_double])
Nun habe ich eine weitere Klasse "class Vector()", welche diese Methoden nun zur Verfügung stellen soll und ein LibVector-Objekt wrappt. Grob sieht das Ganze nun so aus:

Code: Alles auswählen

 #vector.py
class Vector():
    def __init__(self, cobj):
        assert isinstance(cobj, POINTER(libvector.LibVector))
        self._as_parameter_ = cobj

    @classmethod
    def new(cls, dim: int):
        return cls(libvector.new_vector(dim))

    def fill(self, value):
        libvector.fill_vector(self, c_double(value))
Nun habe ich ein Beispielprogramm, welches prinzipiell eine Matrix A und einen Vektor b erzeugt, und dann ein Gleichungssystem lösen soll, sprich Ax = b nach x auflösen soll.
Wenn ich nun nur die ganzen LibXyz-Klassen nutze, funktioniert alles ohne Probleme, ist aber etwas unschön (der Nutzer muss sich etwas mit CTypes auseinandersetzen, das will ich ja eben mit der zweiten Klasse vermeiden). Wenn ich nun aber meine "besseren" Klasse nutze, läuft an folgender Stelle etwas schief: Um Ax = b lösen zu können, muss x mittels Vector.new(dim) erzeugt und dann in der C-Methode befüllt werden. Allerdings sollte durch das .new() ein leerer Vektor erzeugt werden (sprich nur 0en), allerdings scheinen einige Elemente sogar nichtmal gültige doubles zu sein, sodass C daraus "nan"s macht. Dies hat zur Folge, dass die ganze Rechnung nicht mehr funktioniert.

Das Alles wäre gar nicht so ein großes Problem, wenn dieser Fehler nicht so sporadisch auftreten würde. Bspw. bei niedrigeren Dimensionen habe ich den Fehler nicht. Die Dimension bekomme ich von einem Feld einer anderen Klasse, wenn ich dieses Feld vorher in einer Variable speichere, bekomme ich auch keinen Fehler mehr.
Die Dimension bekomme ich über "d._as_parameter_.contents.tri". Dafür habe ich __get_attr__ überschrieben, sodass "d.tri" das gleiche tut. Mit ersterem funktioniert mein Programm, mit letzterem wieder nicht (??).

Mir ist bewusst, dass ich noch nicht viel Code bereitgestellt habe. Ich wollte nur zunächst sichergehen, dass ich nichts fundamentales falsch verstanden habe, was Projektion von C structs in Python anbelangt. Es scheint mir ein Speicherallokationsproblem zu sein, aber ich kann weder sagen ob in C oder in Python (wobei es wohl Python ist), und sobald ich sehe, dass es mit Variablen und anderen Versuchen wieder funktioniert und dann wieder nicht, war mein Verständnis für Python auch langsam am Ende.

Ich bin jedem dankbar, der sich die Mühe macht, das nachzuvollziehen, und freue mich über jeden Tipp in die richtige Richtung, wo ich noch nachschauen kann. Mir scheint das momentan recht willkürlich, obwohl es ja deterministisch ist (wenns mit eine Code geht / nicht geht, dann ist das immer so, egal wann ich das wie oft ausführe). Falls etwas unklar sein sollte, versuche ich das natürlich gern zu erklären.

Gruß Plexian
Plexian
User
Beiträge: 10
Registriert: Mittwoch 15. Juli 2020, 20:24

Ich habe scheinbar altenCode kopiert, die Methoden werden wie folgt definiert:

Code: Alles auswählen

# libvector.py
new_vector = get_func('new_vector', PTR(LibVector), [c_uint])
fill_vector = get_func('fill_vector', None, [PTR(LibVector), c_double])
__deets__
User
Beiträge: 14480
Registriert: Mittwoch 14. Oktober 2015, 14:29

Ich finde deine convenience Klassen nicht besonders bequem. Wenn sie das sein sollen, dann sollten sie ein Python Interface bereitstellen. Du mischst aber Python mit dem Zwang, das unterliegende C-Wrapper-Objekt dann doch von Hand erzeugen zu müssen. Das ist wenig sinnvoll.

Zu deinem Fehler kann man leider nicht wirklich etwas sagen. Denn die Ursachen können überall liegen. Ich kann nur allgemeine Tipps geben: Versuch zuerst mal deinen Fehlerfall so nah am “Metall” wie möglich nachzubauen. Also nur ctypes wrapper nutzen. Keinerlei convenience drüber, kein __del__, nix. Außer get_func oder so, das sollte ok sein. Aber wichtig ist, das du damit quasi in Python einfach das Beispiel In C nachbaust. Zeile für Zeile.

Und dann nimmst du stückweise mehr Abstraktion rein, bis es kracht.
Plexian
User
Beiträge: 10
Registriert: Mittwoch 15. Juli 2020, 20:24

@__deets__ Was gefällt dir an den Python Klassen nicht? Der Konstruktor ist eher für interne Zwecke gedacht, um eben das C-Objekt schnell in der "besseren" Klasse zu wrappen. Zum Erstellen sind dann die classmethods gedacht, wie im Beispiel dann new(). Das habe ich absichtlich so gewählt, da die meisten Klassen a) mehrere Konstruktoren haben, und ich das so übersichtlicher fand.

Das Ding ist, das habe ich schon probiert, sonst wäre ich nicht hier. Wenn ich nur die Lib-Klassen verwende, gehts ohne Probleme. Wenn ich komplett die "convenience" Klassen nehme, aber in niedrigen Dimensionen mich befinde (dim=64), gehts auch (!).
Es gibt aber nicht einen Punkt, wo ich dann eine Convenience-Klasse mehr nehme und das dann nicht mehr geht. Wie gesagt, es langt wenn ich einen Wert in einer Variable speichere anstatt ihn direkt zu verwenden, und auf einmal gehts. Daher kann ich auch so schwer eine konkrete Frage stellen, da es wohl nicht an meinen Klassen spezifisch liegt, sondern generell an einem Missverständnis, was die Speicherverwaltung anbelangt.
Plexian
User
Beiträge: 10
Registriert: Mittwoch 15. Juli 2020, 20:24

Vielleicht hier ein konkreteres Beispiel, ich merke, ich habe mich etwas wage ausgedrückt. Nochmal aber als Hinweis, es gibt mehr als einen Weg, mein Problem zu reproduzieren, dies ist nur einer davon.

Für das Gleichungssystem muss wie gesagt der Vektor x mit einer Dimension erstellt werden. Diese Dimension ist ein Feld einer anderen C-Struct vom Typ unsigned integer. Nun habe ich, damit man in den Wrapperklassen direkt auf die Felder zugreifen kann, eine BasisKlasse geschrieben, die die __getattr__()-Methode überschreibt.
Das Objekt, welches die Dimension hält, heißt `d` und ist vom Typ Surface. Nun könnte man auf das benötigte Feld wie folgt zugreifen (da `d` einen Pointer auf das Lib-Objekt hält):

Code: Alles auswählen

x = Vector.new(d._as_parameter_.contents.tri)  # funktioniert
Mit Hilfe der folgenden __getattr__-Methode, gibt es aber einen "Getter", damit man direkt auf `tri` zugreifen kann:

Code: Alles auswählen

 # Basisklasse
    def __getattr__(self, name):
        # C struct doesnt have this field
        if name not in self.avail_fields():  # -- self.avail_fields() gibt die verfügbaren Felder des LibObjekts zurück, darunter also auch 'tri'
            raise AttributeError(f'\'{self.__class__.__name__}\' has no attribute \'{name}\'')

        # Corresponding method for field is not implemented in this class
        getter = f'_{self.__class__.__name__}__getter_{name}'
        if getter not in dir(self.__class__):
            raise AttributeError(f'\'{self.__class__.__name__}\' has no getter for attribute \'{name}\'')

        # Find getter method and invoke
        return getattr(self, getter)()


# Surface
    def __getter_tri(self) -> int:
        return self._as_parameter_.contents.tri


# Beispieldatei
print(d.tri)
So, und hier die Ergebnisse, wenn man unterschiedlich auf die Werte zugreift

Code: Alles auswählen

    x = Vector.new(d._Surface__getter_tri())  # funktioniert
    x = Vector.new(d.tri)  # funktioniert nicht
    x = Vector.new(d._as_parameter_.contents.tri)  # funktionert
Und obwohl die mittlere Variante nicht geht, wenn ich d.tri ausgebe, kommt der richtige Wert, und auch wenn ich d.tri vorher in einer Variable speichere und dann die Variable übergebe, dann gehts auch.

Wie ich meinte, das scheint kein Programmierfehler zu sein, sondern ein Verständnisfehler der Speicherverwaltung. Die __getattr__-Nutzung funktioniert in allen anderen Fällen auch einwandfrei und wenn d.tri klein genug ist, gehts ja auch.
__deets__
User
Beiträge: 14480
Registriert: Mittwoch 14. Oktober 2015, 14:29

Der Ansatz mit dem new ist zwar nicht per se schlecht, aber in Python doch ungewöhnlich, solange __init__ so wie bei dir zur Verfügung steht.

Ich habe persönlich noch nie mit den ganzen as args und anderen Funktionen gearbeitet. Und das wäre auch mein erster Kandidat für etwas, das da schief geht. Ich würde auch das erstmal rauskürzen.

Das die verschiedenen Größen eine solche Rolle spielen ... da würdevoch trotzdem deine niedrigeren Layer im Verdacht haben. Denn wenn die zb von Stack allokieren statt von heap, oder den vorschnell frei geben, dann ist je grosser der Block je höher die Wahrscheinlichkeit, dass der korrumpiert wird durch Code der danach einfach läuft & wie in Python üblich einfach viel Speicher allokiert und freigibt.

Kannst du mit deinen primitiven Arbeiten, und dann mal zwischen Erzeugung und Nutzung das Programm mal ne Menge anderer Dinge machen lassen? Zb ein paar rekursive Funktionen die ne Menge Listen anlegen? So das du auch da ein bisschen mehr Speicherdruck bekommst.
Benutzeravatar
__blackjack__
User
Beiträge: 12984
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@Plexian: Nur mal so nebenbei: Die doppelten führenden Unterstriche haben da nichts zu suchen. Das macht das ganze auch unnötig komplizierter als es sein müsste, dass Du das „name mangling“ dann manuell wieder umschiffen musst.

Und man kann auch " als Begrenzer für Zeichenketten verwenden. Oder ''' oder """. Das man Anführungszeichen tatsächlich mit \ escapen muss, kommt eigentlich nie vor. Ich würde die auch nicht manuell setzen sondern mit dem Formatierungszusatz !r einfach dafür sorgen das die `repr()`-Form und nicht die `str()`-Form eingesetzt wird. Also: ``raise AttributeError(f"{self.__class__.__name__!r} has no attribute {name!r}")``. Wobei man sich das hier eigentlich auch sparen könnte, weil in Namen ja eigentlich nichts vorkommen kann was die Ausgabe mehrdeutig machen könnte.
“Most people find the concept of programming obvious, but the doing impossible.” — Alan J. Perlis
Plexian
User
Beiträge: 10
Registriert: Mittwoch 15. Juli 2020, 20:24

So, nachdem ich mir das Ganze nochmals genauer angeschaut habe, also auch die C Bibliothek analysiert habe, bin ich zu folgendem Schluss gekommen:
Pythons Speicherverwaltung funktioniert offentsichtlich anders als in C. Wenn ich also in C Speicher für besagten Vektor x alloziere, scheint dieser immer ungenutzt zu sein (ergo nur 0.0), in Python aber nicht.
Warum das so ist, weiß ich nicht, aber wundert mich auch nicht sonderlich, da in Python ja logischerweise (da es ja ein Wrapper ist) mehr passiert, kann man ja nicht davon ausgehen.
Ich muss in Python also nur sicherstellen, dass der Speicher anfangs auch auf 0 gesetzt wird, und dann passt das Ganze.

@__blackjack__: Was soll an den führenden Unterstrichen falsch sein? Ich habe diese absichtlich gesetzt, da diese Methoden nicht gedacht sind, um von außen genutzt zu werden. Mir ist klar, dass das immer noch geht, aber meines Wissens nach, werden dann eben diese __ genutzt, um das zu indizieren.
Das unnötige Escapen von '' habe ich bereits entfernt, das war mehr Macht der Gewohnheit, ist mir dann auch aufgefallen, trotzdem danke!
__deets__
User
Beiträge: 14480
Registriert: Mittwoch 14. Oktober 2015, 14:29

Dein Wissen da ist falsch. __ wird zur Vermeidung von Namenskollisionen benutzt. Ich kann an einer Hand abzaehlen, wie oft ich das in den letzten 10 Jahren nutzen musste. Per Konvention ist ein einzelner Unterstrich die Kennzeichnung von "das ist privat, Finger weg". Und einen ernsthafter Schutz stellen die __ auch nicht dar, wie folgendes Beispiel illustriert:

Code: Alles auswählen

class Foo:

    def __init__(self):
        self.__test = 1000

f = Foo()
print(f._Foo__test)
Benutzeravatar
__blackjack__
User
Beiträge: 12984
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@Plexian: Wenn dieser Unterschied alleine am anfordern von Speicher liegt, dann ist das aber in C wohl auch eher Zufall und Du kannst Dich da nicht darauf verlassen, dass der Speicher mit 0en initialisiert ist.
“Most people find the concept of programming obvious, but the doing impossible.” — Alan J. Perlis
Antworten