Pure Native C++ consuming .NET classes without COM registration
One of the problems that many C++ native developers face when interacting with .NET is the inability to load a .NET COM Dll and use the COM classes without registering the DLL. I faced this problem when adapting the debugging NetExt from pure C++ to pure C++/.NET. As the idea of an extension is that you just drop it in WinDBG folder and use it, requiring COM class registration in registry for both 32 and 64-bits is a non-starter. As I am using ExtCpp Debugger API, using managed C++ does not work (yes, I tried).
This post will walk you through creating a project where a native C++ calls .NET leveraging the power of object orientation provided by COM. To do so, open Visual Studio (any version) and choose New Project. Make sure you choose a class library. This will be our COM DLL.
Add a reference to System.Windows.Form and remove all unused references, References should look like this:
Change the complete code from Class1.cs to:
using System;
using System.Runtime.InteropServices;
using System.Windows.Forms;
namespace ContosoCom
{
[ComVisible(true)]
[System.Runtime.InteropServices.InterfaceTypeAttribute(System.Runtime.InteropServices.ComInterfaceType.InterfaceIsIUnknown)]
public interface IMyClass
{
void DisplayMessageBox([MarshalAs(UnmanagedType.BStr)] string Text);
void GetTicksAndDate([Out] out MyStruct Structure);
}
[ComVisible(true)]
[StructLayout(LayoutKind.Sequential, Pack = 8)]
public struct MyStruct
{
public long TicksOfNow;
public int Day;
public int Month;
public int Year;
}
[ComVisible(true)]
[ClassInterface(ClassInterfaceType.None)]
[ComDefaultInterface(typeof(IMyClass))]
public class MyClass : IMyClass
{
public void DisplayMessageBox(string Text)
{
MessageBox.Show(Text);
}
public void GetTicksAndDate(out MyStruct Structure)
{
Structure.TicksOfNow = DateTime.Now.Ticks;
Structure.Day = DateTime.Now.Day;
Structure.Month = DateTime.Now.Month;
Structure.Year = DateTime.Now.Year;
}
}
}
In order to generate the type library, select the project on Solution Explorer, right-click and choose properties, make sure you include these commands in Post Build Events (change folder “C:\Program Files (x86)\Microsoft SDKs\Windows\v8.1A\bin\NETFX 4.5.1 Tools” to match your API folder):
cd $(TargetDir)
"C:\Program Files (x86)\Microsoft SDKs\Windows\v8.1A\bin\NETFX 4.5.1 Tools"\tlbexp.exe $(TargetPath)
echo xcopy /d /y $(TargetDir)*.* $(SolutionDir)$(PlatformName)\$(ConfigurationName)\
xcopy /d /y $(TargetDir)*.* $(SolutionDir)$(PlatformName)\$(ConfigurationName)\
Build the project and look at the output window on Visual Studio (Ctrl+W,O if not visible) because the destination folder of the type library is important to our C++ application.
This is the line of interest:
Assembly exported to 'C:\projects\ContosoCom\ContosoCom\bin\Debug\ContosoCom.tlb'
Now add a new Visual C++ Win32 Console Application project to the existing solution:
In the Wizard leave all options as default and the choose Finish. Now using the information obtained in the Output Window, make sure your code looks like this
// CppTestContoso.cpp : Defines the entry point for the console application.
//
#include "stdafx.h"
#import "C:\projects\ContosoCom\ContosoCom\bin\Debug\ContosoCom.tlb" auto_rename
using namespace ContosoCom;
int _tmain(int argc, _TCHAR* argv[])
{
IMyClass *mc = NULL;
HINSTANCE hDLL = 0;
// load the DLL
hDLL = ::LoadLibrary(L"ContosoCom.dll");
::CoInitialize(NULL);
if(!hDLL)
{
printf("ERROR: Unable to load library ContosoCom.dll\n");
return -1;
}
//
// TO DO: Add code here to get an instance of MyClass
//
// At this point we do not have how to get a pointer even with the libray loaded
// End of TO DO
if(!mc)
{
printf("ERROR: Unable to get a pointer for MyClass\n");
return -1;
}
mc->DisplayMessageBox("Hello World from native to .NET without registration");
MyStruct st;
ZeroMemory(&st, sizeof(MyStruct));
printf("SUCCESS: Leaving gracefully\n");
return 0;
}
I believe this is the point everyone stops. I conferred with my colleague Earl Beaman who is the expert on COM/COM+ and he told me that .NET DLLs do not export the necessary method to make the interfaces discoverable, actually they do not export any method as they are not platform specific (bitnesswise). What he did not know is that by giving me this explanation he gave me the response. What if I exported a method to get me the interface (or interfaces)? .NET offers DLLImport to call native code, however there is no DLLExport.
As a matter of fact there is a NuGet developed by the talented Robert Giesecke that does exactly that: it enables a .NET Dll to export methods. Don’t take this lightly, DllExport is an amazing piece of software. As one could expects, it requires the .NET library to compiled choosing a CPU (x86 or x64). So in order to resolve the issue, let’s add DellExport NuGet to our C# project. Select ContosoCom project, go to properties and change the processor target to x86 (you can do the same for 64 bits):
Using Package Manager Console (Tools | NuGet Package Manager | Package Manager Console), run:
Install-Package UnmanagedExports
Let’s create a static class and static method to return the pointer to an instance of MyClass to C++. This is the final code:
using System;
using System.Runtime.InteropServices;
using System.Windows.Forms;
using RGiesecke.DllExport;
namespace ContosoCom
{
public static class Exports
{
[DllExport(CallingConvention = CallingConvention.Cdecl)]
public static void GetClass([Out] [MarshalAs((UnmanagedType.Interface))] out IMyClass pInterface)
{
pInterface = new MyClass();
}
}
[ComVisible(true)]
[System.Runtime.InteropServices.InterfaceTypeAttribute(System.Runtime.InteropServices.ComInterfaceType.InterfaceIsIUnknown)]
public interface IMyClass
{
void DisplayMessageBox([MarshalAs(UnmanagedType.BStr)] string Text);
void GetTicksAndDate([Out] out MyStruct Structure);
}
[ComVisible(true)]
[StructLayout(LayoutKind.Sequential, Pack = 8)]
public struct MyStruct
{
public long TicksOfNow;
public int Day;
public int Month;
public int Year;
}
[ComVisible(true)]
[ClassInterface(ClassInterfaceType.None)]
[ComDefaultInterface(typeof(IMyClass))]
public class MyClass : IMyClass
{
public void DisplayMessageBox(string Text)
{
MessageBox.Show(Text);
}
public void GetTicksAndDate(out MyStruct Structure)
{
Structure.TicksOfNow = DateTime.Now.Ticks;
Structure.Day = DateTime.Now.Day;
Structure.Month = DateTime.Now.Month;
Structure.Year = DateTime.Now.Year;
}
}
}
Using dumpbin, we see that GetClass method is exported:
In the C++ application build I will copy the newest version of the C# dll based on the output of the compilation. In my example this is the command:
xcopy /y C:\projects\ContosoCom\ContosoCom\bin\Debug\ContosoCom.dll $(TargetDir)
This is the final C++ code:
// CppTestContoso.cpp : Defines the entry point for the console application.
//
#include "stdafx.h"
#import "C:\projects\ContosoCom\ContosoCom\bin\Debug\ContosoCom.tlb" auto_rename
using namespace ContosoCom;
typedef void(*pGetClass)(IMyClass **iMyClass);
int _tmain(int argc, _TCHAR* argv[])
{
pGetClass getClass = NULL;
IMyClass *mc = NULL;
HINSTANCE hDLL = 0;
// load the DLL
hDLL = ::LoadLibrary(L"ContosoCom.dll");
::CoInitialize(NULL);
if(!hDLL)
{
printf("ERROR: Unable to load library ContosoCom.dll\n");
return -1;
}
//
// TO DO: Add code here to get an instance of MyClass
//
getClass = (pGetClass)GetProcAddress(hDLL, "GetClass");
if(!getClass)
{
printf("ERROR: Unable to find entry for GetClass()\n");
return -1;
}
getClass(&mc);
// At this point we do not have how to get a pointer even with the libray loaded
// End of TO DO
if(!mc)
{
printf("ERROR: Unable to get a pointer for MyClass\n");
return -1;
}
mc->DisplayMessageBox("Hello World from native to .NET without registration");
MyStruct st;
ZeroMemory(&st, sizeof(MyStruct));
mc->GetTicksAndDate(&st);
printf("Ticks %I64i\n",st.TicksOfNow);
printf("Today is %i/%i/%i\n",st.Month,st.Day,st.Year);
printf("SUCCESS: Leaving gracefully\n");
return 0;
}
Download project here: .NET COM no registration
Comments
- Anonymous
December 16, 2016
Excellent article, thank you very much for sharing this. I already have tested your example and truly it works. I'm having an issue trying to replicate this for a project, I hope you can help me. I'm trying to consume from C# side some other personalized dlls also written in C# and then send the result of those calls to C++, everything is being compiled correctly but at the time of run the C++ program I'm getting "Memory Allocation Errors".I have noticed that even only instantiating an object of those dlls the error is thrown.Do you know if I'm missing some directives for doing this? Thanks in advanced.- Anonymous
December 19, 2016
Gabriel,Check if the bitness of the C++ application matches the bitness you are compiling your C# (do not use MSIL). Verify if the marshalling is correct. Start with simple types. In my example here notice that I am using ANSI characters for strings, you may need to use LPWSTR marshalling if you are using wide-characters in C++ (wchar_t*)
- Anonymous