© Martin Korneffel, Stuttgart der 05.10.06 +++ email: info@tracs.de +++ web: www.tracs.de

C++/CLI

Inhalt

  1. Verwaltete C++ Programmierung

    1. Migrationsziele

  2. CLI

  3. C++/CLI aktivieren und deaktivieren

  4. Sprachkonstrukte für die verwaltete C++ Programmierung

    1. Literale

    2. CTS Datentypen

    3. ^handle

    4. % Tracking Referenzen

    5. Call by Value vs Call by Reference

    6. Deterministische Destruktion durch Instanziierung von Referenztypen auf dem Stack

    7. Vererbung

      1. Virtuelle Funktionen

    8. Schnittstellen

  5. C++ Interop

    1. Blitfähige Typen

    2. Speicherblöcke mittels pin_ptr<Type> fixieren

  6. Bereitstellung

    1. C++ Assemblies signieren

  7. Fußangeln

    1. Intellisense ist unzuverlässig

    2. Prüfe, ob Parameternamen verwalteter Methoden mit Bezeichnern für C++ Makros kollidieren

Verwaltete C++ Programmierung

Der Microsoft C++ Compiler ermöglicht Programmierung unter der verwalteten Umgebung als auch unter der klassischen C++ Laufzeit. Mit der Kompileroption /clr können Programme erstellt werden, die während ihres Ablaufes beide Laufzeitumgebungen nutzen. Es wird hierbei von gemischter Programmierung gesprochen. Dies Option wurde geschaffen, um die Migration bestehender C++ Anwendungen in die verwaltete .NET Welt zu erleichtern. So kann eine MFC- Anwendung Problemlos mit /clr kompiliert werden, und damit die .NET Klassenbibliotheken nutzen.

Migrationsziele

Mit den Optionen /clr:pure und /clr:safe werden die Möglichkeiten, Code aufzurufen, der unter der klassischen Laufzeitumgebung läuft, schrittweise eingeschränkt. Unverwalteter Code ist bezüglich der Stabilität und der Sicherheit im Nachteil gegenüber dem Verwalteten Code. Ziel der Migration besteht aus Sicht des Compilers in den Übergängen von den Optioen /clr → /clr:pure → /clr:safe.




CLI

Die Abkürzung CLI steht für Common Language Infrastructure.

CLI

CTS

Common Type System

CLS

Common Language System

CIL

Common Intermediate Language

VES

Virtual Execution System

C++/CLI aktivieren und deaktivieren

Damit aus einem C++ Quelltext eine verwaltete Assembly entsteht, muß bei der Compilation der Schalter /clr aktiviert sein.

Innerhalb des Quelltextes können Typen und Methoden gemischt für die verwaltete und unverwaltete Laufzeitumgebung implementiert werden. Die Bereich mit für die verwaltete Umgebung muß dabei durch #pragma managed ... #pragma unmanaged Blöcke eingelschlossen sein.

Sprachkonstrukte für die verwaltete C++ Programmierung

Literale

Typen

Beispiel

Erläuterung

Referenzen

nullptr

Null- Handle



CTS Datentypen

Bei der Erweiterung der Sprache C++ um neue Schlüsselwörter für die verwaltete Programmierung ging Microsoft ursprünglich nach der Konvention vor, das alle neuen Schlüsselwörter mit dem Präfix doppelter Unterstrich gekennzeichnet wurden. Das führte zu schwer lesbaren C++ Quelltexten.

Ab der Version 2005 beschreitet Microsoft neue Wege: die verwalteten Datentypen werden durch sog. "gespacete" Schlüsselwörter gekennzeichnet. Diese setzen sich i.A. aus zwei, durch ein Leerzeichen getrennte Partikel zusammen.

Diese neue Lösung ist aus ergonomischer Sicht gelungen.

Schlüsselwort

Bedeutung

Beispiel

ref class

Klasse, deren Objekte im verwalteten Heap plaziert werden

ref class SUnit {
public:
     SUnit(double pvalue, SUnits punit)
     : value(pvalue), unit(punit)
     {}

     SUnits unit;
     double value;
     double toMeter();
};
value class

Klasse, deren Objekte auf dem Stack plaziert werden


enum class

