Circular Import bei Ctypes

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

Moin Community,

ich arbeite momentan das erste Mal mit ctypes und versuche, eine (relativ große) C-Bibliothek in Python zu wrappen mit ctypes. Nun besteht die Bibliothek aus recht vielen Structs, welche ich dann mit Python-Klassen wrappe. Nun habe ich ein Problem mit meinem Datenmodell.
Vereinfacht mal folgende Situation: In C existiert eine struct vector und die Methoden new_vector(), del_vector() und norm2_vector(). In Python habe ich nun eine Klasse Vector(ctypes.Structure) mit entsprechenden Methoden __init__(), __del__() und norm2().
Problem ist nun Folgendes:
Man kann sich vielleicht vorstellen, dass einige der C-Methoden mehr als nur einmal genutzt werden in Python. Daher wollte ich eine weitere Datei nutzen, lib_interface.py, die einfach alle Methoden der Bibliothek bereitstellt, welche ich dann überall importiere, wo sie gebraucht wird. Um allerdings die Methoden zu definieren, brauche ich die Wrapper-Klassen der structs, damit die Argumentstypen gesetzt werden können. Dabei entsteht ein Kreisimport.

Nun weiß ich leider nicht, wie ich dieses Problem umgehen kann, und hoffe hier auf Hilfe. Mir ist natürlich klar, dass ich die Methode immer da definieren kann, wo ich sie brauche, aber dann ist die Information, welche Methode welche Argumente hat, mehrfach vorhanden und damit redundant. Hiermal mein Beispiel in Codeform, um das Ganze etwas zu verdeutlichen:

Code: Alles auswählen

# vector.py
from ctypes import Structure
import lib_interface as lib

class Vector(Structure):
    _fields_ = [ ... ]

    def __init__(self, dim):
        self.obj = lib.new_vector(dim)

    def __del__(self):
        lib.del_vector(self.obj)
        
    def norm(self):
        return lib.norm2_vector(self.obj)

Code: Alles auswählen

# lib_interface.py
from ctypes import c_uint, c_double, POINTER as PTR
from vector import Vector

lib = ctypes.CDLL('lib/xyz.so')
def func(name, returntype, argtypes):
    func = lib.__getattr__(name)
    func.restype = returntype
    func.argtypes = argtypes
    return func

