Programmieren mit C++

Borland C++ Builder

Allgemeine Fragen

Delphi Code über COM C++ Builder Anwendungen linken

Lohnt es sich, Delphi-Code über COM einzubinden und wenn ja, wie kann man Delphi Code über COM in C++ Builder Anwendungen einlinken?

Frage

Grundsätzlich kann der C++ Builder alles, was Delphi auch kann. Allerdings gibt es deutlich mehr Delphi Codes und Tools als für C++, so daß es durchaus sinnvoll sein kann, auf bereits verfügbaren Delphi-Code zurückzugreifen.

Der C++ Builder unterstützt das Einbinden von Komponenten, Controls und DLLs, die mit Delphi erzeugt wurden. Um jedoch komplette Projekte in C++ Builder zu integrieren, bedarf es etwas mehr Aufwand. Eine Möglichkeit zum gemeinsamen Verwenden von Code besteht darin, die COM- und OLE-Schnittstelle zu verwenden.

Es gibt grundsätzlich drei Wege, um Delphi COM-Objekte in den C++ Builder einzubinden:

  • OLE Automation oder Dual Interfaces
  • Mit Delphi 3.x ActiveX Controls erzeugen
  • Elementaren COM- oder OLE-Code schreiben, um Delphi einzubinden

Die ersten beiden Methoden werden vom C++ Builder automatisch unterstützt, während letztere deutlich aufwendiger ist und Detailkenntnisse verlangt.

OLE-Automation

Den einfachsten Weg, Delphi-Objekte via COM einzubinden, liefert die OLE Automation, die der C++ Builder vollständig unterstützt.

Automationsobjekte können in zwei Formen angesprochen werden. Der Zugriff erfolgt über das

  • IDispatch-Interface oder
  • über Dual-Interfaces

C++Builder bietet eingebauten Support für IDispatch über die CreateOleObject VCL-Funktion und die Variant Template Klasse. Diese einfache Vorgehensweise sollte die erste Wahl sein, wenn man nicht unbedingt auf die allerletzten Performance-Reserven angewiesen ist.

Für die weiteren Ausführungen sei angenommen, daß ein Delphi IDispatch-Interface vorliegt, das folgende Methoden exportiert:

ITest = interface(IDispatch) 
 
['{1746E520-E2D4-11CF-BD2F-0020AF0E5B81}'] 
 
function  Get_Value: Integer; safecall; 
procedure Set_Value(Value: Integer); safecall; 
function  Get_Name: WideString; safecall; 
procedure Set_Name(const Value: WideString); safecall; 
procedure Prompt(const text: WideString); safecall; 
procedure VarTest(var v1, v2, v3: Variant); safecall; 
property  Value: Integer read Get_Value write Set_Value; 
property  Name: WideString read Get_Name write Set_Name; 

Ferner wird angenommen, daß die DLL, die das Interface enthält in der Registry in Verbindung mit CLSID referenziert und mit der ProgID "TestLib.Test" gerufen wird. Aus C++ Builder heraus kann das Interface dann wie folgt angesprochen und die Methode Prompt aufgerufen werden:

void __fastcall TForm1::Button1Click(TObject *Sender) 
{ 
  Variant V = CreateOleObject("TestLib.Test"); 
  V.OleProcedure("Prompt", "Sammy"); 
}

CreateOleObject() erzeugt zunächst eine Instanz der IDispatch-Klasse, deren Referenz in einem Variant-Record abgelegt wird. Anschließend kann die Variant-Methode OleProcedure() zum Aufrufen einzelner Funktionen verwendet werden.

Werden mehrere Parameter benötigt, so können ggfs. auch mehrere Variant-Records übergeben werden:

void __fastcall TForm1::Button1Click(TObject *Sender) 
{ 
  Variant V1, V2, V3;
  Variant V = CreateOleObject("TestLib.Test");
  V1 = 5; 
  V2 = "Sam"; 
  V3 = V; 
  V.OleProcedure("VarTest", V1, V2, V3); 
}

Delphi-Entwickler sollten den gezeigten Code mit Vorsicht betrachten. Variant ist eine C++ Klasse, die in SYSDEFS.H deklariert ist und kein simpler generischer Type wie die gleichnamige Delphi-Struktur.

Lösung

Sollen Dual Interfaces zum Einsatz kommen, dann erhält man einen sehr schnellen Zugriff auf Automationsobjekte, die in einer DLL untergebracht sind. Auch lokale Server, also Automationsobjekte, die in einer ausführbaren Datei untergebracht sind, lassen sich über ein duales Interface schneller ansprechen. Allerdings macht der Overhead des lokalen Servers den größten Teil der Performance des Dual Interface wieder zunichte.