Verwalteter Aufzählungstyp

enum class SUnits {
   s_micro=1,
   s_mm=2,
   s_cm=3,
   s_dm=4,
   s_m=5,
   s_km=6
};      
interface class

Verwaltete Schnittstelle


ref struct

Datenstruktur, die auf dem verwalteten Heap plaziert wird. Alle Member sind automatisch public

ref struct Point {
     SUnit x, y;
};
value struct

Datenstruktur, die auf dem Stack plaziert wird. Alle Member sind automatisch public


^handle

Eine Instanz auf dem verwalteten Heap wird durch sog. handles adressiert.

// Spezieller Operator gcnew erzeugt eine Instanz auf dem verwalteten Heap
// und liefert eine tracking- Handle auf das Objekt zurück
SUnit ^weg1 = gcnew SUnit(1, s_cm);  
                                     
// Der Operator % liefert die Tracking- Handle eines verwalteten Objektes
// so wie der & Operator für ein unverwaltetes Objekt eine Referenz liefern würde
SUnit ^weg2 = %weg1;

Objekte im verwalteten Heap können durch den GC auf andere Speicherplätze während ihrer Existenz verschoben werden. Die Handle wird automatisch durch den GC nach den Verschiebeoperation aktualisert.

Wie bei unverwalteten C++ Pointern erfolgt der Zugriff auf die Member über den -> Operator:

weg1-> value = 1;
weg1-> unit = s_dm;
double Weg1InMeter = weg1-> ToMeter();

Wie in der unverwalteten Welt existiert auch ein Dereferenzierungsoperator (*):

int ^i = gcnew int();
*i = 99;

% Tracking Referenzen

In der unverwalteten Welt können mittels Instanzen vom Typ <Typname>& Werte auf dem Stack oder Heap referenziert werden. Diese Instanzen werden als Referenzen bezeichnet

// Referenzen in der unverwalteten Welt
int  x    = 99;
int& refx = x;

// Durch folgende Anweisung wird der Wert von x incrementiert !
refx++;

Da die Position von Objekte im Speicher in der verwalteten Welt durch den GC während der Laufzeit verändert werden kann, ist die klassische C++ Referenz nicht mehr verwendbar. An ihre Stelle tritt die Tracking Referenz.

Eine Trackingreferenz ist vom Typ <Typname>%. Beispiel:

// Referenzen in der verwalteten Welt
int  x    = 99;
int% refx = x;

// Durch folgende Anweisung wird der Wert von x incrementiert !
refx++;

SUnit ^weg1 = gcnew SUnit(1, s_cm);
SUnit ^%refWeg = weg1;

// Durch folgende Anweisung wird der Wert von x incrementiert !
refWeg-> value++;

Call by Value vs Call by Reference







Deterministische Destruktion durch Instanziierung von Referenztypen auf dem Stack

Alle verwalteten Referenztypen werden immer auf dem verwalteten Heap platziert. Jedoch ist es auch möglich in C++, Referenztypen als automatische Variablen auf dem Stack zu instanziieren. Auch hierbei erfolgt die Platzierung auf dem verwalteten Heap und implizit generiert der Compiler eine Trackinghandle, die auf das Objekt verweist. Jedoch unterliegt die Destruktion dem Determinismus automatischer Variablen: die Destruktion setzt ein, sobald der Gültigkeitsbereich der Variablen verlassen wird.

Die Destruktion erfolgt in .NET durch das Dispose Muster: Die Klasse, welche eine Destruktion erfordert, implementiert die IDisposable Schnittstelle, wobei die Destruktionslogik in der Dispose- Methode dieser Schnittstelle verpackt wird. In Sprachen wie C# muß die Dispose- Methode explizit zum Zeitpunkt der Destruktion aufgerufen werden oder durch Blöcke wie using {...} automatisch erfolgen. C++ automatisert diesen Prozess, indem die Basis aller Referenzklassen die IDisposable Schnittstelle implementiert, und der C++ Destruktor einer Klasse auf Dispose abgebildet wird. Der Speicherplatz, den ein Referenztyp belegt wird jedoch erst durch den GC freigegeben, welcher für jedes freizugebende Objekt den Finalisier aufruft. Der Finalizer trägt wie der Destruktor den Namen der Klasse, nur ist ihm anstelle der ~ das ! vorangestellt

