.NET meets COM

20.04.2004 von Thomas Wölfer
Nicht immer kann man sofort seine Projekte komplett auf .NET umstellen. Bei der Zusammenarbeit von neuem und altem Code machen jedoch einige Stolperfallen das Leben schwer. Wir zeigen, wie Sie diese Klippen umschiffen.

Ohne "Managed Code" kommt in Zukunft kein Programmierer mehr aus. Ganz gleich, ob unter .NET oder unter Java erzeugt: Immer mehr Module, Bibliotheken und Anwendungsprogramme sind "managed".

Allerdings gibt es immer noch eine Vielzahl von "altem" Code, den man nicht mehr aktualisieren will oder kann - etwa weil man eine ActiveX-Komponente zugekauft hat und der Hersteller keine .NET-Variante anbietet. Darum ist es notwendig, dass man auch alten Code oder Bibliotheken weiterverwenden kann - zumindest mittelfristig. In diesem Beitrag erfahren Sie, wie Sie mit .NET auf "unmanaged" Code zugreifen und umgekehrt.

Der Zugriff mit .NET auf vorhandenen Win32-Code ist am einfachsten, wenn der vorliegende Code als ActiveX-Control oder COM-Objekt vorliegt, denn dafür bietet Visual Studio einen sehr guten Support. Im Prinzip kann man ActiveX-Controls in einem Visual-Studio-Projekt derart nutzen, als wären es .NET-Objekte. Damit das so einfach funktioniert, erzeugt Visual Studio im Hintergrund den passenden Wrapper-Code, der mit dem ActiveX-Control kommuniziert und dem .NET-Programm vorgaukelt, dass es sich um ein .NET-Control handelt.

Damit Sie das benötigte Vorgehen für die Nutzung von ActiveX aus einer .NET-Anwendung heraus besser nachvollziehen können, gibt es begleitend zum Artikel ein Beispielprojekt mit einem ActiveX-Control in MFC und einem C#-Projekt, das dieses Control verwendet.

ActiveX-Controls benutzen

Beim ActiveX-Control handelt es sich um ein ganz einfaches Control mit nur wenigen Möglichkeiten - schließlich soll es ja nur für eine Demonstration eingesetzt werden. Das Control zeichnet einen Kreis in ein Viereck. Normalerweise sind der Hintergrund und die Außenfläche des Kreises weiß. Das Control exportiert eine einzelne Methode mit dem Namen ToggleRed(). Wird die Methode aufgerufen, setzt sie ein Flag im Control und invalidiert das Control:

void CaxctrlCtrl::ToggleRed(void)
{
AFX_MANAGE_STATE(AfxGetStaticModuleState());
isRed = ! isRed;
Invalidate();
}

Das führt dazu, dass das Control neu gezeichnet wird - diesmal aber in roter Farbe für die Außenfläche des Kreises:

void CaxctrlCtrl::OnDraw(
CDC* pdc, const CRect& rcBounds, const CRect& rcInvalid)
{
if (!pdc)
return;

if( isRed)
{
CBrush b( RGB(255,0,0));
pdc->FillRect(rcBounds, &b);
pdc->Ellipse(rcBounds);
}
else
{
pdc->FillRect(rcBounds, CBrush::FromHandle((HBRUSH)GetStockObject(WHITE_BRUSH)));
pdc->Ellipse(rcBounds);

}
}

Nutzung des Control von .NET

Benutzt werden soll das Control in einer Anwendung in Managed Code, beispielsweise einer C#-Windows.Forms-Anwendung. Um das Control einzusetzen, brauchen Sie nicht viel zu tun. Zunächst laden Sie das Form in den Forms-Editor und klicken mit der rechten Maustaste in die Toolbox. Im Objektmenü finden Sie den Befehl "Add/Remove Items". Über diesen Befehl erreichen Sie den Dialog zum Konfigurieren der Toolbox.

