.NET to C++ Bridge
Most people have encountered the need for interoperability between managed and unmanaged code. There are plenty of patterns and tutorials which explain every detail of writing managed code which can call into unmanaged code. The techniques we can use, from most common to least explored, are:
- Straightforward P/Invoke (static extern, [DllImport], sprinkle a [MarshalAs] or two and we're set - there are even tools to help);
- COM interop (import the type library and you're good to go);
- C++/CLI wrapper class;
- Calling unmanaged function directly (CALLI instruction with Reflection.Emit).
The opposite way around, however, is something many people struggle with because it's not as sexy and common. How can we call new managed code from our old legacy native code? Well, there are again several ways to do it:
- Reverse P/Invoke (has to start from .NET delegate passed as callback, so this is only good if the "action" begins in your .NET code);
- COM interop (every .NET class can also be a COM object, with or without explicit interfaces);
- C++/CLI wrapper classes.
What I want to focus on is the third approach - generating C++/CLI wrapper classes to allow pure unmanaged code interaction with our managed code. This has to be done in a tiered approach, because there's no direct way for native C++ code to call into managed code. What we need to go through is the following mechanical process:
- Open a C++/CLI class library project and change the settings so it generates an import lib (under Linker->Output);
- Write a C++ class (not a .NET reference/value type, i.e. not a ref/value class) which wraps the methods of the original .NET class. This means that this C++ class has to be compiled to IL, contain a reference to the .NET object (using the gcroot<> template) and delegate all calls to the .NET object.
- Write a native C++ class (using #pragma unmanaged, so it's not compiled to IL) which wraps the IL bridge written in step 2 and delegates all calls to it.
That last C++ class will also be decorated with __declspec(dllexport), so we can use the resulting class library as a normal native DLL. Note that marshaling decisions (converting unmanaged types to managed types and vice versa) are made at the IL bridge class, which is aware of both unmanaged and managed code.
This flow seems very complicated and might also appear to have a negative effect on performance. However, realistically, even though we have a complex flow, most of the path is simple delegation and therefore a good candidate for inlining. For example, if we have a C# class A, IL bridge B, native C++ class C and a native client D, we're likely to have two extra function calls only: D->C (because of DLL boundaries, but if D lives in the same DLL as C, inlining is likely again; or if PGO is employed, inlining is an option), and then C->B performs the unmanaged to managed transition (which is most of the cost anyway). After that, the code in the IL bridge is likely to be inlined with the original .NET class if the method on that class is small.
These steps are highly mechanical and annoyingly similar across various classes, so I wanted to see if I can devise an automatic tool for generating these wrappers. It seems fairly simple once you have a good code generation framework in place; without one, I was able to bake some sample-quality code which takes a managed type and wraps it with an IL bridge and a native C++ class. It lacks in many areas (such as support for recursively converting structures and other non-primitive types), but I still decided to attach it because I am not at all sure if I will have the time to wrap it up. So if anyone feels like picking it up from here, or contributing parts of the work, it would be great.
Without further ado, here's a piece of sample output from the tool. With the following class in place:
public class Calculator
{
public int Add(int first, int second)
{
return first + second;
}
public string FormatAsString(float i)
{
return i.ToString();
}
}
Here's the IL bridge generated for this class:
#pragma once
#pragma managed
#include <vcclr.h>
class ILBridge_CppCliWrapper_Calculator {
private:
//Aggregating the managed class
gcroot<CppCliWrapper::Calculator^> __Impl;
public:
ILBridge_CppCliWrapper_Calculator() {
__Impl = gcnew CppCliWrapper::Calculator;
}
int Add(int first, int second) {
System::Int32 __Param_first = first;
System::Int32 __Param_second = second;
System::Int32 __ReturnVal = __Impl->Add(__Param_first, __Param_second);
return __ReturnVal;
}
wchar_t* FormatAsString(float i) {
System::Single __Param_i = i;
System::String __ReturnVal = __Impl->FormatAsString(__Param_i);
wchar_t* __MarshaledReturnVal = marshal_to<wchar_t*>(__ReturnVal);
return __MarshaledReturnVal;
}
};
And here's the native exported header and source for the class. Note that the exported header is callable by any C++ client - that C++ client doesn't have to be compiled with /CLR or even know what .NET is.
//This is the .h file
#pragma once
#pragma unmanaged
#ifdef THISDLL_EXPORTS
#define THISDLL_API __declspec(dllexport)
#else
#define THISDLL_API __declspec(dllimport)
#endif
//Forward declaration for the bridge
class ILBridge_CppCliWrapper_Calculator;
class THISDLL_API NativeExport_CppCliWrapper_Calculator {
private:
//Aggregating the bridge
ILBridge_CppCliWrapper_Calculator* __Impl;
public:
NativeExport_CppCliWrapper_Calculator();
~NativeExport_CppCliWrapper_Calculator();
int Add(int first, int second);
wchar_t* FormatAsString(float i);
};
//This is the .cpp file
#pragma managed
#include "ILBridge_CppCliWrapper_Calculator.h"
#pragma unmanaged
#include "NativeExport_CppCliWrapper_Calculator.h"
NativeExport_CppCliWrapper_Calculator::NativeExport_CppCliWrapper_Calculator() {
__Impl = new ILBridge_CppCliWrapper_Calculator;
}
NativeExport_CppCliWrapper_Calculator::~NativeExport_CppCliWrapper_Calculator()
{
delete __Impl;
}
int NativeExport_CppCliWrapper_Calculator::Add(int first, int second) {
int __ReturnVal = __Impl->Add(first, second);
return __ReturnVal;
}
wchar_t* NativeExport_CppCliWrapper_Calculator::FormatAsString(float i) {
wchar_t* __ReturnVal = __Impl->FormatAsString(i);
return __ReturnVal;
}
The very preliminary sample code used to generate these classes can be downloaded from
here as a Visual Studio 2005 solution. If you play with it please let me know.