DLL in C sauber schreiben



  • Hallo zusammen,

    ich stehe vor dem Problem, dass ich eine DLL schreiben möchte, deren Funktionalität über eine ANSI-C Schnittstelle angesteuert werden soll. Ich habe bisher C++ programmiert und stehe deshalb vor dem Problem, wie ich diese DLL sauber umsetzen kann.

    Nehmen wir an, ich habe eine Klasse Auto mit den internen Variablen "Zahl der Passagiere", "Leergewicht des Autos", und "Motor". Diese können Über Funktionen der Klasse gesetzt und ausgewertet werden, außerdem gibt es noch die Methode

    double beschleunige(),

    die über irgendeine Formel ausrechnet, wie lange das Auto benötigt, um von 0 auf 100 zu beschleunigen.

    In C++ würde ich das für meine DLL jetzt so lösen, dass ich den Konstruktor der Klasse, die set- und get-Funktionen, sowie die Funktion "beschleunigen" per dllexport zur Verfügung stelle. In dem Programm, das auf die DLL zugreift, würde ich mir eine Instanz der Klasse erstellen, sie benutzen und anschließend den Speicher wieder freigeben.

    Jetzt ist meine Frage, wie setzt man sowas in C am saubersten um? Nehmen wir zum Beispiel die Anzahl der Passagiere... löst man sowas über eine globale Variable? Wo wird der Wert dafür am besten gespeichert? Es müssen ja verschiedene Funktionen (set, get und beschleunigen) auf diesen Wert zugreifen... Wie lässt sich das ohne Klassen vernünftig lösen?`

    Vielen Dank für eure Hilfe und die hoffentlich wertvollen Anregungen.



  • - struct mit Variablen definieren
    - Funktionen implementieren, die jeweils einen Zeiger auf das Objekt (quasi this) übergeben bekommen
    - struct Objekt definieren, in der Initialisierungliste die Ausgangswerte für Variablen angeben
    - Funktionen mit Zeiger auf das Objekt aufrufen



  • Danke, für die schnelle Hilfe.

    Ich würde aber gerne vermeiden, dass die Funktionen einen Zeiger auf das Objekt (das struct, in dem die Variablen hinterlegt sind) übergeben bekommen.

    Die DLL soll ja als Schnittstelle für andere Programmierer dienen und dieses Vorgehen würde ja bedeuten, dass der Anwender der DLL die Werte der Variablen auch ohne die get- und set-Funktkionen direkt im struct ändern kann, oder nicht? Dies würde ich gerne ausschließen, um in meinem Programm von der Integrität der Daten ausgehen zu können.

    Was ich im ersten Post vergessen habe: Ich habe die Klasse Auto bereits in C++ fertiggestellt und würde sie gerne nutzen. Nur muss die Schnittstelle (also die Signatur der beteiligten Funktionen) dem ANSI-C Standard entsprechen.

    Könnte ich mir dann eine DLL schreiben, deren Quelltext etwa so aussieht:

    #define API extern "C" __declspec (dllexport)
    
    API int setLeergewicht(int gewicht){
       extern Auto* auto;
       if (auto == NULL){
          initAuto();
       }
       auto->setLeergewicht(gewicht);
       return 0;
    }
    
    int initAuto(){
       extern Auto* auto;
       if (auto == NULL){
          auto = new Auto();
       }
       auto->setLeergewicht(1500)
       auto->setPassagiere(1)
       auto->setMotor(3);
       return 0;
    }
    
    Auto* auto = NULL;
    

    Das müsste ja funktionieren, nur mache ich mir Sorgen wegen der bösen globalen Variablen auto.



  • Du kannst dir auch ein Beispiel an den I/O Funktionen von C nehmen.
    Beim fopen bekommt der Programmierer auch Zugriff auf FILE . (Ob das eine Struktur ist oder etwas anderes bleibt verborgen.)

    Und es gibt einen Haufen Funktionen, denen du diese Information mitgibst.



  • Globale Variablen sind Schrott.
    Daran ändert sich auch nichts, wenn du sie in eine Lib auslagerst.
    Außerdem kannst du bei deiner Variante nur eine Instanz deines Objekts gleichzeitig benutzen, das ist ebenso Schrott.
    Außerdem ist auto ein reserviertes Schlüsselwort in ANSI C. (was die C++ler aber nicht gehindert hat, es aus Faulheit "wiederzuverwenden").
    Und löse dich mal von deinem Fokus auf DLL und so Zeugs.
    Schreib erstmal eine statische Lib, also *.lib statt *.dll.
    Das DLL-Zeugs lenkt bloß ab.
    Um eine gewissen Dynamik in die Schnittstelle solcher Libs zu bringen, setzt man häufig auch Callbacks sein, aber alles läuft nur vernünftig, bei Übergabe des Objektes als Zeiger/Ersatz-this.

    Ich würde aber gerne vermeiden, dass die Funktionen einen Zeiger auf das Objekt ... übergeben bekommen.

    Ohne das wird es nicht gehen.

    /* z.B. auto.h
    das gibst du als Schnittstelle nach außen, und nicht mehr */
    typedef struct {char s[1000];} Anonymous;
    typedef union {Anonymous a;} Auto;
    void set_d(Auto *a,int i);
    void set_e(Auto *a,int i);
    int rechne(Auto *a);
    /************************************************************/
    
    /* z.B. auto.c
    das behältst du für dich; die Variablen und die konkreten Implementierungen
    wenn der Anwender deiner Lib die konkrete interne Struktur nicht kennt,
    kann er auch nicht direkt unter Umgehung deiner Schnittstellen etwas ändern, und darum sorgst du dich ja wohl */
    typedef struct {int d,e,f;} Privat;
    typedef union {Anonymous a;Privat p;} MeinAuto;
    
    void set_d(Auto *a,int i) { MeinAuto *m=(MeinAuto*)a; m->p.d=i; } /* hier mal ein Cast, aber Casts unter unions sind wohldefiniert */
    void set_e(Auto *a,int i) { MeinAuto *m=(MeinAuto*)a; m->p.e=i; }
    int rechne(Auto *a) { MeinAuto *m=(MeinAuto*)a; return m->p.d*m->p.e; }
    /************************************************************/
    
    int main()
    {
      /* deine Lib kann somit beliebig viele Instanzen/Objekte gleichzeitig verarbeiten, die Definition der Objekte geschieht durch den Anwender selbst; */
      Auto x,y; /* du kannst dem Anwender auch die Initialisierung der Objekte vorschreiben, z.B. Auto x={0},y={0} o.ä. */
    
      assert( sizeof(Anonymous)>sizeof(Privat));
    
      set_d(&x,8);
      set_e(&x,9);
    
      set_d(&y,2);
      set_e(&y,3);
    
      if( rechne(&x)>rechne(&y) )
        puts("x ist schwerer als y");
    
      return 0;
    }
    


  • @Wutz
    Danke, für deine umfangreiche Antwort. Meine hat sich etwas verzögert, da ich mich erstmal genauer mit typedefs und unions beschäftigen musste, um deine Code nachzuvollziehen. Das müsste jetzt aber weitestgehend geklappt haben.

    Wutz schrieb:

    Globale Variablen sind Schrott.

    Da gebe ich dir vollkommen recht.

    Wutz schrieb:

    Außerdem kannst du bei deiner Variante nur eine Instanz deines Objekts gleichzeitig benutzen, das ist ebenso Schrott.

    Das ist zwar nicht so richtig nett formuliert aber auch da gebe ich dir Recht.

    Wutz schrieb:

    Und löse dich mal von deinem Fokus auf DLL und so Zeugs.

    Das geht leider nicht, es muss eine DLL werden, die sich dynamisch einbinden lässt.

    Allerdings bin ich mit dieser Union-Lösung auch nicht ganz zufrieden, weil es ja nicht möglich ist, eine Klasse in die Union einzubeziehen. Mein Problem sieht ja etwa folgendermaßen aus:

    Unbekannte Dtrittanbietersoftware <-- Meine Schnittstelle (DLL) --> Mein C++-Code (DLL statisch zur Schnittstelle gelinkt)

    Bei deiner Lösung kann ich, wenn ich nichts übersehen habe nicht so ohne weiteres auf meine Klassen zurückgreifen, die ich in meinem vorhandenen C++-Code erstellt habe. Dort nutze ich auch weitere Bibliotheken, die es mir beispielsweise ermöglichen mit Signalen und Slots zu arbeiten. Das würde ich nur ungerne aufgeben. Daher habe ich mich jetzt für folgende Lösung entschieden, die mir ganz vernünftigt scheint. Ich hoffe, ich habe keine verborgenen Stolpersteine übersehen.

    Der Header meiner DLL sieht jetzt in einem Minimalbeispiel so aus:

    #ifdef DLLEXPORTOPORTO
    #define API extern "C" __declspec(dllexport)
    #else
    #define API extern "C" __declspec(dllimport)
    #endif
    
    // Dieser Teil wird als Schnittstelle zur Verfügung gestellt
    API void* getInstance();
    API void destroyInstance(void* ptr);
    API int instanceCount();
    
    API void setValues(void* ptr,int a, int b);
    API int add(void* ptr);
    
    // Das folgende ist nicht mehr sichtbar
    class Test{
    public:
    	Test();
    	~Test();
    
    	void setA(int in);
    	void setB(int in);
    
    	int add();
    	static int count();
    
    private:
    	static int iCount;
    	int a;
    	int b;
    };
    

    Die zugehörige Implementierung sieht so aus:

    #include "dllheader.h"
    
    int Test::iCount = 0;
    
    // Definitionen der Schnittstelle
    void* getInstance(){
    	Test* test = new Test();
    	return (int*)test;
    }
    
    void destroyInstance(void* ptr){
    	Test* test = static_cast<Test*>(ptr);
    	delete test;
    }
    
    API int instanceCount(){
    	return Test::count();
    }
    
    void setValues(void* ptr, int a, int b){
    	Test* test = static_cast<Test*>(ptr);
    	test->setA(a);
    	test->setB(b);
    }
    
    int add(void* ptr){
    	Test* test = static_cast<Test*>(ptr);
    	return test->add();
    }
    
    // Definitionen der Klasse Test
    Test::Test(){
    	a = 0;
    	b = 0;
    	iCount++;
    }
    
    Test::~Test(){
    	iCount--;
    }
    
    void Test::setA(int in){
    	a = in;
    }
    
    void Test::setB(int in){
    	b = in;
    }
    
    int Test::add(){
    	return a + b;
    }
    
    int Test::count(){
    	return iCount;
    }
    

    Und die ANSI-C konforme Anwendung:

    #include <stdio.h>
    #include <Windows.h>
    
    int main(int argc, char ** argv){
    	// Zum Laden zur Laufzeit
    	typedef void* (*GET_INSTANCE)();
    	typedef void (*DESTROY_INSTANCE)(void*);
    	typedef int (*INSTANCE_COUNT)();
    
    	typedef void (*SET_VALUES)(void*, int, int);
    	typedef int (*ADD)(void*);
    
    	void *pointerA;
    	void *pointerB;
    	int result;
    
    	GET_INSTANCE getInstance;
    	DESTROY_INSTANCE destroyInstance;
    	INSTANCE_COUNT instanceCount;
    	SET_VALUES setValues;
    	ADD add;
    
    	HINSTANCE hInstance = LoadLibraryA("dll_mit_typedef.dll");
    
    	getInstance = (GET_INSTANCE) GetProcAddress((HMODULE)hInstance, "getInstance");
    	destroyInstance = (DESTROY_INSTANCE) GetProcAddress((HMODULE)hInstance, "destroyInstance");
    	instanceCount = (INSTANCE_COUNT) GetProcAddress((HMODULE)hInstance, "instanceCount");
    	setValues = (SET_VALUES) GetProcAddress((HMODULE)hInstance, "setValues");
    	add = (ADD) GetProcAddress((HMODULE)hInstance, "add");
    
    	// Anwendung der Funktionen aus der DLL
    	printf("Es gibt %i Instanzen.\n", (*instanceCount)());
    
    	pointerA = (*getInstance)();
    	(*setValues)(pointerA, 39, 3);
    	result = (*add)(pointerA);
    
    	printf("Es gibt %i Instanzen.\n", (*instanceCount)());
    	printf("Die Antwort lautet: %i.\n", result);
    
    	pointerB = (*getInstance)();
    	(*setValues)(pointerB, 10, 3);
    	result = (*add)(pointerB);
    
    	printf("Es gibt %i Instanzen.\n", (*instanceCount)());
    	printf("Die Antwort lautet: %i.\n", result);
    
    	(*destroyInstance)(pointerA);
    	printf("Es gibt %i Instanzen.\n", (*instanceCount)());
    
    	(*destroyInstance)(pointerB);
    	printf("Es gibt %i Instanzen.\n", (*instanceCount)());
    
    	getchar();
    	return 0;
    }
    

    Die Ausgabe sieht, wie man es erwartet, so aus:

    Es gibt 0 Instanzen.
    Es gibt 1 Instanzen.
    Die Antwort lautet 42.
    Es gibt 2 Instanzen.
    Die Antwort lautet 13.
    Es gibt 1 Instanzen.
    Es gibt 0 Instanzen.
    

    Ich hoffe, dass das eine saubere Lösung darstellt.



  • Deine Variante hat einen ganz anderen Ansatz, du kreierst die Objekte per Einzelaufruf in der Lib, bei mir liefert der aufrufende Kontext (also main) selbst die Objekte (RAII fähig), kann man so machen, muss man aber nicht.
    Du verwendest static in deiner Lib, das ist schlecht. Baue dir eine Factory drumherum (die die Instanzen verwaltet).
    Mit Windows.h verlässt du ANSI C.


Log in to reply