Der setzt sich im Wesentlichen aus zwei Reitern zusammen. Der eine Reiter enthält .NET-, der andere COM-Objekte. Wenn das ActiveX-Beispielobjekt auf dem Rechner installiert ist, finden Sie es in der Liste unter dem Titel "AxCtrl". Das Kontrollelement ist mit einer Checkbox ausgestattet. Wenn Sie die Checkbox anklicken und den Dialog schließen, taucht das Element mit dem Titel "OCX axctrl Control" auf.

Außerdem finden Sie in den Referenzen des Anwendungsprojekts zwei neue Einträge: Die kümmern sich im Wesentlichen darum, dass Sie das Control nutzen können. Der komplette benötigte "Glue"-Code (Glue, da .NET und COM zusammengeklebt werden) für die Nutzung des Kontrollelements wird von Visual Studio erzeugt und in einer DLL abgelegt, die sich im Verzeichnis der Anwendung wiederfindet.

ActiveX-Control in der Toolbox

Wenn Sie nun eine Instanz des Controls aus der Toolbox auf das Form ziehen, bettet der Forms-Editor eine passende Objekt-Variable in das Form ein. Über diese Variable können Sie das Kontrollelement vollständig benutzen.

Um das zu demonstrieren, noch ein kurzes Beispiel, wie Sie die vom Control exportierte Methode ToggleRed() nutzen können. Wie Sie sehen werden, geht das ganz so, als wäre es ein .NET-Control.

Für das Beispiel brauchen Sie lediglich noch eine Schaltfläche auf dem Form, die Sie mit einem Event-Handler für das Event Click ausstatten. Das geht am einfachsten, indem Sie auf den Button doppelklicken.

Im Click-Handler rufen Sie einfach die Methode ToggleRed() auf:

private void button1_Click(object sender, System.EventArgs e)
{
this.axaxctrl1.ToggleRed();
}

Wenn Sie das Projekt nun übersetzen und starten, können Sie die Farbe des Controls bei laufender Anwendung per Knopfdruck umschalten.

Datenaustausch mit COM

Sie können natürlich nicht nur fertige Controls nutzen, sondern auch mit COM-Objekten Daten austauschen. Dazu das nächste Beispiel.

Beim Beispielprojekt mit COM und ATL finden Sie ein COM-Objekt mit einem einfachen Interface und einer passenden Implementierung. Das Objekt bietet eine Methode zum Addieren von zwei Double-Werten.

interface Iadder : IDispatch{
[id(1), helpstring("method Add")] HRESULT Add([in] DOUBLE a, [in] DOUBLE b, [out] DOUBLE* c);
};

STDMETHODIMP Cadder::Add(DOUBLE a, DOUBLE b, DOUBLE* c)
{
AFX_MANAGE_STATE(AfxGetStaticModuleState());
*c = a + b;
return S_OK;
}

Die beiden Zahlen werden einfach addiert und das Resultat zurückgeliefert. Mit anderen Worten: Das Objekt kann zum Addieren von Zahlen verwendet werden. Das ist zwar kein besonders umfangreiches oder sinnvolles Objekt, aber zur Demonstration reicht es völlig aus.

Um das Objekt nutzen zu können, müssen Sie natürlich zunächst das COM-Objekt-Projekt übersetzen. Im Zuge des Build-Vorgangs wird das Objekt registriert, worauf es sich auf Ihrem Rechner genauso wie alle anderen COM-Objekte darstellt.

Danach wechseln Sie in das Projekt mit Mangaged Code. Im Beispiel ist das Form des C#-Projekts mit zwei Edit-Controls, einem Button und einem Label ausgestattet.

In den Edit-Controls können Sie jeweils eine Zahl eingeben, ein Klick auf die Schaltfläche erzeugt dann das COM-Objekt, addiert die Werte und trägt das Resultat im Label ein.

Damit das COM-Objekt benutzt werden kann, ist jedoch zuvor eine Referenz auf das Objekt zum Projekt hinzuzufügen. Das erledigen Sie einfach über den Referenzen-Ordner des Anwendungsprojekts. Der "Add-Reference"-Dialog bietet drei Reiter: jeweils einen für Referenzen auf .NET-Objekte, auf COM-Objekte und auf eigene Projekte.