public ref class CMeasureS
{
  public:
    double value;

    enum class UnitS { mm, cm, dm, m, km};
    UnitS unit;

    CMeasureS() {
        value = 0;
        unit = UnitS::m;
    }

    // Deterministischer Destruktor
    ~CMeasureS() {
        System::Console::WriteLine(
           "Destruktion von CMeasureS {0:N} {1:G}",
           value,
           unit.ToString());
    }

    // Finalisirung durch den GC
    !CMeasureS() {
        System::Console::WriteLine(
           "Finalisierung von CMeasureS {0:N} {1:G}", 
           value, 
           unit.ToString());
    }

    :
};



SUnit weg1(1, s_m);

Platzierung auf dem Stack. Das Objekt wird im verwalteten Heap allokiert. Der Destruktor wird aufgerufen, wenn der Gültigkeitsbereich des Objektes verlassen wird

SUnit ^weg2 = gcnew SUnit(1, s_cm);

Platzierung auf dem verwalteten Heap. Referenziert wird über eine Tracking Handle. Der Destruktor wird aufgerufen, wenn ein delete auf der Handle erfolgt. Nach dem delete ist der Finalizer abgeschaltet. Wurde die Handle durch kein delete explizit freigegeben, dann wird durch den GC vor der Speicherfreigabe der Finalizer aufgerufen.

Vererbung

Virtuelle Funktionen

Werden in abgeleiteten Klassen virtuelle Funktionen überschrieben, so ist dies gegenüber klassischem C++ durch das Schlüsselwort override zu kennzeichnen:

namespace grafik {