Da der C++ Builder keine eingebaute Dual Interface Schnittstelle beinhaltet, müßte diese ggfs. selbst implementiert werden. Der Aufwand hierfür ist im Vergleich zum Nutzen jedoch recht hoch.

Mit Delphi 3 erzeugte ActiveX Controls einbinden

Der C++ Builder hat einen ActiveX-Support integriert. Diese Controls können installiert und wie Native Komponenten verwendet werden.

Dual Interfaces

Die Installation einer ActiveX-Control in C++ Builder erfolgt analog zur Installation einer Komponente über den Befehl „Komponente installieren“, jedoch ist im anschließenden Dialog die Schaltfläche „OCX“ zu betätigen.

Die neue Control wird auf der OCX-Seite der Komponenten-Palette plaziert. Gleichzeitig wird der Interface-Source automatisch generiert im LIB-Verzeichnis abgelegt.

ActiveX-Control installieren

Wer die oben gezeigte automatische Methode nicht einsetzen möchte, kann natürlich auch mit Hilfe der C++ Sprache einen eigenen OLE-Container bilden, was naturgemäß keine einfache Aufgabe ist – aber mit C++ ist grundsätzlich alles möglich. Obendrein enthält BC++ Tools zum Erzeugen von OLE-Container.

Delphi über elementaren COM- oder OLE-Code einbinden

Das Einbinden von Delphi über elementaren COM- oder OLE-Code ist zweifelsohne die schwierigste aller hier vorgestellten Methoden – allerdings auch die flexibelste. Zum einen ist Delphi 3 ein sehr mächtiges Werkzeug, mit dem COM- und OLE-Objekte dank der RAD-Tools sehr schnell entwickelt werden können. Delphi 3 unterstützt Automationsobjekte, Dual Interface, ActiveX und alle Arten von COM-Objekten. Damit wird es möglich, bereits bestehenden Delphi-Code anderer Projekte einzubinden.

Der C++Builder hat jedoch keinen vorgefertigten Support für COM-Objekte, wie sie in Delphi 3 zu finden sind. Dafür bietet C++ Builder aber Zugriff auf allen OLE-Code von BC++ 5.0, dem Windows SDK und alle verfügbaren Module und Bibliotheken.

Nachfolgend ein Beispiel für eine Definition eines einfachen COM-Objekts in Delphi:

const 
  CLSID_ITable: TGUID = ( D1:$58BDE140;
                          D2:$88B9;
                          D3:$11CF;
                          D4:($BA,$F3,$00,$80,$C7,$51,$52,$8B) ); 
 
type 
  ITable = class(IUnknown) 
  private 
    FRefCount: LongInt; 
    FObjectDestroyed: TObjectDestroyed; 
    Table: TTable; 
  public 
    constructor Create(ObjectDestroyed: TObjectDestroyed); 
    destructor Destroy; override; 
    function QueryInterface(const iid: TIID; var obj): HResult; 
        override; stdcall; 
    function AddRef: Longint; override; stdcall; 
    function Release: Longint; override; stdcall; 
 
  { interface } 
    procedure Open; virtual; stdcall; 
    procedure Close; virtual; stdcall; 
    procedure SetDatabaseName(const Name: PChar); virtual; 
        stdcall; 
    procedure SetTableName(const Name: PChar); virtual; stdcall; 
    procedure GetStrField(FieldName: PChar; Value: PChar); 
        virtual; stdcall; 
    procedure GetIntField(FieldName: PChar; var Value: Integer); 
        virtual; stdcall; 
    procedure GetClassName(Value: PChar); virtual; stdcall; 
    procedure Next; virtual; stdcall; 
    procedure Prior; virtual; stdcall; 
    procedure First; virtual; stdcall; 
    procedure Last; virtual; stdcall; 
    function EOF: Bool; virtual; stdcall; 
  end;

In einem C++ Projekt würde das gleiche Interface wie folgt deklariert:

