Programmieren mit C++

Allgemeines

Objekt Orientierte Programmierung

Explizites Linken mit Klassen in DLLs

Wie erfolgt das explizite Linken mit Klassen in DLLs und worin besteht der Unterschied zum Linken mit Standard C-Funktionen?

Frage

Das explizite Linken mit einer globalen C- bzw. C++-Funktion, die kein Member ist, ist prinzipiell recht einfach. So läßt sich beispielsweise die Funktion ExportedFn() wie folgt in eine DLL exportieren, wobei ggfs. auch eine DEF-Datei verwendet werden kann.

extern "C" _declspec(dllexport) void ExportedFn(int   param1, 
                                                char* param2);

Die Spezifikation „extern "C"“ ist erforderlich, damit der C++ Compiler keinen dekorierten Namen für die Funktion erzeugt, da dies dazu führen würde, daß die Funktion nicht als ExportedFn() exportiert würde, sondern mit einem mehr zufälligen Namen wie z.B. „??ExportedFn@QAEX“.

Befindet sich die Funktion in einer DLL, nachfolgend beispielhaft DLL1.DLL, kann eine Client-Anwendung die Funktion nach Laden des Moduls und entsprechender Prototypendeklaration wie folgt laden und einsetzen:

HMODULE hMod = LoadLibrary("Dll1.dll");
 
typedef void (*PExportedFn)(int, char*);
 
PExportedFn pfnEF = (PExportedFn)GetProcAdress("ExportedFn");
 
pfnEF(1, "Ein String");

Sollen hingegen eine Reihe von Memberfunktionen einer C++-Klasse exportiert und explizit gelinkt werden, ergeben sich zwei Probleme:

  • Die Namen von C++-Memberfunktionen sind dekorierte Namen, woran auch das explizite Einsetzen von „extern "C"“ nichts ändert.
  • Die C++-Sprachdefinitionen erlauben es nicht, daß ein Zeiger auf eine Memberfunktion auf andere Typen konvertiert wird.

Diese beiden Probleme schränken das explizite Linken mit C++-Klassen in DLLs ein. Allerdings läßt sich die Problematik etwas abschwächen und umschiffen. Dazu bieten sich zwei Vorgehensweisen an:

  • Der Einsatz von virtuellen Funktionen oder vtable entspricht der Vorgehensweise der COM-Schnittstelle
  • Direkte Aufrufe via GetProcAddress()

Als Beispielklasse dient für die nachfolgenden Betrachtungen:

Lösung

class A
{
  private:
    int m_nNum; 
 
  public: 
    A();
    A(int n);
    virtual ~A();
 
    void SetNum(int n);
    int GetNum();
};

Exportieren von Klassen über VTable

Das Exportieren von Klassen über VTable ist der Weg, den die COM-Schnittstelle beschreitet, um Memberfunktionen zu exportieren. Der darunterliegende Gedanke ist der, daß alle virtuellen Funktionen vom Compiler in einer Tabelle zusammengefaßt werden. Dabei wird die Reihenfolge eingehalten, in der die Funktionen in der Deklaration auftreten. Wird ein Objekt der Klasse erzeugt, bilden die ersten vier Bytes des Objekts einen Zeiger auf die Tabelle der virtuellen Funktionen.

Wird die Beispielklasse A auf virtuelle Funktionen wie folgt umgestellt, erzeugt der Compiler eine Tabelle mit den drei virtuellen Funktionen.

Beispielklasse A

class A
{
  private:
    int m_nNum; 
 
  public: 
    A();
    A(int n);
    virtual ~A();
 
    virtual void SetNum(int n);
    virtual int GetNum();
};

Als nächstes muß das Objekt in der DLL erzeugt werden. Da ein explizites Linken erfolgen soll, muß in der DLL eine globale Funktion enthalten sein, die eine Instanz des Objekts über den Operator new() erzeugen kann, und die explizit gelinkt werden kann. Erst durch den Umweg über diese globale Funktion kann auf das Objekt zugegriffen werden.

