C#-Ausblicke und Wrappen von C++-Klassen

Veröffentlicht: 18. Feb 2003 | Aktualisiert: 23. Jun 2004

Von Eric Gunnerson

Im letzten Artikel wurde das Aufrufen vorhandener Bibliotheken über P/Invoke erläutert. Dieses Mal befassen wir uns mit dem Wrappen von C++-Klassen, damit diese von C# aus verwendet werden können. Zunächst aber folgt ein kurzer Blick in die Zukunft von C#.

Auf dieser Seite

 Zukünftige C#-Features
 Wrappen von C++-Klassen
 Ein einfaches Beispiel
 Erstellen der Datei
 Informationen auf der Website

Zukünftige C#-Features

Anfang November gab der C#-Entwickler Anders Hejlsberg eine Reihe neuer Features bekannt, die in einer zukünftigen Version der C#-Sprache verfügbar sein werden. Zu diesen Neuerungen zählt die lang erwartete Erweiterung von C# um generische Features. Weitere Informationen zu diesen Features finden Sie unter http://www.csharp.net (in Englisch).
Anmerkung:
Diese Features sind nicht in der nächsten Version von Visual Studio enthalten.

 

Wrappen von C++-Klassen

Wenn Sie C++-Code in C# verwenden müssen oder über eine Reihe komplizierter Win32-Funktionen verfügen, die Sie von C# aus aufrufen möchten, müssen Sie eventuell die verwalteten Erweiterungen für C++ verwenden.

Wenn Sie C++ bereits verwendet haben, wird der gezeigte Code relativ einleuchtend sein, wobei nur einige neue Konstrukte verstanden werden müssen. Falls Sie noch nicht mit C++ gearbeitet haben, kann die Syntax etwas verwirrend sein. Ich behandle hier nur die Grundlagen der Verwendung von verwaltetem C++. Wenn Sie ausführliche Informationen wünschen, empfehle ich den Essential Guide to Managed Extensions for C++ (in Englisch) von Siva Challa und Artur Laksberg, zwei Entwicklern im Microsoft Visual C++® .NET-Team.

Es gibt zwei Möglichkeiten, um verwalteten Zugriff auf eine C++-Klasse zu erhalten. Wenn Sie die Klassen sowohl von verwalteten als auch systemeigenen C++-Umgebungen aus verwenden müssen, empfehle ich den hier erläuterten Wrapperansatz. Wenn Sie nur eine Migration zu verwaltetem Code durchführen, könnten Sie die vorhandene C++-Klasse ändern, anstatt sie zu wrappen.

 

Ein einfaches Beispiel

Zu Beginn wrappe ich eine einfache C++-Klasse. Diese Klasse implementiert eine sehr einfache Verschlüsselung, mit der Sie Informationen von sehr geringem Wert verbergen können.

C++ trennt die Deklaration einer Klasse von einer Definition, weshalb wir zwei Dateien für unsere Klasse erstellen müssen. Die erste lautet EncodeDecode.h:

#pragma once 
__nogc 
class EncodeDecode 
{ 
private: 
   int keyOffset; 
public: 
   EncodeDecode(int theKeyOffset); 
   char* Encode(char* message); 
   char* Decode(char* message); 
};

Die Klasse verfügt über ein privates Ganzzahlfeld, das den verwendeten Schlüssel speichert, sowie über Encode()- und Decode()-Methoden. Das __nogc-Schlüsselwort vor der Klassendefinition ist eine Neuerung bei C++ und gibt an, dass es sich bei dieser Klasse um eine normale C++-Klasse und keine verwaltete Klasse handelt.

Die zweite Datei lautet EncodeDecode.cpp und enthält die Implementierung der Klasse:

