Die Union löst aber die Probleme nicht:
- Wenn sie in der Library angelegt wird, muss die Library jeden Treiber "kennen", damit dessen struct Teil der union werden kann.
- Nicht alle Compiler unterstützen den anonymen "Durchgriff" in die einzelnen structs, da dies nicht C-Standard-konform ist. Also muss die Library die Elemente der structs mit vollem Namen ansprechen, wodurch sie wieder eine Fallunterscheidung für alle einzelnen Treiber machen muss.
typedef struct
{
int a;
int b;
} A_STRUCT;
typedef struct
{
int a;
int b;
} B_STRUCT;
union
{
A_STRUCT struct1;
B_STRUCT struct2;
} uUnion;
// das geht nicht bei allen Compilern:
uUnion.a = 1;
// das hier ist standardkonform, benoetigt aber eine Fallunterscheidung in der Lib:
uUnion.struct1.a = 1;
Display More
Für das ursprüngliche Problem (Code arbeitet mit einer Abstraktion von Daten, nicht mit deren konkreten Ausprägung) gibt es in objektorientierten Sprachen das Konzept der Vererbung: Die Treiber werden als Klassen implementiert, die alle von einer gemeinsamen Basisklasse abgeleitet werden. Die Library arbeitet ausschließlich mit den Methoden und Daten der Basisklasse, während die Treiber diese beliebig erweitern und an ihre konkrete Implementierung anpassen können.
Dieses Konzept kannst du in C nachbilden. Dazu wurde im C-Standard extra die Eigenschaft definiert, dass Strukturen, die mit gleichen Elementen beginnen, vom Compiler immer gleich angelegt werden müssen. Damit werden sie kompatibel im Sinne einer Basisklasse:
// Definition in einem gemeinsamen Header
typedef struct
{
char *devicename;
int anzahl;
float skalierung;
} DEVICE_BASE;
// Definition in Treiber A
typedef struct
{
char *devicename;
int anzahl;
float skalierung;
// -------------------
int spezifischesElement;
char array[200];
} DEVICE_A;
DEVICE_A TypeADevice;
// Definition in Treiber B
typedef struct
{
char *devicename;
int anzahl;
float skalierung;
// -------------------
int anderesElement;
char daten[50];
} DEVICE_B;
DEVICE_B TypeBDevice;
// Um der Library zu sagen, welchen konkreten Treiber sie benutzen soll, bekommt sie
// zur Laufzeit oder auch statisch beim Compilieren einen "Base-Pointer"
// uebergeben:
DEVICE_BASE* ptrDevice = (DEVICE_BASE*)&TypeBDevice; // oder TypeADevice
// Die Library greift dann ueber diesen Pointer auf den Treiber zu:
ptrDevice->anzahl = 7;
Display More
Die Library ist somit total unabhängig von den Treibern und kann auch mit noch gar nicht existierenden Treibern zusammenarbeiten, solange diese sich später an die vereinbarte Schnittstelle halten. Wie von dreamshader beschrieben, kann man sogar Funktionen im Treiber aufrufen, wenn man diese als Funktionspointer in der Struktur ablegt. Die Treiber können die Struktur darüber hinaus beliebig erweitern. Natürlich kann die Library mit diesen Erweiterungen nicht umgehen.
Wenn du diesen Ansatz umsetzt, solltest du übrigens trotzdem darüber nachdenken, jeder Funktion den Treiber-Pointer zu übergeben. Dadurch könnte die Library später sogar gleichzeitig mit mehreren Treibern umgehen.