COM-Objekte direkt verwenden

Um eine Referenz aus das COM-Objekt hinzuzufügen, benötigen Sie natürlich den Reiter für COM-Objekte. Das Beispiel-Objekt finden Sie in der Liste der Objekte unter dem Namen "tc_demoLib". Sobald Sie die Referenz in das Projekt eingefügt haben, stehen die Interfaces und Klassen zur Verfügung.

Allerdings gibt es nicht sonderlich viele Interfaces beziehungsweise Klassen - das Demoprojekt enthält im Wesentlichen nur eine Klasse: tc_demoLib.adderClass.

Um eine Instanz dieser Klasse zu verwenden, nutzen Sie einfach das Schlüsselwort new. Interessant ist dabei, dass es sich beim eigentlichen COM-Objekt ja nicht um ein Managed-Objekt handelt: Das ist aber kein Problem, denn der vom Visual Studio erzeugte Wrapper kümmert sich automatisch darum, das der verwendete Speicher später wieder freigegeben wird - und der Wrapper selbst ist natürlich "managed", so dass Sie den nicht dediziert freigeben müssen.

Letzten Endes können Sie die Addition dann unter Verwendung des COM-Objekts ganz genau so durchführen, wie man das erwarten würde: So als wäre es ein .NET-Objekt:

private void button1_Click(object sender, System.EventArgs e)
{
tc_demoLib.adderClass o = new tc_demoLib.adderClass();
double x;
o.Add(
double.Parse( this.textBox1.Text),
double.Parse( this.textBox2.Text),
out x);
this.label1.Text = x.ToString();
}

Win32 aufrufen: Platform Invoke

COM- und ActiveX-Objekte sind meist einfach zu verwenden - aber manchmal braucht man eben keine Objekte, sondern einfach nur C-Funktionen aus DLLs. Das kann zum Beispiel der Fall sein, wenn Sie eine Funktion aus der Windows-API verwenden möchten, für die es keine .NET-Variante gibt.

In diesem Fall kommt P/Invoke (für Platform-Invoke) zum Zuge. Bei P/Invoke definieren Sie einen Prototyp, mit dem Sie .NET mitteilen, wie die aufzurufende Funktion aussieht, und die Laufzeitbibliothek übernimmt dann den eigentlichen Aufruf für Sie. Eine Demonstration dazu finden Sie im nächsten Beispielprojekt.

Das Beispielprojekt enthält den Quellcode für eine native C++-DLL und ein weiteres Anwendungsprogramm in C#. Das Anwendungsprogramm soll nun eine Funktion aus der nativen DLL aufrufen.

Zur Demonstration von P/Invoke enhält die DLL die Funktion GoBeep(). Diese Funktion ist im Wesentlichen ein Wrapper für die Funktion Beep() aus Kernel32, über die Töne über den PC-Lautsprecher ausgegeben werden können:

DLL_DEMO_API BOOL GoBeep( DWORD dwFreq, DWORD dwDuration)
{
return Beep( dwFreq, dwDuration);
}

Das Makro DLL_DEMO_API ist dabei nur eine einfache Variante von __declspec( dllexport) und legt fest, dass die Funktion aus der DLL exportiert wird.

Um diese native Funktion von C# aus aufrufen zu können, müssen Sie in C# einen Prototyp für die Funktion anlegen. Dieser Prototyp definiert die Typen des Rückgabewertes und der Parameter, den Namen der nativen Funktion und die DLL, in der sich diese Funktion befindet.

C - Windows Typen mappen

Bei der Definition der Datentypen gibt es jedoch ein kleines Problem: Funktionen, die aus DLLs exportiert werden - also beispielsweise alle Windows-APIs - sind fast immer in C geschrieben und orientieren sich an den Makros und typedefs der Windows-API. So gibt es jede Menge Typen wie BOOL, DWORD, UINT und so weiter. All diese Typen sind entweder typedefs oder Makros, die auf Basis der eigentlichen C-Typen arbeiten. Diese sind in der CLR (Common Language Runtime) allerdings unbekannt. C# seinerseits kennt aber nur die Datentypen der CLR - also etwa System.Int32. Daher ist es notwendig, den Prototyp der nativen Funktion im managed Code mit CLR-Datentypen zu erstellen, die zu den in C verwendeten Datentypen passen. Dazu muss man sich zunächst den nativen Prototyp der DLL-Funktion ansehen. Im Beispielfall sieht der wie folgt aus:

BOOL GoBeep( DWORD dwFreq, DWORD dwDuration);

Zunächst zum Rückgabewert: Der ist vom Typ BOOL, unter Windows also in Wirklichkeit ein 32-Bit-Integer. Der passende CLR-Typ wäre also System.Int32. Die CLR bietet aber auch einen besseren Typ an: System.Boolean. Da sich P/Invoke auch um die richtige Umwandlung der Daten kümmert, ist System.Boolean aus Lesbarkeitsgründen die bessere Wahl für den Funktions-Prototyp im managed Code.

Ein DWORD von Windows ist ein 32 Bit breiter, vorzeichenloser Integer-Wert. Das lässt sich über die CLR mit dem Typ System.UInt32 ausdrücken. Damit ist der Anfang für den Prototypen gemacht:

System.Boolean GoBeep( System.UInt32 frequency, System.UInt32 duration);

Die Typen wären damit geklärt - es fehlen aber noch ein paar andere Informationen. Zum einem muss der Prototyp darüber Auskunft geben, dass sich die Funktion an einer anderen Stelle befindet: Das geschieht mit dem Keyword "extern". Des Weiteren muss die Funktion als "static" markiert sein: Die Funktion ist schließlich nicht Teil eines Objekts oder einer Objekt-Instanz.

static extern System.Boolean GoBeep( System.UInt32 frequency, System.UInt32 duration);

Das reicht jedoch immer noch nicht aus: Die Laufzeit-Bibliothek muss zudem noch wissen, in welcher DLL denn diese Funktion zu finden ist. Diese Information stellen Sie mit dem Attribut DLLImport() zur Verfügung. Das Attribut hat eine ganze Reihe meist optionaler Parameter, der wichtigste Parameter ist aber nicht optional: Den Namen der DLL, die die Funktion enthält.

Im Beispielprojekt hat die DLL den Namen dll_demo.dll. Der komplette Prototyp für die Funktion sieht also wie folgt aus:

[DllImport("dll_demo.dll")]
static extern System.Boolean GoBeep( System.UInt32 frequency, System.UInt32 duration);

Eigene Wrapper-Klasse

In C# müssen Funktionen immer innerhalb einer Klasse untergebracht sein. Das tun Sie am besten auch mit Funktionen, die Sie per Interop erreichen: Schreiben Sie dafür eine Wrapper-Klasse in managed Code. Im Fall von MessageBeep könnte dieser Wrapper etwa wie folgt aussehen:

public sealed class Pieper
{
public static void Piep( UInt32 f, UInt32 d)
{
if( ! GoBeep( f, d))
{
Int32 err = Marshal.GetLastWin32Error();
throw new Win32Exception( err);
}
}

[DllImport("dll_demo.dll", SetLastError=true)]
static extern Boolean GoBeep( UInt32 f, UInt32 d);

private Pieper() {}
}

Um nun also im C#-Anwendungsprogramm den Lautsprecher ertönen zu lassen, können Sie dann die Klasse Pieper verwenden:

private void button1_Click(object sender, System.EventArgs e)
{
Pieper.Piep( 300, 400);
}

Tabelle: Mapping

Mapping der wichtigsten Datentypen

Win32-Typ

CLR-Typ

Breite

char, INT8, SBYTE, CHAR

System.SByte

8 bit signed integer

Short, short int, INT16, SHORT

System.Int16

16 bit signed integer

Int, long, INT32, INT

System.Int32

32 bit signed integer

BOOL

System.Boolean

32 bit signed integer

INT64, LONGLONG

System.Int64

64 bit signed integer

UCHAR, UINT8, BYTE

System.Byte

8 bit unsigned integer

