Seite 1 von 1

DLL mit ctypes ansprechen (typedef struct, typedef enum)

Verfasst: Montag 15. Juni 2020, 09:13
von Falk0
Hallo,

ich stehe gerade vor dem Problem eine DLL mithilfe von ctypes ansprechen zu müssen. Diese DLL hat viele verschiedene Attribute wovon ich die einfachen schon erfolgreich ansprechen konnte. Bei den etwas komplizierteren mit "typedef struct / enum" komme ich gerade nicht weiter.

Das ist der C-Code zum Ansprechen der DLL:

int32_t CAENDPP_API
CAENDPP_AddBoard(
int32_t handle,
CAENDPP_ConnectionParams_t connParams,
int32_t *boardId
);

//----------------
//Types Definition
//----------------

typedef struct {
CAENDPP_ConnectionType LinkType;
int32_t LinkNum;
int32_t ConetNode;
uint32_t VMEBaseAddress;
char ETHAddress[IP_ADDR_LEN + 1];
} CAENDPP_ConnectionParams_t;
//----------------

typedef enum {
CAENDPP_USB = 0,
CAENDPP_PCI_OpticalLink = 1,
CAENDPP_ETH = 2,
CAENDPP_Serial = 3,
} CAENDPP_ConnectionType;

//--------------------
//Constants Definition
//--------------------

#define IP_ADDR_LEN 255


Mein Versuch das Problem zu lösen sieht bisher so aus:

from PyQt5 import QtWidgets, uic, QtCore
from PyQt5.QtCore import (QCoreApplication, QObject, QRunnable, QThread,
QThreadPool, pyqtSignal)
from PyQt5.QtWidgets import QApplication, QWidget, QInputDialog, QLineEdit, QFileDialog

import sys
import time
from random import randint

import ctypes
from enum import IntEnum

def wrap_function(lib, funcname, restype, argtypes):
"""Simplify wrapping ctypes functions"""
func = lib.__getattr__(funcname)
func.restype = restype
func.argtypes = argtypes
return func

class CAENDPP_ConnectionType(IntEnum):
CAENDPP_USB = 0
CAENDPP_PCI_OpticalLink = 1
CAENDPP_ETH = 2
CAENDPP_Serial = 3

class ConnectionParams_t(ctypes.Structure):
_fields_ = [('LinkType', CAENDPP_ConnectionType),
('LinkNum', ctypes.c_int),
('ConetNode', ctypes.c_int),
('VMEBaseAddress', ctypes.c_uint),
('ETHAddress', ctypes.c_char * 256)]

def __init__(self, ha, hb, hc, hd, he):
hb_c = (ctypes.c_int * 1)(*hb)
hc_c = (ctypes.c_int * 1)(*hc)
hd_c = (ctypes.c_uint * 1)(*hd)
he_c = (ctypes.c_char * 256)(*he)
super(Histotype, self).__init__(ha, hb_c, hc_c, hd_c, he_c)

...

c_int_p = ctypes.POINTER(ctypes.c_int)
c_int_pp = ctypes.POINTER(c_int_p)
load = wrap_function(MyDllObject, 'CAENDPP_AddBoard', ctypes.c_int, [c_int_p, ctypes.POINTER(ConnectionParams_t),c_int_pp])
# Instantiate an int * pointer.
# Use byref to pass the address of the pointer.
d1 = ctypes.c_int
linktype = CAENDPP_ConnectionType
linknum = 1
conetnode = 2
vmebaseaddress = 3
ethaddress = ctypes.c_char * 256
d2 = ConnectionParams_t(linktype,linknum,conetnode,vmebaseaddress,ethaddress)
d3 = c_int_p()
DLLanswer = load(ctypes.byref(d1),ctypes.byref(d2),ctypes.byref(d3))


Ich bekomme bisher die Fehler-Meldung:
TypeError: second item in _fields_ tuple (index 0) must be a C type



Kann mir jemand helfen? Gern auch mit einer allgemeinen Lösung für die Übersetzung von "typedef struct" in Python ctypes.
Vielen Dank, euch!

Re: DLL mit ctypes ansprechen (typedef struct, typedef enum)

Verfasst: Dienstag 16. Juni 2020, 11:22
von Falk0
Ich habe das Problem nun soweit gelöst. Mit folgendem Code funktioniert es:

Code: Alles auswählen

import sys
import ctypes
from ctypes import byref, c_uint8, c_int16, c_int32, c_uint32, c_double
from ctypes import c_int32 as c_enum

def wrap_function(lib, funcname, restype, argtypes):
    """Simplify wrapping ctypes functions"""
    func = lib.__getattr__(funcname)
    func.restype = restype
    func.argtypes = argtypes
    return func

class ConnectionParams_t(ctypes.Structure):
    _fields_ = [('LinkType', c_enum), 
                ('LinkNum', ctypes.c_int),
                ('ConetNode', ctypes.c_int),
                ('VMEBaseAddress', ctypes.c_uint),
                ('ETHAddress', ctypes.c_char * 256)]

    def __init__(self, ha, hb, hc, hd, he):
        hb_c = ctypes.c_int(hb)
        hc_c = ctypes.c_int(hc)
        hd_c = ctypes.c_uint(hd)
        super(ConnectionParams_t, self).__init__(ha, hb_c, hc_c, hd_c, he)

    def __repr__(self):
        return 'LinkType: {0}\nLinkNum: {1}\nConetNode: {2}\nVMEBaseAddress: {3}\nETHAddress: {4}'.format(self.LinkType[:], self.LinkNum[:], self.ConetNode[:], self.VMEBaseAddress[:], self.ETHAddress[:])