Da die Klasse zwei Konstruktoren kennt, können zwei Funktionen CreateObjectofA() und CreateObjectofA1(int) implementiert und exportiert werden.

extern "C" __declspec(dllexport) A* CreateObjectofA1()
{
  return new A();
}

Wichtig ist dabei, daß die Instanz des Objekts durch den Operator new erzeugt wird, damit die Clientanwendung später das Objekt sicher wieder über den Operator delete freigeben kann. Auf die erzeugten Objekte kann dann wie folgt zugegriffen werden:

typedef A* (*PFNCreateA1)();
 
PFNCreateA1 pfnCreateA1 = (PFNCreateA1)GetProcAddress(
                                 hMod, TEXT("CreateObjectofA1"));
 
A* a = (pfnCreateA1)();
 
// Objektzugriffe aller Art
 
delete a;

Diese Vorgehensweise ist sehr hilfreich, wenn der Anwender in die Lage versetzt werden soll, PlugIns zu integrieren. Der Nachteil dieser Methode besteht darin, daß der Speicher für die Klasse immer in der DLL allokiert werden muß, so daß insbesondere der Client keine Möglichkeit hat, Speicher auf anderem Wege zu allokieren.

Direkte Aufrufe via GetProcAddress()

Die nächste Vorgehensweise basiert auf dem direkten Adressieren der Funktionen durch GetProcAddress() und dem folgendem Aufruf über Zeiger. Der Trick dabei ist, den von GetProcAddress() returnierten FARPROC-Zeiger in einen C++-Zeiger auf Memberfunktionen zu konvertieren. Dies kann glücklicherweise aufgrund des C++-Features von Templates und Unions einfach erfolgen. Dazu muß lediglich eine Funktion wie das nachfolgende Template definiert werden.

Dest force_cast(Src src)
{
 union
 {
  Dest d;
  Src s;
 } convertor;
convertor.s = Src;
return convertor.d;
}

Diese Funktion erlaubt das Konvertieren beliebiger Typen und ist mächtiger als reinterpret_cast. Wird zum Beispiel ein Zeiger definiert als

typedef void (A::*PSetNum)(int);

kann ein Zeiger fp vom Typ FARPROC auf PSetNum konvertiert werden, indem er einfach verwendet wird.

FARPROC fp;
PSetNum psn = force_cast(fp);

Diese Lösung ist mit reinterpret_cast oder einem C-Style Cast nicht möglich. Nachdem dieser Weg zum Konvertieren eines FARPROC-Zeigers gefunden ist, kann man daran gehen, die Memberfunktionen der C++-Klassen mit benutzerfreundlichen Namen zu exportieren. Dies kann über DEF-Dateien erfolgen.

Der erste Schritt besteht darin, die dekorierten Namen der einzelnen Funktionen, die exportiert werden sollen, zu finden. Möglich wird dies wahlweise über eine MAP-Datei oder durch Sichten des Assembler-Listings. Sind die Namen erst einmal bekannt, können sie durch benutzerfreundliche Namen gemäß der nachfolgenden Syntax in der DEF-Datei ersetzt werden.

Angepaßte Beispielklasse A

EXPORTS
ConstructorOfA1 = ??0A@@QAE@XZ        PRIVATE
ConstructorOfA2 = ??0A@@QAE@H@Z       PRIVATE
SetNumOfA       = ?SetNum@A@@UAEXH@Z  PRIVATE
GetNumOfA       = ?GetNum@A@@UAEHXZ   PRIVATE 
DestructorOfA   = ??1A@@UAE@XZ        PRIVATE

Somit werden die Funktionen unter benutzerfreundlichen und aussagekräftigen Namen exportiert. Die Typendefinitionen der Memberfunktionen ergeben sich gemäß:

Export-Syntax

typedef void (A::*PfnConstructorOfA1)();
typedef void (A::*PfnConstructorOfA2)(int);
typedef void (A::*PfnDestructorOfA)();
typedef void (A::*PfnSetNumOfA)(int);
typedef int  (A::*PfnGetNumOfA)();