UINT16, USHORT, WORD, ATOM

System.UInt16

16 bit unsigned integer

UINT32, ULONG32 DWORD, ULONG, UINT

System.UInt32

32 bit unsigned integer

mappen müssen.

Aufwendig: Managed Extensions für C++

Eine andere Methode, um vorhandenen nativen Code weiter nutzen zu können, ist die Verwendung von "Managed Extensions für C++". Dabei schreiben Sie einen Wrapper in C++, der mit Hilfe der Erweiterungen zu managed Code wird. Dieser Wrapper kann direkt den betroffenen nativen C++-Code verwenden und seinerseits gleichzeitig von normalem managed Code aufgerufen werden.

Die "Managed Extensions für C++" sind eigentlich einen eigenen Beitrag wert, denn Sie bieten wirklich umfangreiche Möglichkeiten. An dieser Stelle soll daher nur ein einfaches Beispiel für eine Wrapper-Klasse einer bestehenden C++ Klasse erläutert werden.

Um das zu demonstrieren, benötigen Sie zunächst einmal eine native C++-Klasse. Im Beispielprojekt ist das die Klasse "NativeDemo" aus dem gleichnamigen Projekt.

Dem Beispielcharakter entsprechend handelt es sich bei der Demo-Klasse um eine sehr einfache Klasse mit einem Konstruktor und gerade mal zwei Methoden.

class NativeDemo
{
private:
int m_calls;

public:
NativeDemo();
void Call();
int GetCalls();
};

Die Klasse hat ein privates Daten-Member m_calls, das die Anzahl der Aufrufe von Call() speichert. Mit der Methode GetCalls() können Sie die bisherige Anzahl an Aufrufen ermitteln.

Beim "NativeDemo Projekt" handelt es sich um ein Projekt, das eine dynamische Linkbibliothek (und natürlich eine statische Import-Library) erzeugt. So wird das auch in der Praxis aussehen, denn Ihre eigene Klassenbibliothek wird ebenfalls entweder als statische (eher selten) oder als dynamische Bibliothek vorliegen.

Um das Beispiel ein wenig übersichtlicher zu gestalten, enthält der Beispielcode Namespaces. So liegt die native Klasse im Namespace TechChannel.Native.

Im ursprünglichen nativen Code würde einfach die Bibliothek mit der Anwendung mitgelinkt. Ferner würde die Anwendung das Headerfile der Bibliothek an passender Stelle inkludieren und könnte auf diese Art die Funktionalität der NativeDemo-Klasse verwenden.

Wrapper-Klasse mit Managed Extensions für C++

Um die gleiche Klasse nun auch mit managed Code verwenden zu können, brauchen Sie eine Wrapper-Klasse, die mit den "Managed Extensions für C++" geschrieben wurde. Dazu gibt es ein weiteres Projekt: ManagedDemo. In diesem Projekt wird ebenfalls eine Klasse definiert. Sie heißt ManagedDemo und liegt im Namespace TechChannel.Managed. (Um ein Projekt mit Managed Extensions für C++ anzulegen, wählen Sie im Projekt-Wizard den Projekt-Typ Class Library.NET aus. Zumindest dann, wenn Sie auch eine Klassenbibliothek erzeugen wollen.)

Nun muss man sich zunächst an eine Eigenart des Projekt-Wizards von Visual Studio gewöhnen: Beim Erzeugen von Projekten mit managed Code C++ wandert der komplette Quellcode nämlich von Haus aus immer in die Headerfiles. Wenn Sie also den Quellcode für die ManagedDemo-Klasse suchen und in ManagedDemo.cpp nicht finden: Nicht verzweifeln - der Code findet sich in ManagedDemo.h.

public __gc class ManagedDemo
{
private:
TechChannel::Native::NativeDemo* n;
public:
ManagedDemo()
{
n = new TechChannel::Native::NativeDemo();
}

~ManagedDemo()
{
delete n;
}

void Call()
{
n->Call();
}

int GetCalls()
{
return n->GetCalls();
}
};

