Brückencode-Chronik oder ein C++/CLI-Erfahrungsbericht

von | 24.01.2026

Ahoi Landratte. Ich bin Käpt’n Blackbyte. Kein ganz gewöhnlicher Softwareentwickler, der nur gucken will, ob er ein solchen Blogeintrag in einem professionellen digitalen Hafen veröffentlichen kann.

Ich erzähle Ihnen von meiner Zeit auf den teils stürmischen Gewässern des CLI/C++-Meers. Dieses liegt zwischen dem vergleichsweise sicheren Hafen von C#, wo grafische Oberflächen blinken und man sich kaum Gedanken über verlorene Goldstücke in Form von Memory Leaks machen muss, und der rauen See von C++, auf der seit Jahrzehnten legendäre Schiffe wie OpenSSL, VTK oder GLM unterwegs sind.

Dies ist kein Seemannsgarn, sondern eine ehrliche Chronik darüber, wie man zwischen diesen beiden Welten navigiert, auch dann, wenn die Wellen hoch schlagen. Hören Sie also genau hin und profitieren Sie von den Erfahrungen, und ja, auch von den Fehlern, die ich auf dieser Reise gemacht habe.

Binnenschifffahrt mit Platform Invocation Services

Wenn Sie nur ein paar Golddukaten transportieren wollen, halten Sie sich am besten an die alte Piratenweisheit „Ein Kuss auf die Segel hält den Wind günstig“. Auch bekannt als KISS, „keep it simple and stupid“. In diesem Fall nutzen Sie P/Invoke, also die Platform Invocation Services.

Dabei wird eine C++-Funktion direkt als unmanaged Code aufgerufen. Das funktioniert allerdings nur für einfache Datentypen. In diesen Fällen ist es jedoch deutlich unkomplizierter, als gleich ganze Teekisten über Bord werfen zu müssen.

using System.Runtime.InteropServices;
class Program{
    [DllImport("mylib.dll", CallingConvention = CallingConvention.Cdecl)]
    public static extern int AddNumbers(int a, int b);
    static void Main()    {
        int result = AddNumbers(5, 7);
        Console.WriteLine($"Ergebnis: {result}");
    }}

Handel mit komplexen, zusammengesetzten Schätzen

Was aber tun, wenn Sie eine Schatzkiste besitzen, in der sich unterschiedlich wertvolle Güter befinden, und Sie diese gesamte Kiste auf einmal transportieren möchten, statt jedes Objekt einzeln zu verladen?

