C-Dll Multi-Instanz

Python in C/C++ embedden, C-Module, ctypes, Cython, SWIG, SIP etc sind hier richtig.
Antworten
Benutzeravatar
lightos
User
Beiträge: 39
Registriert: Montag 12. Dezember 2011, 19:39
Wohnort: Raum Bruchsal
Kontaktdaten:

Grundsatzfrage:

Ich möchte mit ctypes eine C-Dll in einer Python-Klasse kapseln.
Die C-Dll kann über das C-API mit OpenDevice/CloseDevice Hardware-Devices (z.B. am USB) öffnen.

Ist es nun sinnvoller, pro python Klasse ein Device zuzulassen (d.h. jedes Python-Objekt für eine C-Instanz hat genau ein "Handle"). Die Dll wird damit pro Python-Instanz n-mal geöffnet.

Oder, alternativ eine Liste der Instanzen innerhalb einer Python-Klasse zu führen. Die Dll wird in __init__ einmal
geöffnet und über open/close Funktionen werden die C-Devices geöffnet.
Problematisch sehe ich hier u.U. den "Aufräumaufwand" um das Ganze konsistent zu halten.

Hat hierzu jemand Erfahrungen bzgl. Vor/Nachteile?
Jede Anmerkung/Tipps sind gerne willkommen.
BlackJack

@lightos: Ich bin ein bisschen von der Beschreibung verwirrt‽ Was meinst Du mit „[d]ie Dll wird in __init__ einmal
geöffnet”? Dass Du eine Klasse hast die in ihrer `__init__()` den Code stehen hat um die DLL zu laden? Solange da nicht wirklich gute Gründe dafür sprechen würde ich das auf Modulebene erledigen. Und dort danach erst einmal die ganzen Funktions-Proxys erstellen, die man verwenden will. Nicht-öffentliche bekommen einen `_` als Präfix. Und daraus bastelt man sich dann die Datentypen für eine „pythonische” API. Da das Modul für die DLL steht, kann man es als Singleton sehen was durch das importieren erstellt wird. Sofern die DLL keine generellen Funktionen zum initialisieren und aufräumen besitzt, die man aufrufen muss. In dem Fall gehört in das Modul noch ein Proxytyp, der am besten das Kontextmanager-Protokoll implementiert (`__enter__()` und `__exit__()` für ``with``).

Die DLL sollte normalerweise auch nur *einmal* geladen werden, auch wenn man mehr als einmal die entsprechende Funktion aufruft. Ähnlich wie beim importieren von Python-Modulen.

Die Objekte die von `open()` zurückgegeben werden sollten neben einer `close()`-Methode auch Kontextmanager sein.
Benutzeravatar
lightos
User
Beiträge: 39
Registriert: Montag 12. Dezember 2011, 19:39
Wohnort: Raum Bruchsal
Kontaktdaten:

OK, danke. Das heißt dann also, alle Python Funktionen und die Dll-Zuweisung sind modul global?

D.h. nur die eigentlichen Device-Instanzen werden dann in Python-Klassen gekapselt.

Wenn aber im Modul dann steht:

dll = ctypes.WinDLL("xyz_api.dll")

und das Modul dann von anderen mehrfach importiert wird, was macht dann diese Zeile?


Alternativ sah ich bisher die Dll-Python Klasse als "root" und die jeweiligen Instanzen werden von dieser erzeugt.


Gibt es für diesen "Standardfall" ein gutes Referenz-Beispiel?

Offensichtlich gibt es im Python-Umfeld die verschiedensten Ansätze.
BlackJack

@lightos: Code auf Modulebene wird nur einmal ausgeführt wenn das Modul das erste mal importiert wird. Bei jedem weiteren Import wird nur das bereits importierte Modul-Objekt verwendet. Module sind in Python also Singletons.

Das gleiche gilt auch für das laden von DLLs. Egal wie oft Du die DLL mit `ctypes.WinDLL()` „lädtst”, Du bekommst immer Proxy-Objekte auf die selbe DLL im Speicher zurück:

Code: Alles auswählen

n [16]: a = ctypes.CDLL('./libgen.so')

In [17]: b = ctypes.CDLL('./libgen.so')

In [18]: a._handle
Out[18]: 173267208

In [19]: b._handle
Out[19]: 173267208
Es macht also nicht viel Sinn das immer und immer wieder zu machen, weil sich die damit erzeugten Proxy-Objekte immer auf das selbe DLL-Exemplar beziehen.

Das heisst grundsätzlich kann man sagen eine DLL ist sozusagen ein Singleton, und ein Python-Modul ist auch ein Singleton, also kann man erst einmal eine 1:1 Beziehung zwischen Modul und DLL herstellen. Davon würde ich nur abweichen wenn die DLL per Funktionsaufruf initialisiert werden muss *und* bei dieser Initialisierung Parameter vom Benutzer der DLL übergeben werden sollen. Denn das kann man beim Laden des Moduls per ``import`` nicht realisieren. In dem Fall würde ich einen Singleton-Typ bereitstellen, mit dem man die DLL initialisieren kann. Falls die DLL am Ende Aufrufe zum Aufräumen verlangt, sollte der Singleton-Typ auch ein Kontextmanager sein.

Initialisierung ohne Parameter oder mit festen Parametern dagegen kann man auf Modulebene machen. Aufräumen für ein Modul lässt sich mit dem `atexit`-Modul realisieren, falls die DLL Funktionsaufrufe zum Aufräumen verlangt.

Welche Unterschiedlichen Ansätze meinst Du denn? Ich denke der Anfang ist immer gleich: Python-Modul importiert auf Modul-Ebene die DLL, weil beides Singletons sind. Und dann erstellt man erst einmal die Datentyp- und Funktionsproxies für die DLL-Funktionen die man wrappen will, und versieht die Funktionsproxies mit Signaturen und hängt Fehlerbehandlung dran, also Fehlercodes → Ausnahme(n).

Dann kann man entscheiden welche von den DLL-Funktionen man als öffentliche API bereitstellen möchte, welche Hilfs-/Komfortfunktionen man schreibt und welche objektorientiere API man sich aus den Funktionsproxies (und Datentypenproxies) basteln möchte.

Ob das Modul die DLL repräsentiert oder ein zusätzliches Proxyobjekt würde ich wie gesagt davon abhängig machen ob man das machen muss. KISS — Keep It Simple & Stupid.
Benutzeravatar
lightos
User
Beiträge: 39
Registriert: Montag 12. Dezember 2011, 19:39
Wohnort: Raum Bruchsal
Kontaktdaten:

Jetzt hab ich's soweit kapiert.
Danke für die ausführliche Erklärung.

Leider gibt es hier genügend Python API Code von Firmen, die das genau nicht beachten!

Werde nun erstmal versuchen das "korrekt" aufzusetzen und hier als Sample posten.
BlackJack

@lightos: Hier im Forum hatte ich mal ein Quelltext-Schnippsel mit einem kleinen Ausschnitt der ``libusb`` gepostet: http://www.python-forum.de/viewtopic.ph ... 90#p229590

Da würde ich den Test für die Ausnahme als ``errcheck` an das Funktionsproxy-Objekt von `_init()` hängen statt im Code der das benutzt den Rückgabewert zu prüfen.

Ansonsten habe ich noch https://bitbucket.org/blackjack/pysensors ins Netz gestellt. Da steht auf meiner TODO-Liste (wenn ich da mal Zeit zu habe :-)) das `init()` und `cleanup()` von der Modulebene in einen Kontextmanager zu verschieben.
Antworten