Zunächst fällt bei der Klasse das neue Schlüsselwort __gc auf, das für "Garbage Collected" steht. Das bedeutet: die Klasse wird vom Garbage Collector der CLR verwaltet, was wiederum bedeutet, dass es sich bei der Klasse um managed Code handelt.

Im Konstruktor wird eine Instanz der nativen Klasse erzeugt. Diese Instanz unterliegt nicht dem Garbage Collector und muss darum auch freigegeben werden. Das erledigt der Destruktor von ManagedDemo().

Darüber hinaus bietet die ManagedDemo-Klasse nicht viel Funktionalität: Sie hat einfach für jede öffentliche Funktion aus NativeDemo eine Wrapper-Funktion, die den Aufruf und eventuelle Parameter einfach an die native Instanz weitergibt und die Ergebnisse der Funktionen zurückliefert.

Nun muss die native Bibliothek noch mit der nativen Import-Library gelinkt werden. Die managed DLL enthält alle benötigten Informationen über die native Klasse statisch eingebunden, die Implementierung dagegen bleibt in der separaten DLL.

Problem bei mixed-mode-DLLs

Wenn Sie nicht einfach das Beispielprojekt angesehen, sondern ein eigenes nachgebaut haben, bekommen Sie nun das erste Problem. Beim Erzeugen der DLL meckert der Linker die verschiedensten Punkte an: Am bemerkenswertesten ist die Tatsache, dass new und delete fehlen.

Hier liegt ein spezielles Problem beim Linken von mixed-mode-DLLS vor. Das sind DLLs, die sowohl managed als auch nativen Code enthalten. Um Ihnen die lange Suche in den verschiedenen Webseiten-Artikeln und Readme-Dateien zu ersparen, hier die Kurzlösung:

1. Das MixedMode-Projekt muss mit /NOENTRY gelinkt werden. Das erledigen Sie, indem Sie diese Option in den Linker-Eigenschaften unter den "Additional Options" angeben.

2. Die Runtime-Bibliothek muss mitgelinkt werden. Das ist bei managed-DLL-Projekten mit C++ von Haus aus nicht der Fall. Geben Sie also unter den "Additional Dependencies" im Linker-Input die msvcrt.lib mit an.

3. Von Haus aus enthalten die Linker-Einstellungen die Datei "nochkclr.obj" in den "Additional Dependencies". Entfernen Sie diese Datei; sie darf nicht mitgelinkt werden.

4. Stellen Sie sicher, dass die C-Runtime tatsächlich mitgelinkt wird. Das ist nur dann notwendig, wenn Sie keine Symbole daraus direkt referenzieren. Ist das nicht der Fall, dann fügen Sie das Symbol DllMainCRTStartup@12 auf dem Input-Bereich der Linker-Einstellungen unter "Force Symbol References" hinzu. Dadurch wird der Linker quasi gezwungen, die Runtime einzubinden.

Die Langversion dieser Erklärung mit dem kompletten Hintergrund, warum das so sein muss, finden Sie in MSDN unter der Knowledgebase-ID 814472.

Test des Wrappers

Um nun den managed Wrapper zu testen, brauchen Sie ein weiteres Projekt, mit dem Sie eine C#-Anwendung erzeugen. In diesem Projekt legen Sie im "Solution Explorer" zunächst eine neue Referenz an - und zwar zum Projekt mit dem managed Wrapper.

Danach können Sie im Form der C#-Anwendung einfach eine private Variable vom passenden Typ erzeugen:

private TechChannel.Managed.ManagedDemo md = new TechChannel.Managed.ManagedDemo();

Damit haben Sie eine Instanz der Wrapper-Klasse, die Sie einfach verwenden können:

private void button1_Click(object sender, System.EventArgs e)
{
md.Call();
MessageBox.Show( md.GetCalls().ToString());
}

Damit das Beispiel auch läuft, ist eine Besonderheit zu beachten: Die Testanwendung hat eine Referenz auf die managed DLL - aber keine auf die native. Der Grund dafür ist der, dass man schlicht und ergreifend keine Referenzen auf native DLLs legen kann.