Genau dafür gibt es C++/CLI, einen Microsoft-spezifischen Dialekt, der C++ erweitert und die Brücke zwischen ausführbarem C++-Code und .NET-Code schlägt. (Damit funktioniert das Zusammenspiel übrigens auch mit F# oder VB. Ein alter Pirat erzählt seine Geschichten jedoch lieber geradlinig, daher bleibt es hier bei C#.

Die Handelsroute sieht in diesem Beispiel wie folgt aus [1]:

Handelsroute im C++/CLI-Projekt - Blog - t2informatik

Abbildung 1: Handelsroute im C++/CLI-Projekt

Jeder Halt ist ein eigenes Projekt, wobei InterfaceCLI und InterfaceBackCpp, den Microsoft C++ Compiler mit dem Schalter /clr nutzen, womit wir in diesen die CLI-Syntax verwenden können.

In unsere nativen C++ Anwendung, können wir komplexe Daten erzeugen (ironischerweise sind die Daten als Beispiel sehr einfach gehalten):

ValuableItem::ValuableItem(std::wstring name, int value){_name = name; _value = value;}
std::wstring ValuableItem::GetName() { return _name;}
int ValuableItem::GetValue() { return _value; }

Treasure::Treasure(ValuableItem& item1, ValuableItem& item2,ValuableItem& item3)
{
    contents.push_back(item1);
    contents.push_back(item2);
    contents.push_back(item3);
}
std::vector<ValuableItem>& Treasure::GetContents(){return contents;}

Wir erzeugen zunächst einige Beispieldaten und übergeben sie an das InterfaceCLI. Da der Aufruf aus nativem C++ erfolgt, muss der verwendete Header reines C++ sein und darf keine CLI-Bestandteile enthalten.

Er stellt jedoch sicher, dass die entsprechende Klasse in der Bibliothek gefunden werden kann. Auf eine detaillierte Erklärung, wie declspec dies im Hintergrund ermöglicht, verzichte ich an dieser Stelle bewusst. Ich möchte die Aufmerksamkeit auf die eigentliche Geschichte lenken.

Zusätzlich enthält der Header ein Objekt, das den Zugriff auf die Funktionalitäten der C#-Seite erlaubt.

class CSharpInterface;
class __declspec(dllexport) CppCliWrapper
{
public:
    virtual void SendTreasure(Treasure& treasure);
private:
    CSharpInterface* _managedState;
};

Und nun nehmen Sie beide Augenklappen ab und schauen Sie, wie wir CLI-Syntax benutzen:

void CppCliWrapper::SendTreasure(Treasure& treasure)
{
    ITreasure^ managedTreasure = TreasureConverter::ToITreasure(treasure);
    _managedState->CSharpLib->ReceiveTreasure(managedTreasure);
}
ITreasure^ TreasureConverter::ToITreasure(Treasure& treasure)
{
    std::vector<ValuableItem>& items = treasure.GetContents();
    String^ item1Name = msclr::interop::marshal_as<String^>(items[0].GetName());
    int item1Value = items[0].GetValue();
    String^ item2Name = msclr::interop::marshal_as<String^>(items[1].GetName());
    int item2Value = items[1].GetValue();
    String^ item3Name = msclr::interop::marshal_as<String^>(items[2].GetName());
    int item3Value = items[2].GetValue();

    ManagedValuableItem^ item1 = gcnew ManagedValuableItem(item1Name, item1Value);
    ManagedValuableItem^ item2 = gcnew ManagedValuableItem(item2Name, item2Value);
    ManagedValuableItem^ item3 = gcnew ManagedValuableItem(item3Name, item3Value);
    
    Collections::Generic::List<IValuableItem^>^ managedValuableItemList = gcnew Collections::Generic::List<IValuableItem^>();
    managedValuableItemList->Add(item1);
    managedValuableItemList->Add(item2);
    managedValuableItemList->Add(item3);
    ManagedTreasure^ managedTreasure = gcnew ManagedTreasure(managedValuableItemList);
    
    return managedTreasure;
}

Über Marshaling lassen sich unmanaged native C++-Datentypen, etwa std::wstring, in managed Datentypen wie String konvertieren. Mit gcnew erzeugen Sie sogenannte Handles, die mit ^ deklariert werden, auf dem Managed Heap liegen und von der Garbage Collection verwaltet werden.
Der Rückweg von C# nach C++ funktioniert nach einem ähnlichen Prinzip. Allerdings müssen Sie den Speicher der erzeugten C++-Objekte in diesem Fall selbst verwalten.

Wo wir gerade bei Speicherverwaltung sind: Mit dem unsafe-Feature von C# lassen sich auch Pointer direkt durchreichen, wodurch sich Kopiervorgänge einsparen lassen. Das ist insbesondere bei großen Datenmengen sinnvoll. Bei kleineren kann es ebenfalls eingesetzt werden, erfordert dann jedoch erhöhte Sorgfalt, da Pointer-Arithmetik den Code schnell komplexer und wartungsintensiver macht.

Die Weisheiten eines alten Seebären

Und nun zu den eigentlichen Perlen meines langen Berichts. Es handelt sich um eine Sammlung von Erkenntnissen, die sich erst bei der praktischen Umsetzung gezeigt haben und nicht schon beim Studium der Dokumentation. Letztere ist an vielen Stellen übrigens deutlich verständlicher und vollständiger als dieses leicht knurrige Protokoll eines alten Seemanns.

  • Microsoft scheint sehr genau zu wissen, was sie tun. Oder alle anderen wissen es nicht, wenn es um den selbst entwickelten C++/CLI-Dialekt geht. Rider funktioniert grundsätzlich gut, produziert nach Änderungen an Projektdateien jedoch gelegentlich schwer nachvollziehbare Fehler, die Visual Studio problemlos auflöst.
  • Achten Sie genau auf die unterstützten Marshalling-Typen. std::wstring funktioniert zuverlässig, std::string hingegen nicht.
  • Halten Sie sich beim Interface von C# zu C++ mit C#-Sprachfeatures zurück. Primärkonstruktoren oder record-Typen können zu unerwarteten Problemen führen.
  • Eine strikte Trennung zwischen C++/CLI-Headern und normalen C++-Headern ist zwingend erforderlich. Native C++-Projekte dürfen ausschließlich vollständig native Header einbinden. Die Implementierung darf anschließend CLI sein. Ein Header-only-Ansatz ist hier nicht möglich.
  • Nutzen Sie das CLI-Projekt konsequent als Adapter-Schicht und halten Sie es frei von Geschäftslogik.
  • Bewährt hat sich die Verwendung von zwei Konstruktoren. Einer akzeptiert managed Datentypen, der andere unmanaged Datentypen.
  • Legen Sie frühzeitig ein konsistentes Namensschema fest. Häufig existiert derselbe Datentyp in drei Varianten: für C++, für C# und als CLI-Adapter.
  • Nicht jedes Interface sollte eins zu eins übersetzt werden. Was in C# als Vector3 sinnvoll ist, lässt sich in C++ oft einfacher als drei Integer abbilden, ohne dafür einen eigenen Datentyp einzuführen.
  • Treten nach dem Erstellen von Schnittstellen kryptische Compilerfehler auf, prüfen Sie zuerst, ob alle ^ korrekt gesetzt sind. Das ist mir häufiger passiert als ein fehlendes Semikolon in meinen Anfangsjahren.
  • Die Marshalling-Header gehören ganz an den Anfang der Includes. Keine Ausnahme.
  • Denken Sie frühzeitig an Fehlerbehandlung. Ein Error-Callback wird früher oder später unvermeidlich sein.

Am Ende gilt: Wer sich auf diese Reise einlässt, wird C++/CLI nicht lieben müssen, aber verstehen lernen, warum diese Brücke trotz aller Eigenheiten ihren festen Platz hat.

Fazit zu C++/CLI

Mein ehrliches, subjektives Fazit zu C++/CLI lautet: Die Syntax ist gut lesbar, aber schwer zu schreiben, und es gibt immer wieder kleinere Stolpersteine, sowohl in der Funktionalität als auch im Tooling. Mir hat es sehr geholfen, C++/CLI als eigene Programmiersprache zu begreifen und nicht als C++ mit ein paar zusätzlichen Sprachmitteln.

In unserem konkreten Anwendungsfall stellte sich das Kopieren von Daten nicht als Performanceproblem heraus. [2] Die Sprache löst damit am Ende genau das Problem, für das sie geschaffen wurde: Sie ermöglicht den Aufbau einer schmalen, stabilen Schnittstelle zwischen C++ und C#.

Und damit lichtet Käpt’n Blackbyte wieder den Anker und macht sich auf, neue Technologien zu erkunden.

 

Hinweise:

[1] Hier finden Sie die genaue Handelsroute.
[2] Die Technik haben wir auch in einem Kundenprojekt bei der Dr. Sennewald Medizintechnik Gmbh angewandt.

Suchen Sie nach einem Team für Ihre Softwareentwicklung oder -modernisierung? Dann laden Sie sich den t2informatik Steckbrief herunter oder sprechen Sie mit uns über Ihr Projekt.

Wollen Sie als Meinungsmacherin oder Kommunikator über C++/CLI diskutieren? Dann teilen Sie den Beitrag gerne in Ihrem Netzwerk. Und falls Sie sich für weitere Beiträge aus der Welt der Softwareentwicklung interessieren, dann testen Sie unseren beliebten Newsletter.

Jan Kasper hat einen weiteren Beitrag im t2informatik Blog veröffentlicht:

t2informatik Blog: 3D-Visualisierung mit VTK und Avalonia-UI

3D-Visualisierung mit VTK und Avalonia-UI

Jan Kasper
Jan Kasper

Jan Phillip Kasper ist Softwareentwickler. Er startete im Embedded Bereich mit C++ und wechselte später wegen des starken Toolings zu C#. Seine erste technische Liebe gilt den Unit Tests, im Systemtest spürt er mit leiser Schadenfreude skurrile Fehler auf. Und privat? Da baut er mit seinen Söhnen Technikspielzeug und lässt sie gelegentlich sogar selbst damit spielen.

Im t2informatik Blog veröffentlichen wir Beträge für Menschen in Organisationen. Für diese Menschen entwickeln und modernisieren wir Software. Pragmatisch. ✔️ Persönlich. ✔️ Professionell. ✔️ Ein Klick hier und Sie erfahren mehr.