class MainWindow(QtWidgets.QMainWindow):
...

Code: Alles auswählen

            MyDllObject = ctypes.cdll.LoadLibrary('CAENDPPLib.dll')
            # CAEN_AddBoard
            # --------------------------------------------------------------------------------
            c_int_p = ctypes.POINTER(ctypes.c_int)
            c_int_pp = ctypes.POINTER(c_int_p)
            load = wrap_function(MyDllObject, 'CAENDPP_AddBoard', ctypes.c_int, [c_int_p, ctypes.POINTER(ConnectionParams_t),c_int_pp])
            # Instantiate an int * pointer.
            # Use byref to pass the address of the pointer.
            d1 = ctypes.c_int()
            linktype = c_enum(1)
            linknum = 1
            conetnode = 2
            vmebaseaddress = 3
            ethaddress = b'01234567890'
            d2 = ConnectionParams_t(linktype,linknum,conetnode,vmebaseaddress,ethaddress)
            d3 = c_int_p()
            DLLanswer = load(ctypes.byref(d1),d2,ctypes.byref(d3))
            self.textBrowser.append('AddBoard: '+str(DLLanswer))
            # --------------------------------------------------------------------------------

Re: DLL mit ctypes ansprechen (typedef struct, typedef enum)

Verfasst: Dienstag 16. Juni 2020, 16:32
von __blackjack__
@Falk0: Im ersten Beispiel dürfte das Problem sein, dass Du bei `super()` die falsche Klasse als erstes Argument übergibst. Ich würde von `super()` sowieso die Finger lassen, das ist nämlich alles einfache als einfach zu verstehen. Die Namen `ha`, `hb`, `hc`, `hd`, `he`, `hb_c`, `hc_c`, und `hd_c` sind ein schlechter Witz. Was soll das? Zudem sehe ich nicht warum das nötig sein sollte. Die komplette `__init__()` kann man sich sparen.

`linktype` hat im ersten Beitrag einen falschen Wert, nämlich die das `Enum`-Objekt und keinen konkreten Wert. Das `CAENDPP_` würde ich da auch nicht überall in die Namen schreiben. Das ist da ja nur weil C keine Namensräume kennt. In Python steckt das ja in einem eigenen Modul das diesen Namensraum repräsentiert. Oder sollte es zumindest, ich sehe da bei Dir irgendwelchen Qt-Kram.

Wenn die C-Headerdateien Datentypen mit expliziten Bitgrössen verwenden, sollte man das auch in der Python-Anbindung so übernehmen und dort nicht wieder die Typen bei denen die Bitgrössen plattform- oder ABI-abhängig sind. Umgekehrt ist das ``c_int32 as c_enum`` gefährlich, denn das funktioniert nur wenn `c_int` das gleiche wie `c_int32` ist, was natürlich nicht garantiert ist.

Bei `ConnectionParams_t` würde ich das `_t` weglassen. Das ist eine in C übliche Abkürzung um Datentypen zu kennzeichnen. Das eine Klasse einen Datentypen definiert ist implizit klar.

Die ”magischen” Methoden sollte man nur direkt aufrufen wenn es keinen anderen offiziellen weg gibt. Statt `__getattr__()` also die `getattr()`-Funktion.

Die Argumenttypen für die Funktion sind falsch. Das erste und das letzte Argument hat jeweils eine ”indirektion” zu viel. Das erste ist kein Zeiger auf ein int32_t sondern direkt ein int32_t und das letzte ist kein Zeiger auf einen Zeiger auf ein int32_t sondern nur ein Zeiger auf ein int32_t.

Bei `MyDllObject` sind `My` und `Object` überflüssig. `My` sagt nichts aus und `Object` letztlich auch nicht weil alles was man in Python an einen Namen binden kann, ein Objekt ist. `Dll` ist dann aber auch nicht mehr so wirklich toll.

`load` ist ein komischer Name für diese `AddBoard`-Funktion. Und die würde man noch mal in eine Python-Funktion verpacken, damit der ganze ”komische” C-Kram nicht den Python-Code ”verseucht” der den dann verwendet. Denn das `ConnectionParams_t` wird sonst nirgends verwendet, da gibt es also keinen Grund das nicht als Argumente „platt“ zu machen. Und die C-Funktionen verwenden den Rückgabewert als Fehlercode. Das will man in Python an *einer* Stelle für alle Funktionen behandeln und aus dem Fehlercode eine Ausnahme machen. Die tatsächlichen Rückgabewerte von den C-Funktionen sind ”out”-Parameter, also Argumente wo ein Zeiger für das Ergebnis übergeben wird. Das sollte auf Python-Seite in tatsächliche Rückgabewerte verwandelt werden.

Also eigentlich keine Funktion, sondern eine Methode, denn die ganzen Funktionen von der C-API wollen ja das Handle von `CAENDPP_InitLibrary()` als Argument haben. Es gibt auch eine Aufräumfunktion. Das alleine wäre ein Grund für einen Kontextmanager.

Von Python aus verwenden möchte man das am Ende ja beispielsweise so:

Code: Alles auswählen

from caen_dpp_lib import ConnectionType, DPP


def main():
    with DPP() as dpp:
        board = dpp.add_board(
          ConnectionType.PCI_OpticalLink, 1, 2, 3, "01234567890"
        )
        board.check_communication()
        print(board.is_aquiring)
        print(board.info.model_name)
        
        for channel in dpp:
            print(channel.temperature)