Bridge code chronicle or a C++/CLI experience report

by | 24.01.2026

Ahoy, landlubber. I am Captain Blackbyte. I am not your average software developer who just wants to see if he can publish a blog entry like this in a professional digital harbour.

I’m going to tell you about my time on the sometimes stormy waters of the CLI/C++ sea. This lies between the comparatively safe harbour of C#, where graphical interfaces flash and you hardly have to worry about lost gold coins in the form of memory leaks, and the rough seas of C++, where legendary ships such as OpenSSL, VTK and GLM have been sailing for decades.

This is not a sailor’s yarn, but an honest chronicle of how to navigate between these two worlds, even when the waves are high. So listen carefully and benefit from the experiences, and yes, also from the mistakes I made on this journey.

Inland waterway transport with platform invocation services

If you only want to transport a few gold ducats, it’s best to stick to the old pirate saying, ‘A kiss on the sails keeps the wind favourable.’ Also known as KISS, ‘keep it simple and stupid.’ In this case, use P/Invoke, i.e. platform invocation services.

This involves calling a C++ function directly as unmanaged code. However, this only works for simple data types. In these cases, however, it is much less complicated than having to throw entire tea chests overboard.

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}");
    }}

Trading complex, composite treasures

But what if you have a treasure chest containing items of varying value and you want to transport the entire chest at once instead of loading each item individually?

This is exactly what C++/CLI is for, a Microsoft-specific dialect that extends C++ and bridges the gap between executable C++ code and .NET code. (Incidentally, this also works with F# or VB. However, an old pirate prefers to tell his stories in a straightforward manner, so we’ll stick with C# here.

The trade route in this example looks like this [1]:

Trade route in the C++/CLI project

Figure 1: Trade route in the C++/CLI project

InterfaceCLI and InterfaceBackCpp use the Microsoft C++ compiler with the /clr switch, allowing us to use CLI syntax in them.

In our native C++ application, we can generate complex data (ironically, the data is kept very simple as an example):

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;}

First, we generate some sample data and pass it to the InterfaceCLI. Since the call is made from native C++, the header used must be pure C++ and must not contain any CLI components.

However, it ensures that the corresponding class can be found in the library. I will deliberately refrain from giving a detailed explanation of how declspec makes this possible in the background at this point. I would like to draw your attention to the actual story.

In addition, the header contains an object that allows access to the functionalities of the C# side.

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

Now remove both eye patches and observe how we utilise CLI syntax:

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;
}

Marshaling can be used to convert unmanaged native C++ data types, such as std::wstring, into managed data types such as String. With gcnew, you can create so-called handles, which are declared with ^, reside on the managed heap and are managed by garbage collection.

The reverse process from C# to C++ works on a similar principle. However, in this case, you must manage the memory of the created C++ objects yourself.

While we’re on the subject of memory management: C#’s unsafe feature also allows pointers to be passed directly, which saves on copying operations. This is particularly useful for large amounts of data. It can also be used for smaller amounts, but requires greater care, as pointer arithmetic quickly makes the code more complex and maintenance-intensive.

The wisdom of an old sea dog

And now to the real gems of my lengthy report. This is a collection of insights that only became apparent during practical implementation and not while studying the documentation. Incidentally, the latter is much clearer and more complete in many places than this slightly grumpy logbook of an old sailor.

  • Microsoft seems to know exactly what they are doing. Or everyone else is clueless when it comes to the C++/CLI dialect they developed themselves. Rider works well in principle, but occasionally produces errors that are difficult to trace after changes to project files, which Visual Studio resolves without any problems.
  • Pay close attention to the supported marshalling types. std::wstring works reliably, but std::string does not.
  • When using the C# to C++ interface, refrain from using C# language features. Primary constructors or record types can lead to unexpected problems.
  • A strict separation between C++/CLI headers and normal C++ headers is essential. Native C++ projects may only include completely native headers. The implementation may then be CLI. A header-only approach is not possible here.
  • Use the CLI project consistently as an adapter layer and keep it free of business logic.
  • The use of two constructors has proven successful. One accepts managed data types, the other unmanaged data types.
  • Establish a consistent naming scheme early on. Often, the same data type exists in three variants: for C++, for C# and as a CLI adapter.
  • Not every interface should be translated one-to-one. What makes sense as Vector3 in C# can often be mapped more easily in C++ as three integers without introducing a separate data type.
  • If cryptic compiler errors occur after creating interfaces, first check whether all ^ are set correctly. This happened to me more often than a missing semicolon in my early years.
  • The marshalling headers belong at the very beginning of the includes. No exceptions.
  • Think about error handling early on. An error callback will be inevitable sooner or later.

In the end, those who embark on this journey will not have to love C++/CLI, but they will learn to understand why this bridge has its place despite all its peculiarities.

Conclusion on C++/CLI

My honest, subjective conclusion on C++/CLI is: The syntax is easy to read but difficult to write, and there are always minor stumbling blocks, both in terms of functionality and tooling. It helped me a lot to understand C++/CLI as a separate programming language and not as C++ with a few additional language features.

In our specific use case, copying data did not prove to be a performance problem. [2] In the end, the language solves exactly the problem for which it was created: it enables the construction of a narrow, stable interface between C++ and C#.

And so Captain Blackbyte weighs anchor again and sets off to explore new technologies.

 

Notes:

[1] Here you will find the exact trade route.
[2] We also used this technology in a customer project at Dr. Sennewald Medizintechnik GmbH.

Are you looking for a team for your software development or modernisation? Then talk to us about your project.

Would you like to discuss C++/CLI as an opinion leader or communicator? Then feel free to share this article in your network.

Jan Kasper has published another article on the t2informatik Blog:

t2informatik Blog: 3D visualisation with VTK and Avalonia UI

3D visualisation with VTK and Avalonia UI

Jan Kasper
Jan Kasper

Jan Phillip Kasper is a software developer. He started out in the embedded sector with C++ and later switched to C# because of the powerful tooling. His first technical love is unit testing, and he takes quiet delight in tracking down bizarre errors in system testing. And in his private life? He builds technical toys with his sons and even lets them play with them occasionally.

In the t2informatik Blog, we publish articles for people in organisations. For these people, we develop and modernise software. Pragmatic. ✔️ Personal. ✔️ Professional. ✔️ Click here to find out more.