  ref class CLine : public CFigur
  {
  public:
    CPoint a, b;

    virtual void print(CPlotter ^plotter) override {
        plotter->print_line(a, b, style);
    }

    virtual void translate(CPoint ^vec) override {
        a.x += vec.x;
        a.y += vec.y;

        b.x += vec.x;
        b.y += vec.y;
    }
    :
}

Schnittstellen

C++ Interop

Eine Besonderheit von C++ besteht in der Möglichkeit der gemischten Programmierung. Dabei kann eine Assembly erstellt werden, in der Code unter Verwaltung der modernen CLR- Runtime (verwaltet), als auch unter der klassischen Runtime (unverwaltet) ausgeführt werden kann. Dabei werden die Übergänge zwischen den beiden Laufzeitumgebungen noch durch spezielle C++ Sprachmittel erleichtert, die im folgenden besprochen werden.

Blitfähige Typen

Wertetypen in der verwalteten Welt unterliegen einer ähnlichen Speicherverwaltung (Stack) wie in der unverwalteten. Wenn die Binärformate sich zwischen beiden Welten nicht unterscheiden, dann können die verwalteten Werte durch eine 1:1 Kopie aus den nichtverwalteten Werten erstellt werden und umgekehrt. Man spricht auch von blitfähigen Typen.

Folgende verwaltete Typen sind blitfähig:

void, byte, usigned byte, short, ushort, int, uint, long, ulong, float, double, *(Pointer)

Speicherblöcke mittels pin_ptr<Type> fixieren

Objekte, die auf dem managed Heap liegen, führen kein ruhiges Leben: der GC kann sie zu jedem Zeitpunkt verschieben, um sich ein großes Stück zusammenhängenden freien Speichers zu besorgen. Werden also Adressen von Objekten an unverwaltete Routinen übergeben, dann könnten diese während der Ausführung der unverwalteten Routinen ungültig werden. Der einzige Ausweg besteht in diesem Falle darin, die zu Übergebenen Objekte zu kopieren, was sich negativ auf die Laufzeit auswirkt.

In C++/CLI kann mittels der pin_ptr<Type> Deklaration ein Speicherblock im verwalteten Heap von den Verschiebeaktionen des GC ausgenommen werden, solange der pin_ptr existiert. Man bezeichnet die durch einen pin_ptr adressierten Objekte auch als fixierte Objekte.

Wenn ein Objekt mittels eines pin_ptr fixiert wurde, dann kann anstelle der Kopie die Referenz an die unverwaltete Routine übergeben werden. Die unverwaltete Routine hat dann Zugriff auf den durch das Objekt belegten Speicherplatz im managed Heap.

Folgendes Beispiel stammt aus der MSDN 2005(ms-help://MS.MSDNQTR.v80.de/MS.MSDN.v80/MS.VisualStudio.v80.de/dv_vccore/html/c2b37ab1-8acf-4855-ad3c-7d2864826b14.htm)

// PassArray1.cpp
// compile with: /clr
#include <iostream>

using namespace std;
using namespace System;

#pragma unmanaged

void TakesAnArray(int* a, int c) {
   cout << "(unmanaged) array recieved:\n";
   for (int i=0; i<c; i++)
      cout << "a[" << i << "] = " << a[i] << "\n";

   cout << "(unmanaged) modifying array contents...\n";
   for (int i=0; i<c; i++)
      a[i] = rand() % 100;
}

#pragma managed

int main() {
   array<int>^ nums = gcnew array<int>(5);

   nums[0] = 0;
   nums[1] = 1;
   nums[2] = 2;
   nums[3] = 3;
   nums[4] = 4;

   Console::WriteLine("(managed) array created:");
   for (int i=0; i<5; i++)
      Console::WriteLine("a[{0}] = {1}", i, nums[i]);

   pin_ptr<int> pp = &nums[0];
   TakesAnArray(pp, 5);

   Console::WriteLine("(managed) contents:");
   for (int i=0; i<5; i++)
      Console::WriteLine("a[{0}] = {1}", i, nums[i]);
}

Bereitstellung

C++ Assemblies signieren

Die Voraussetzung zur Installation einer Assembly im GAC ist, daß sie einen starken Namen besitzt. Dazu wird sie mit einem privaten Schlüssel. Der öffentliche Schlüssel wird zum entschlüsseln der Signatur muß in der Assembly mitgespeichert werden. Beide Schritte erfolgen durch das signieren. Das Signieren wird in C++ durch den Linker abgewickelt. In den Projekteinstellungen ist dazu unter folgender Option ein Verweis auf die mittels des Tools sn.exe erstellten Schlüsselpaardatei einzutragen:

Projekteigenschaften/Konfigurationseigenschaften/Linker/Erweitert/Schlüsseldatei = [Dateipfad auf die Schlüsseldatei]

Fußangeln

C++ ist eine Sprache mit langer Geschichte. Es gibt sehr viele Schalter für den Compiler, und die eingesetzten Bibliotheken wie MFC strotzen nur so von Makros. In einer solchen Umgebung kommt es leicht zu Namenskollisionen, die besonders im Fall von Makros nicht vom Compiler erkannt werden, und dem Entwickler Knobeleien beim Kompilieren bescheren. Beispiele:

Intellisense ist unzuverlässig

Im Unterschied zu C# und VB.NET ist Intellisense unter C++ ein sehr launischer Geselle. Ursache ist die Komplexität der Sprache. Werden Templates instanziiert, dann wirft er in der Regel das Handtuch. Sporadisch taucht Intellisense ab- man muss blind weitertippen und plötzlich ist er wieder da. Daran muß man sich wohl gewöhnen...

Prüfe, ob Parameternamen verwalteter Methoden mit Bezeichnern für C++ Makros kollidieren

Bei der Implementierung einer verwalteten Schnittstelle, die in C# definiert war, kam ich in C++ mit dem errno Makro in Konflikt

public ref class MfcMsgBoxHnd : public mko::ILogHnd
{
public:
  
 // Implementierung der Schnittstelle
 virtual void OnError(int errno, System::String ^msg) {
   :
 }

Der Compiler beschwerte sich unverständlicherweise, das in der Klasse MfcMsgBoxHnd keine Implementierung der IlogHnd::OnError- Methode vorhanden sei. Beim Überfahren des errno- Parameters mit der Maus meldete Intellisense als Typ von errno

#define int* (*)(void)

erno wurde also vom Präprozessor durch ein Makro als Funktionspointer überschrieben- die Signatur von OnError war nun eine völlig andere als von mir angenommen, was den Fehler erklärte. Indem errno in errNr umbenannt wurde, war der Fehler behoben. Das ist nicht wirklich ermutigend ...