Das Erzeugen des Zeigers auf den Konstruktor erfolgt durch:

A* a1 = (A*)_alloca(sizeof(A));
 
PfnConstructorOfA1 pfnConsA = force_cast 
                  (GetProcAddress(hMod, TEXT("ConstructorOfA1")));

Anschließend kann der Konstruktor mit der nachfolgenden Syntax aufgerufen werden:

(a1->*pfnConsA)();

Analog dazu kann SetNumOfA() durch die nachfolgende Anweisung erzeugt ...

PfnSetNumOfA pfnSetNumA = force_cast
                        (GetProcAddress(hMod, TEXT("SetNumOfA")));

...und mit dem Parameter „1“ aufgerufen werden.

(a1->*pfnSetNumA)(1);

Die noch verbleibenden Funktionn ergeben sich zu:

PfnGetNumOfA pfnGetNumA = force_cast
                        (GetProcAddress(hMod, TEXT("GetNumOfA"))); 
 
PfnDestructorOfA pfnDestA =  force_cast
                    (GetProcAddress(hMod, TEXT("DestructorOfA")));

Das interessante ist hierbei, daß die beiden Konstruktoren wie auch der Destruktor explizit aufgerufen werden. Dies ist möglicherweise der einzige Weg, um Klassen-Konstruktoren explizit aufzurufen.

Der andere zu beachtende Aspekt ist der, daß das Objekt Speicher auf dem Stack über den Aufruf von alloca() allokiert hat. Hier könnte ggfs. auch malloca() für eine Speicherreservierung auf dem Heap eingesetzt werden. Dies liegt daran, daß beim Speicherallokieren über new oder auch nur beim Deklarieren eines Objekts vom Typ A der Konstruktor der Klasse A automatisch aufgerufen wird. Dies ist jedoch unerwünscht, da der Konstruktor der KlasseA in der DLL implementiert ist und diese nicht implizit eingelinkt ist.

Der explizite Aufruf des Konstruktors und Destruktors ist nicht unbedingt erforderlich. Man kann stattdessen auch den Konstruktor in der Clientanwendung implementieren als:

A::A()
{
 static PfnConstructorOfA1 pfnConsA1 = 
    force_cast
       (GetProcAddress(ClassLoader::s_hMod, 
                       TEXT("ConstructorOfA1")));
 
 (this->*pfnConsA1)();
}

Der Destruktor ergibt sich analog:

A::~A()
{
 static PfnDestructorOfA pfnDestA = 
    force_cast
       (GetProcAddress(ClassLoader::s_hMod, 
                       TEXT("ConstructorOfA1")));
 
 (this->*pfnDestA)();
}

Die oben gezeigten Implementationen delegieren ihre Aufgabe lediglich an die tatsächliche Funktion in der DLL und erlauben damit gleichzeitig, ein Objekt A in der herkömmlichen Art zu deklarieren und zu verwenden.

Werden hingegen Memberfunktionen wie SetNum() und GetNum() nur über die erzeugten Zeiger aufgerufen, müssen diese nicht erneut implementiert werden.

Auf diese Weise wird es möglich, auch C++-Klassen über DLLs explizit einzulinken. Allerdings ist dazu - wie gezeigt - deutlich mehr Aufwand nötig als bei normalen globalen C-Funktionen. Dafür entschädigt aber wiederum der objektorientierte Ansatz, der mit dem Rest der Anwendung harmoniert.

Typendefinitionen





Sachgebiet


© 2009-2012 by Alojado Publishing. Alle Rechte vorbehalten. Ausgewiesene Marken gehören ihren jeweiligen Eigentümern.
Mit der Benutzung dieser Seite erkennen Sie die Nutzungsbedingungen und die Datenschutzerklärung an. Der Betreiber übernimmt keine Haftung für den Inhalt verlinkter externer Internetseiten.
Seite erzeugt 2012-05-20 02:04:51 von textarchiv.alojado.de