new_vector = func('new_vector', PTR(Vector), [c_uint])
del_vector = func('del_vector', None, [PTR(Vector)])
norm2_vector = func('norm2_vector', c_double, [PTR(Vector)]
Ich bin über Hilfe dankbar,
Gruß Plexian
Benutzeravatar
__blackjack__
User
Beiträge: 13004
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@Plexian: Man kann da einfach noch eine weitere Ebene einziehen, beziehungsweise die jetzige Trennung zwischen Low-Level und Python-Seite strikter durchziehen in dem man die `struct`\s auch bei den C-Funktionen definiert. Also in diesem Fall das ``class Vector(Structure): _fields_ = [ ... ]`` mit in die ``lib_interface.py``, aber *ohne* die Methoden. Und in der ``vector.py`` erbt man dann von `lib_interface.Vector` und packt dort dann erst die Methoden dazu. Dann braucht `lib_interface` nichts importieren was eigentlich von der Abstraktion her eine Ebene darüber liegt.

Das mit dem `self.obj` sieht ein bisschen komisch aus und passt nicht so wirklich zu den Signaturen die da ”deklariert” werden‽

Und bei `__del__()` sollte man sich der Gefahren bewusst sein. Bei so etwas wie Vektoren fällt mir da auch nicht so wirklich was besseres ein, ausser wohl da nicht `ctypes` zu verwenden, sondern eine C-Erweiterung zu erstellen, beispielsweise mit cython.
“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

@__blackjack__
Die Einführung einer extra "Zwischenklasse" hat tatsächlich wunderbar geklappt, vielen Dank dafür.

Mir ist auch schon aufgefallen, dass das mit dem self.obj nicht optimal ist. An sich ist die Python-Klasse ja schon ein 1-zu-1-Abbild der C-Struct, allerdings gibt es in der C-Bibliothek verschiedene Methoden (so wie new_vector) zum Initialisieren der struct, die ich natürlich verwenden will.
Aber ich kann ja nicht einfach den Rückgabewert von new_vector mir selbst zuweisen, oder doch?

Was genau meinst du mit Gefahren bei __del__() ? Idee war, dass wenn das Python-Objekt freigegeben wird, auch die Destruktor-Methode der C-Bibliothek aufgerufen wird. Aber das Ganze scheint ja sowieso anders zu gehören, wenn ich das self.obj schon falsch nutze.
Benutzeravatar
__blackjack__
User
Beiträge: 13004
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@Plexian: Das mit dem `self.obj` passt nicht dazu das die Klasse von `Structure` erbt. Das sollte sie nicht. Und statt `obj` würde ich das `_as_parameter_` nennen. Der Name hat eine besondere Bedeutung für `ctypes`.

`__del__` ist kein determinister Destruktor. Es ist nicht garantiert wann die Methode aufgerufen wird, oder ob sie überhaupt aufgerufen wird. Und wenn sie am Programmende aufgerufen wird ist nicht garantiert was dann noch alles im Speicher vorhanden ist. Man kann sich da fast nur auf Attribute auf dem Objekt selbst verlassen. Das `lib_interface`-Modul kann beispielsweise zu dem Zeitpunkt schon weg sein.

Dann ist da das Problem mit Kreisen auf die man aufpassen muss. Wenn das Objekt auf etwas verweist das direkt oder indirekt wieder auf das Objekt verweist, dann kann es sein das `__del__` nie aufgerufen wird und man ein Speicherleck hat.

Aus diesen Gründen sollte man `__del__` nicht oder nur in Ausnahmefällen verwenden. Einer der Ausnahmefälle ist IMHO dann auch tatsächlich das was hier gemacht wird — Ressourcenverwaltung bei `ctypes` — wenn die beiden Alternativen nicht wirklich in Frage kommen.

Die Alternativen sind Kontextmanager für die ``with``-Anweisung und `weakref.finalize()`. Wobei man die auch kombinieren kann.

``with`` ist bei Vektor-Objekten extrem unhandlich, das will sicher keiner mit so etwas benutzen, und bei `weakref.finalize()` ist die Frage wie viele Objekte da so erzeugt und vernichtet werden. Das hat sowohl was Speicher als auch Laufzeit angeht einen Overhead. Vom Gefühl meinem Gefühl her nichts was man für Vektor-Objekte haben möchte.

Ungetestet:

Code: Alles auswählen

import lib_interface as lib


class Vector:
    def __init__(self, dim):
        self._as_parameter_ = lib.new_vector(dim)
        self._del_vector = lib.del_vector

    def __del__(self):
        self._del_vector(self)

    def norm(self):
        return lib.norm2_vector(self).value
“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

@__blackjack__ :
Ich verstehe. Allerdings scheine ich jetzt noch eine andere Option gefunden zu haben.
Momentan sieht es bei mir so aus, wie du es vorgeschlagen hattest, sprich
1. eine Klasse Vector welcher das C-Object im _as_parameter_-Feld hält
2. eine LibVector-Klasse, die von Structure erbt, die Felder bereitstellt und als Typ in den C-Methoden verwendet wird
Und das Ganze funktioniert soweit

Alternativ ginge aber auch folgendes:
Die Vector-Klasse erbt nun von LibVector und überschreibt die __new___-Methode wie folgt

Code: Alles auswählen

class AVector(LibAVector):
    def __new__(cls, *args, **kwargs):
        dim = args[0] if len(args) > 0 else 0
        instance = lib.new_avector(dim).contents
        instance.__class__ = AVector
        return instance
Damit funktioniert alles genauso, und ich hab zusätzlich sogar die Felder zur Verfügung, ohne extra Methoden zu schreiben. Ich stelle mir sogar Speichervorteile hierbei vor.
Spricht etwas dagegen, das so umzusetzen? Mir gefällt letztere Variante besser, da man "dichter" an der C-struct ist, als wenn man praktisch eine Wrapper-Klasse schreibt.
Benutzeravatar
__blackjack__
User
Beiträge: 13004
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@Plexian: Mir wäre das zu viel Magie. Und falls man das bisschen näher an der Strukturklasse tatsächlich braucht würde ich das nicht mit `ctypes` sondern mit `cython` anbinden.
“Most people find the concept of programming obvious, but the doing impossible.” — Alan J. Perlis
Antworten