#include "stdafx.h" 
#include <string.h> 
#include "DecoderRing.h" 
#include "EncodeDecode.h" 
EncodeDecode::EncodeDecode(int theKeyOffset) 
{ 
   keyOffset = theKeyOffset; 
} 
char* EncodeDecode::Encode(char* pMessage) 
{ 
   char* pEncoded = new char[strlen(pMessage) + 1]; 
   char* pDest = pEncoded; 
   char* pSource = pMessage; 
   while (*pSource != '\0') 
   { 
   *pDest = *pSource + keyOffset; 
   pSource++; 
   pDest++; 
   } 
   *pDest = '\0'; 
   return pEncoded; 
} 
char* EncodeDecode::Decode(char* pMessage) 
{ 
   char* pDecoded = new char[strlen(pMessage) + 1]; 
   char* pDest = pDecoded; 
   char* pSource = pMessage; 
   while (*pSource != '\0') 
   { 
   *pDest = *pSource - keyOffset; 
   pSource++; 
   pDest++; 
   } 
   *pDest = '\0'; 
   return pDecoded; 
}

In der Encode()-Methode weise ich eine neue Zeichenfolge zu und füge das Schlüsseloffset jedem Zeichen hinzu. Die Decode()-Methode führt die gleiche Operation umgekehrt durch.

Wenn Sie C++ nicht verstehen, kümmern Sie sich nicht um die seltsamen Zeigeroperationen im Code. Wichtig dabei ist lediglich die Tatsache, dass die Methoden eine neue Zeichenfolge zuweisen und an diese dann den Zeiger zurückgeben. Beim Wrappen dieser Klasse sollte dies stets berücksichtigt werden.

Für das Wrappen benötigen wir die Definition einer verwalteten Klasse in DecoderRing.h:

#include "EncodeDecode.h" 
using namespace System; 
public __gc class DecoderRing: public IDisposable 
{ 
private: 
   EncodeDecode __nogc* m_pEncodeDecode; 
public: 
   DecoderRing(int theKeyOffset); 
   ~DecoderRing(); 
   void Dispose(bool disposing); 
   void Dispose(); 
   String* Encode(String* message); 
   String* Decode(String* message); 
};

Der Zweck dieser Datei besteht einfach im Wrappen der EncodeDecode-Klasse. Wir behalten einen Zeiger auf die EncodeDecode-Klasse, leiten die Encode- und Decode-Aufrufe über den Zeiger weiter und verarbeiten die Übersetzung zwischen .NET-Zeichenfolgen und systemeigenen C++-Zeichenfolgen.

Am Anfang dieser Headerdatei füge ich die Headerdatei EncodeDecode.h ein. Die using-Anweisung danach entspricht der using System;-Anweisung in einer C#-Datei.

Die Klasse ist als eine __gc-Klasse markiert, was bedeutet, dass es sich um eine verwaltete Klasse handelt. Sie implementiert die IDisposable-Schnittstelle, damit die Bereinigung der nicht verwalteten Zeiger gewährleistet ist, wenn diese nicht mehr benötigt werden.

Beachten Sie, dass die Encode()- und Decode()-Methoden über String*-Parameter verfügen. Diese besitzen den .NET-Zeichenfolgentyp, anstatt den systemeigenen C++-Typ char*.

Nun geht es weiter mit der Implementierung. Hier ist der erste Teil davon:

#include "stdafx.h" 
#include <stdio.h> 
#include "EncodeDecode.h" 
#include "DecoderRing.h" 
using namespace System::Runtime::InteropServices; 
DecoderRing::DecoderRing(int theKeyOffset) 
{ 
   m_pEncodeDecode = new EncodeDecode(theKeyOffset); 
}

Dieser Teil ist relativ einfach. Der Konstruktor verarbeitet dabei lediglich den Schlüssel, erstellt eine Instanz der EncodeDecode-Klasse und speichert diese als Feld. Als Nächstes müssen wir uns damit befassen, wie wir diese Instanz freigeben, wenn wir damit fertig sind. Dies geschieht auf die gleiche Art und Weise wie bei Implementierung von IDisposable in C#, jedoch mit einer leicht unterschiedlichen Syntax. So sieht der Code aus:

void DecoderRing::Dispose(bool disposing) 
{ 
   if (m_pEncodeDecode) 
   { 
   delete m_pEncodeDecode; 
   m_pEncodeDecode = NULL; 
   } 
   if (disposing) 
   { 
   GC::SuppressFinalize(this); 
   } 
} 
DecoderRing::~DecoderRing() 
{ 
   Dispose(false); 
} 
void DecoderRing::Dispose() 
{ 
   Dispose(true); 
}

