Call C# code from your legacy C++ code
For many decades, folks have been quite productive in creating C++ code. These “legacy” apps have been quite useful. However, as most software developers know, software needs to be maintained. Business needs change, and the applications need to change with it. Sometimes a complete rewrite of an application is needed.
Writing code in a managed language like C# or VB is much more productive than C++. Just managing character strings is quite difficult in C++: there are so many string types, like CString, BSTR, WCHAR *, char *, etc. Creating complex custom controls, like TreeViews and TabControls is much more difficult in C++. Using more modern inter-application communications, like Windows Communications Foundation, requires managed code.
Here’s how you can modify your native C++ application with a few lines of code to allow the managed code world to interop with your legacy application
In a prior post (Use reflection from native C++ code to run managed code) I showed how to start the CLR and run arbitrary managed code, using System.Reflection from C++ to get information on the C# code.
Here, we’ll start the CLR and run much simpler code to just call a managed method
File->New->Project->C++ Win32 Console Application “StartClr”
Accept all defaults in the Win32 Application Wizard and click Finish.
Add “mscoree.lib” to the linker additional dependencies:
Right click on the Solution node in Solution Explorer->Add New Project->C# Class Library “ClassLibrary1”
(or you could use an existing C# EXE or DLL that you may have around)
Paste in the C# code below
Now we want the build to output the ClassLibrary1.dll to the same folder as the StartClr.exe. To do this, double click on the ClassLibrary1 Properties node, change the Build Output Path to “..\Debug\” (or you can hit the Browse Button to find the folder. The path will be converted to a relative path if possible). Similarly for release builds.
Now hit F10 to build and single step into the program
To enable managed debugging as well, right click on StartClr in Solution Explorer, Properties. Change DebuggerType from “Auto” to “Mixed”
Then you can set a breakpoint in the managed code too.
If you get an error code, like 0x80070002, use the Error Lookup tool: Menu->Tools->Error Lookup
I tried altering the signature of the managed method, to see what error I got: 0x80131513
Error Lookup didn’t find it.
So I did a Find in the C++ Include directories for 0x80131513. Still not found. I changed it to search for 80131513, 131513, 1513
Then I found what I was looking for: A missing method exception
C:\Program Files (x86)\Windows Kits\8.1\Include\um\CorError.h(722):#define COR_E_MISSINGMETHOD EMAKEHR(0x1513)
BTW, you can put a folder path in the “Look In” field too:
C:\Program Files (x86)\Windows Kits\8.1\Include\um\corprof.h
Points to Ponder:
· What if the managed methods are not marked “Public”: can C++ code call private C# code?
· Note that the code is marked “extern”C” dllExport. Why is that? Hint: see Create managed Tests for native code
· What if I want to make the managed code do more than just return an integer? You can actually make this an extremely powerful starting point to allow C++ call C# code Coming in a future post.
See also:
About 9 years ago, starting the CLR was a little different: Host the CLR and Generate IL to call a MessageBox
<C++ code>
// StartClr.cpp : Defines the entry point for the console application.
//
#include "stdafx.h"
#include <metahost.h>
#include <atlbase.h>
#include <atlcom.h>
#define IfFailRet(expr) { hr = (expr); if(FAILED(hr)) return (hr); }
#define IfNullFail(expr) { if (!expr) return (E_FAIL); }
extern "C" int __declspec(dllexport) CALLBACK CallClrMethod(
const WCHAR *AssemblyName,
const WCHAR *TypeName,
const WCHAR *MethodName,
const WCHAR *args,
LPDWORD pdwResult
)
{
int hr = S_OK;
CComPtr<ICLRMetaHost> spHost;
hr = CLRCreateInstance(CLSID_CLRMetaHost, IID_PPV_ARGS(&spHost));
CComPtr<ICLRRuntimeInfo> spRuntimeInfo;
CComPtr<IEnumUnknown> pRunTimes;
IfFailRet(spHost->EnumerateInstalledRuntimes(&pRunTimes));
CComPtr<IUnknown> pUnkRuntime;
while (S_OK == pRunTimes->Next(1, &pUnkRuntime, 0))
{
CComQIPtr<ICLRRuntimeInfo> pp(pUnkRuntime);
if (pUnkRuntime != nullptr)
{
spRuntimeInfo = pp;
break;
}
}
IfNullFail(spRuntimeInfo);
BOOL bStarted;
DWORD dwStartupFlags;
hr = spRuntimeInfo->IsStarted(&bStarted, &dwStartupFlags);
if (hr != S_OK) // sometimes 0x80004001 not implemented
{
spRuntimeInfo = nullptr;
hr = spHost->GetRuntime(L"v4.0.30319", IID_PPV_ARGS(&spRuntimeInfo));
bStarted = false;
}
CComPtr<ICLRRuntimeHost> spRuntimeHost;
IfFailRet(spRuntimeInfo->GetInterface(CLSID_CLRRuntimeHost, IID_PPV_ARGS(&spRuntimeHost)));
if (!bStarted)
{
hr = spRuntimeHost->Start();
}
hr = spRuntimeHost->ExecuteInDefaultAppDomain(
AssemblyName,
TypeName,
MethodName,
args,
pdwResult);
return hr;
}
int _tmain(int argc, _TCHAR* argv[])
{
DWORD dwResult;
HRESULT hr = CallClrMethod(
L"ClassLibrary1.dll", // name of DLL (can be fullpath)
L"ClassLibrary1.Class1", // name of managed type
L"ManagedMethodCalledFromExtension", // name of static method
L"some args",
&dwResult);
return 0;
}
</C++ code>
And the C# code:
<C# Code>
namespace ClassLibrary1
{
public class Class1
{
/// <summary>
/// a managed static method that gets called from native code
/// </summary>
public static int ManagedMethodCalledFromExtension(string args)
{
// need to return an integer: the length of the args string
return args.Length;
}
}
}
</C# Code>