Für das Entwickeln spielt das weiter keine Rolle - aber beim Start des Programms sehr wohl: Das geht nämlich nicht. Wenn Sie die Testanwendung aus dem Debugger heraus starten, wird die managed DLL wegen der darauf zeigenden Referenz in das Laufzeitverzeichnis der Anwendung kopiert. Zur nativen DLL gibt es aber keine Referenz und darum wird die DLL auch nicht kopiert. Also können Sie die Anwendung auch nicht starten.

Die Lösung ist aber einfach: Sie müssen die native DLL eben von Hand ins Laufzeitverzeichnis der Anwendung kopieren - oder sie erweitern das Projekt mit einem "Custom Build Step", der diese Aufgabe für Sie erledigt.

Trickreich: managed Code von nativem Code aus nutzen

Im bisherigen Verlauf des Beitrags war es immer so, dass Sie vom managed Code aus nativen Code aufgerufen haben. Es gibt aber auch Fälle, wo man das Umgekehrte erreichen will. Wie das funktioniert, zeigt der restliche Teil dieses Beitrags.

Der Sachverhalt wird bei der Nutzung von managed Code durch nativen Code ganz erheblich komplizierter. Damit Sie nicht die Orientierung verlieren, hier zunächst einmal der allgemeine Überblick, was eigentlich passieren soll.

Als Grundlage wird die Solution mit der mixedmode DLL verwendet. Dabei gibt es aber eine kleine Änderung: Die native Methode Call() hat im vorherigen Beispiel nur einen Integer-Wert hochgezählt. Stattdessen soll Sie nun eine etwas realistischere Aufgabe erhalten - eine, die verhältnismäßig lange dauert. In der Praxis wäre das zum Beispiel eine Methode einer nativen Klasse, die eine aufwendige Berechnung durchführt, die Sie nicht zu managed Code portieren möchten. Im Beispiel ist das einfach eine Schleife, die bis 1000 zählt und bei jedem Iterationsschritt eine Weile abwartet.

Für diese langwierige Aufgabe wird ein Fortschrittsmelder benötigt. Der befindet sich in der C#-Forms-Anwendung, also im GUI und damit im managed Code des Programms. Es wird also eine Möglichkeit gebraucht, diesen Fortschrittsmelder vom nativen Code aus anzusteuern.

Aufruf von managed Code

Das geht über ein paar Zwischenschritte: Nativer C++-Code kann durchaus auch C++-Code mit managed Extensions aufrufen - allerdings nur mit Einschränkungen. Die sind sogar recht groß: Im Wesentlichen kann man nur Funktionen aus managed DLLs aufrufen, die als extern C und __cdecl deklariert sind.

Der Fortschrittsmelder wird darum mit einem ganz einfachen prozeduralen Interface ausgestattet. Das besteht (aus nativer Sicht) aus drei Funktionen:

extern "C"
{
__declspec( dllimport) void __cdecl ProgressBegin( int max);
__declspec( dllimport) void __cdecl ProgressSet( int current);
__declspec( dllimport) void __cdecl ProgressEnd();
}

ProgressBegin() soll den Fortschrittsmelder starten. Dabei wird der Maximalwert für den Fortschrittsmelder übergeben. ProgressSet() setzt den Fortschrittsmelder auf den übergebenen Wert und ProgressEnd() beendet den Fortschrittsmelder. Im Code werden die nativen Funktionen dann wie folgt aufgerufen:

void
NativeDemo::Call()
{
TechChannel::Managed::ProgressBegin( 1000);
for( int step=0; step<1000; step++)
{
TechChannel::Managed::ProgressSet( step);
Sleep( 10);
}
TechChannel::Managed::ProgressEnd();
}

Auf nativer Seite ist die Sache damit fast erledigt. Was noch fehlt, sind die Funktionen selbst, denn ohne die wird es die Fehlermeldung "unresolved externals" geben. Bei den Prototypen der Funktion haben Sie bereits angegeben, dass die Funktionen aus einer DLL stammen sollen (__declspec( dllimport)). Diese DLL wird in einem späteren Arbeitsschritt angelegt. Ist die DLL fertig, müssen Sie noch die Import-Library der DLL zum nativen Projekt hinzufügen. Dann kann man das auch wieder linken.