class IDBClass : public IUnknown 
{ 
  public: 
    STDMETHOD(QueryInterface) (THIS_ REFIID, LPVOID*) PURE; 
    STDMETHOD_(ULONG,AddRef) (THIS) PURE; 
    STDMETHOD_(ULONG,Release) (THIS) PURE; 
 
// Interface 
    STDMETHOD_(VOID, Open) (THIS) PURE; 
    STDMETHOD_(VOID, Close) (THIS) PURE; 
    STDMETHOD_(VOID, SetDatabaseName) (THIS_ LPSTR Name) PURE; 
    STDMETHOD_(VOID, SetTableName) (THIS_ LPSTR Name) PURE; 
    STDMETHOD_(VOID, GetStrField) (THIS_ LPSTR FieldName, LPSTR 
                                   Value) PURE; 
    STDMETHOD_(VOID, GetIntField) (THIS_ LPSTR FieldName, 
                                   int * Value) PURE; 
    STDMETHOD_(VOID, GetClassName) (THIS_ LPSTR Name) PURE; 
    STDMETHOD_(VOID, Next) (THIS) PURE; 
    STDMETHOD_(VOID, Prior) (THIS) PURE; 
    STDMETHOD_(VOID, First) (THIS) PURE; 
    STDMETHOD_(VOID, Last) (THIS) PURE; 
    STDMETHOD_(BOOL, EOF) (THIS) PURE; 
};

Anhand dieser Definition ist der folgende Code notwendig, um aus C++ Code heraus die Delphi-Objekte zu nutzen:

#pragma argsused 
 
void Window1_OnLButtonDown(HWND hwnd, BOOL fDoubleClick, int x, 
                           int y, UINT keyFlags) 
{ 
  char CR[2] ="\r"; 
  HRESULT hr; 
  PIDBClass P; 
  LPSTR S, Temp;
 
  CoInitialize(NULL); 
  hr = CoCreateInstance(CLSID_IDBClass,NULL,CLSCTX_INPROC_SERVER, 
  
  IID_IUnknown, (VOID**) &P); 
if (SUCCEEDED(hr)) { 
  S    = (char *) malloc(1000); 
  Temp = (LPSTR)  malloc(100); 
 
  P->SetDatabaseName("DBDEMOS"); 
  P->SetTableName("COUNTRY"); 
  P->Open(); 
 
  strcpy(S, "Länder und Hauptstädte: "); 
  strcat(S, CR); 
  strcat(S, CR); 
 
  while (!P->EOF()) { 
    P->GetStrField("NAME", Temp); 
   
    strcat(S, Temp); 
    strcat(S, ": "); 
 
    P->GetStrField("CAPITAL", Temp); 
 
    strcat(S, Temp); 
    strcat(S, CR); 
    P->Next(); 
  } 
 
  MessageBox(hwnd, S, „Ländertabelle", 
             MB_OK); 
 
  P->Release(); 
 
  free(S); 
  free(Temp); 
  } 
}

Das gezeigte Beispiel greift auf die Ländertabellen in DBDEMOS zurück und zeigt, daß die Vorgehensweise nicht allzu kompliziert ist, wenn auch deutlich aufwendiger, als die bisher gezeigten Methoden.

Zugriff auf Standard-DLLs

Werden DLLs verwendet, um auf von Delphi erzeugten Code aus C++ heraus zuzugreifen, so erfolgt dies wie jeder andere Zugriff auf Funktionen in DLLs. Hier gilt grundsätzlich: Kann die DLL unter Delphi problemlos aus einem Formular heraus aufgerufen werden, dann funktioniert das gleiche unter C++ Builder ebenso.

OLE-Container

Auf Daten, die innerhalb der Delphi-DLL deklariert wurden, hat man von C++ aus – wie übrigens auch von Delphi aus – keinen Zugriff, ohne den Umweg über einen Funktionsaufruf zu gehen, der den Wert einer Variablen bzw. Struktur liefert oder setzt. Dies ist eine Limitation für alle Sprachen und im DLL-Konzept begründet.

Zugriffs-Limitation

DLLs sind insbesondere dann sinnvoll, wenn ein größeres Projekt in mehrere kleinere und leichter wartbare Module aufgeteilt werden soll. Hierbei macht der Zugriff auf Delphi-DLLs dann Sinn, wenn ausgetestete Delphi-Codes zur Verfügung stehen. Diese können in aller Regel auch dann für C++ verfügbar gemacht werden, wenn sie in Delphi als reine Unit eingebunden wurden. Unabhängig davon, daß der C++ Builder Compiler auch Pascal-Quellen kompilieren kann, stellt es normalerweise kein Problem dar, sauber implementierte Funktionen und Prozeduren einer Delphi-Unit für eine DLL zu exportieren.

Tabelle der virtuellen Methoden mappen

Soll eine C++ Builder Anwendung auf Delphi Objekte zugreifen, die innerhalb einer DLL implementiert wurden, dann wird es etwas schwieriger, da diese Objekte normalerweise für C++ Projekte nicht erreichbar sind.

Wie immer gibt es jedoch auch hier ein Hintertürchen: Delphi verwaltet alle Objekte und deren Member über eine Vir-tual Method Table (VMT), die in C++ über ein virtuelles abstraktes („PURE“) Objekt abgebildet werden kann.