Die erste Dispose()-Funktion übernimmt die Freigabe der Instanz. Anschließend kann diese Funktion direkt über Dispose() oder von der Laufzeit über den Destruktor (DecoderRing::~DecoderRing) aufgerufen werden.

Der oben stehende Code verarbeitet die Infrastruktur des Wrapvorgangs. Nun müssen wir die Encode()- und Decode()-Methoden implementieren. Die größte Herausforderung dabei liegt im Umgang mit dem Unterschied zwischen den .NET-Datentypen und den C++-Datentypen. Die Encode()-Methode soll in Portionen von einigen Zeilen erläutert werden:

String* DecoderRing::Encode(String* message) 
{ 
   IntPtr messageBuffer = 
   Marshal::StringToHGlobalAnsi(message);

Die Marshal-Klasse enthält eine Reihe nützlicher Dienstprogrammfunktionen. Dieselben Funktionen werden auch von der Laufzeit bei Aufruf einer Funktion mit P/Invoke verwendet. Es gibt eine Reihe von Funktionen, die Zeichenfolgen betreffen. In diesem Fall muss eine Konvertierung von einer .NET-Zeichenfolge in eine ANSI-Zeichenfolge durchgeführt werden, die mit AllocHGlobal zugewiesen wurde. Wir erhalten diese als IntPtr-Typ zurück, da die Marshal-Klasse von allen .NET-Sprachen aufrufbar sein muss. Aus diesem Grund kann sie den Zeiger nicht direkt zurückgeben.

   char* pMessage =  
   (char*) messageBuffer.ToPointer(); 
   char* pEncoded = m_pEncodeDecode->Encode(pMessage);

Als Nächstes erhalten wir einen normalen C++-Zeiger aus dem IntPtr und rufen die Encode()-Funktion auf. Diese Funktion gibt einen regulären char*-Zeiger aus C++ zurück. Anschließend muss diese Zeichenfolge wieder in eine .NET-Zeichenfolge umgewandelt werden. Wiederum verwenden wir eine Funktion aus der Marshal-Klasse:

   String* encodedString = 
   Marshal::PtrToStringAnsi(pEncoded);

Schließlich müssen noch einige Bereinigungsarbeiten durchgeführt werden, um die Zeichenfolge freizugeben, die wir aus der Encode()-Methode erhalten haben. Außerdem muss die Zeichenfolge freigegeben werden, die für die Übergabe an Encode() zugewiesen wurde.

   delete pEncoded; 
   Marshal::FreeHGlobal(pMessage); 
   return encodedString; 
}

Und damit wären wir fertig. Die Decode()-Funktion wird hier nicht erläutert, da sie fast identisch ist.

 

Erstellen der Datei

Sie können diese Dateien mit der folgenden Befehlszeile erstellen:

/Od /D "WIN32" /D "_DEBUG" /D "_MBCS" /D "_WINDLL" /FD /EHsc /MTd /GS  
/Yu"stdafx.h" /Fp"Debug/DecoderRing.pch" /Fo"Debug/" /Fd"Debug/vc70.pdb"  
/W3 /nologo /c /Zi /clr /TP

Anmerkung:
In das Codebeispiel weiter oben wurden aus Gründen der Lesbarkeit Zeilenumbrüche eingefügt, die bei Verwendung des Codes entfernt werden sollten.

Ich empfehle Ihnen, den Beispielcode downzuloaden. Er enthält ein Visual Studio-Projekt, das Sie verwenden können.

 

Informationen auf der Website

Neben Informationen über zukünftige Sprachfeatures enthält die C#-Website http://www.csharp.net (in Englisch) einen Link zu einer Reihe interessanter Artikel von Ron Jeffries, einer Autorität zum Thema Extreme Programming (in Englisch). Suchen Sie nach Adventures in C# im Abschnitt Other Links (in Englisch).