Proxy-DLL

Die DLL, gegen die der native Code gelinkt wird, erfüllt zwei Aufgaben: Zum einen exportiert sie ein Funktions-Interface in den nativen Code, zum anderen kommuniziert sie mit dem managed Code.

Dabei kommen mehrere Tricks zum Einsatz: Zum einen bekommt die DLL eine Klasse, die nur statische Methoden hat, die aber selbst durch das Schlüsselwort __gc zu managed Code wird.

Die statischen Methoden sind dann die Methoden, die von den mit __cdecl deklarierten Funktionen aufgerufen werden können - diese Funktionen selbst werden auch in der DLL implementiert. Damit kann der native Code schon einmal Funktionen aufrufen, die letztlich in den Methoden einer verwalteten Klasse landen. Die Klasse hat den Namen ProgressMeterProxy und die statischen Methoden haben Namen, die passend für den Fortschrittsmelder gewählt sind. Die Implementierung der __cdecl-Funktionen sieht also wie folgt aus:

extern "C"
{
__declspec( dllexport) void __cdecl ProgressBegin( int max)
{
ProgressMeterProxy::Begin( max);
}
__declspec( dllexport) void __cdecl ProgressSet( int current)
{
ProgressMeterProxy::Set( current);
}
__declspec( dllexport) void __cdecl ProgressEnd()
{
ProgressMeterProxy::End();
}
}

Die Proxy-Klasse muss ja nun irgendetwas mit den Aufrufen tun - und hier kommt wieder ein kleiner Trick zum Zuge: Sie definieren ein Interface, das ebenfalls Methoden hat, die zum Fortschrittsmelder passen, und geben der Proxy-Klasse einen statischen Zeiger auf ein solches Interface als Member mit:

public __gc class ProgressMeterProxy
{
private:
static IProvideProgressMeter* m_p = 0;

Dann statten Sie die Klasse mit einer Funktion Init() aus: Die erhält einen solchen Zeiger als Parameter und speichert ihn in einem privaten Member.

static void Init( IProvideProgressMeter* progress)
{
m_p = progress;
}

Nun kann der Proxy die Aufrufe seiner Methoden einfach an das Interface weiter delegieren. Hier zum Beispiel die Methode Set():

static void Set( int current)
{
m_p->Set( current);
}

Sie brauchen nun nur noch eine Klasse, die das Interface IProgressMeter implementiert und auch einen echten Fortschrittsmelder erreichen kann. Der befindet sich wie zuvor erwähnt in der C#-Anwendung. Dort ziehen Sie einfach ein entsprechendes Kontrollelement auf das Form. Dann muss noch das Interface implementiert werden: Die Implementierung delegiert dabei die eigentliche Tätigkeit einfach an das Fortschrittsmelder-Control:

public void Set( int current)
{
this.progressBar1.Value = current;
}

Damit ist der Kreis geschlossen: Sie wissen nun, wie Sie nativen Code aus managed Code heraus aufrufen können, und ebenfalls wie sie vom nativen Code wieder zurück in den managed Code gelangen. Die Richtung vom nativen zum verwalteten Code ist dabei ein wenig aufwendig - zum Glück ist das aber auch eine Richtung, die nicht oft benötigt wird.

Beispiele zum Download

Hier finden Sie alle vorgestellten Beispiele zum Download:

com_ax.zip: Solution mit zwei Projekten. Ein MFC ActiveX-Control und eine C#-Anwendung, die das Control benutzt.

com_atl: Solution mit zwei Projekten. Ein ATL COM-Objekt und eine C#-Anwendung, die das Objekt benutzt.

Pinvoke.zip: Solution mit einem Projekt, das die Funktionalität von P/Invoke demonstriert.

Mixedmode.zip: Solution, die eine mixed-mode-Wrapper-DLL demonstriert.

Backwards.zip: Solution, die den Aufruf von managed Code aus nativem Code erläutert.