Die in Delphi deklarierte Klasse TMyObject, die hier zu Beispielzwecken nur als kleiner Rumpf ausgeführt ist, lautet:

TMyObject = class 
  function AddOne(i: Integer): Integer; virtual; stdcall; 
  function AddTwo(i: Integer): Integer; virtual; stdcall; 
end;

Eine virtuelle abstrakte Version des gleichen Objekts wird in C++ wie folgt deklariert:

class __declspec(pascalimplementation) TMyObject: public TObject 
{ 
public: 
  virtual _stdcall int AddOne(int i) = 0; 
  virtual _stdcall int AddTwo(int i) = 0; 
};

Zur Abbildung der VMT des Delphi-Objekts auf das C++ Objekt wird ein Zeiger auf das Objekt in der DLL benötigt. Ein Weg, dies zu erreichen ist die Funktion CreateObject, die speziell für diesen Zweck implementiert ist:

function CreateObject(ID: Integer; var Obj)
                     : Boolean; 
var 
  M: TMyObject; 
begin 
  if ID = ID_MyObject 
    then begin 
           M := TMyObject.Create; 
           Result := True 
         end 
    else begin 
           M := nil; 
           Result := False 
         end; 
 
  Pointer(Obj) := M; 
end;
 
exports 
  CreateObject name 'CreateObject';

CreateObject() ist in der DLL implementiert und hat daher Zugriff auf die Objekte der DLL. Beim Aufruf erzeugt die Methode dann das gewünschte Objekt und liefert einen Zeiger darauf zurück.

Die Funktion kann aus einem C++ Builder Projekt mit dem folgenden Code aufgerufen werden:

typedef Boolean (_stdcall *LPCreateObject)(int ID, void *obj);

Ein vollständiges Beispiel zum Aufruf einer Methode des Delphi-Objekts zeigt der nachfolgende Code:

void __fastcall TForm1::Button1Click(TObject *Sender) 
{ 
  TMyObject *MyObject; 
  LPCreateObject CreateObject;
 
  HANDLE hlib = LoadLibrary("OBJECTLIB.DLL"); 
 
  CreateObject = (LPCreateObject)GetProcAddress(hlib, 
                                                "CreateObject"); 
 
  if (CreateObject(1, &MyObject))  {
    int i = MyObject->AddOne(1); 
    ShowMessage((AnsiString)i); 
  } 
 
  FreeLibrary(hlib); 
}

Die Methode Button1Click() deklariert zunächst einen Zeiger auf eine Funktion mit der gleichen Parameterstruktur wie die CreateObject-Funktion in der DLL. Anschließend wird die DLL über LoadLibrary() in den Speicher geladen und die Adresse der Funktion über GetProcAddress() bestimmt.

Hat man die Adresse der Funktion bekommen, kann man anschließend die Funktion CreateObject() aufrufen und kommt dadurch in den Besitz eines Zeigers auf das Delphi-Objekt. Mit Hilfe dieses Zeigers kann nun eine Methode des Delphi-Objekts über den Objekt-Zeiger aufgerufen werden.

Warum Delphi-DLLs?

Die gezeigte Vorgehensweise basiert darauf, daß die Methoden der Delphi-Objekte virtuell deklariert sind, da nur virtuelle Methoden in der VMT abgelegt werden, bzw. ohne virtuelle Methoden erst gar keine VMT angelegt wird und der beschrittene Weg zur Sackgasse wird.

Daneben kann das Delphi-Objekt natürlich weiterhin auch nicht virtuelle Methoden haben. Aber die Methoden, auf die von C++ Builder zugegriffen werden soll, müssen virtuell sein.

Bedingt durch die Art des Zugriffs ist auch die Reihenfolge der Methoden wichtig. Sowohl in dem Delphi-Objekt wie auch in der korrespondierenden C++ Deklaration muß die Reihenfolge der virtuellen Methoden vollständig übereinstimmen, da ansonsten indexgesteuerte Zugriffe fehlschlagen.

Alles in allem ist der Zugriff über die VMT eine haarige Angelegenheit, die bei den kleinsten Flüchtigkeitsfehlern – wie beispielsweise nicht exakt übereinstimmende Deklarationen oder Reihenfolgen – zu schwer lokalisierbaren Fehlern führt. Wann immer möglich, sollte man daher zu anderen Formen der Einbindung von Delphi-Objekten in C++ Builder Projek-te greifen. Diese sind mit Sicherheit stabiler und zukunftssicherer!

Virtuelle Methoden





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 01:36:46 von textarchiv.alojado.de