Login

Javascript must be enabled to download our products and perform other essential functions on the website.

ionicons-v5-m
ionicons-v5-j
Buy Now Download Free Trial
ionicons-v5-m
ionicons-v5-f

Unmanaged C++ calling managed C++ or other .NET components

After having worked MANY hours on getting a solid solution for this, I thought I'd document it for someone else's benefit. This isn't formatted pretty, but hopefully it conveys information. There are a lot of gotchas which I'll attempt to point out.

There are a few options:

  • COM Callable Wrappers (CCW) - You call regasm on your .NET component which creates a prodID/GUID for your component, and then you communicate with it via standard COM (CoCreateInstance, etc)
  • Close to the above, there is registration free COM wrappers (can't remember exactly what they're called now). They only work on XP SP2, Win 2003 and higher though.
  • Flat file interface - You create your managed C++ component as a 'mixed mode' DLL, and have an exported function which your unmanaged C++ app calls after doing LoadLibrary and GetProcAddress. This works great, until you run into the dreaded 'loader lock' / 'mixed-mode deadlock' problem. Then you're hosed. Seriously, no one should take this approach if you're using .NET 1.1! Apparently it's been fixed in .NET 2.0
  • Make the managed C++ DLL a pure managed component. Host the CLR yourself and interact with the component via IDispatch.

That last option is the one I'll discuss since it seems to work anywhere the CLR works and doesn't have the DLL-hellishness of COM-registered components.

Hosting the CLR is actually quite simple. Google for CorBindToRuntimeEx and ICorRuntimeHost and you'll have what you need.

Next you'll want to get an IDispatch pointer to your .NET object. You'll be working with what feels like COM, smells like COM and looks like COM, but from all the stack traces I looked like, it is completely implemented in .NET--which really means you'll be working with interfaces.

To get an IDispatch to your .NET object, you first get an IDispatch pointer to the app domain:

#include <mscoree.h>
#include <atlbase.h>
#include <comutil.h>
CComPtr<_AppDomain> gDefAppDomain;
CComPtr<IUnknown> pUnknown;
//Retrieve the IUnknown default AppDomain
gCORRuntimeHost->GetDefaultDomain(&pUnknown);
pUnknown->QueryInterface(&gDefAppDomain.p);

Now it's time to create your internal object. For example, consider the following managed C++ object (NOTE: This technique works equally well on C# or VB.NET classes, not just on C++/CLI classes):

namespace MyObjNameSpace
{
   public __gc class CreateMyObjClass
    {

        //No whimping out on the easy param types in this example!! :)
        bool MyObjMethod(String* param1, int param2, [Out] String** retVal)
        ...

You need to get a pointer to the object, and then some way to call MyObjMethod.

CComPtr<_ObjectHandle> gpMyObjObjectHandle;
CComPtr<IDispatch> gpMyObjDisp;
gDefAppDomain->CreateInstance(_bstr_t(L"MyObjAssembly"), _bstr_t(L"MyObjNameSpace.CreateMyObjClass"), &gpMyObjObjectHandle);
CComVariant VntUnwrapped;
gpMyObjObjectHandle->Unwrap(&VntUnwrapped);
if (VntUnwrapped.vt != VT_DISPATCH)
{
    //something bad happened!--see below
}
else
    gpMyObjDisp = VntUnwrapped.pdispVal;

A couple of things to note (pay attention to this!):

  • You MUST pass in real BSTRs, not just wchar_t* strings which you could often get away with in COM
  • If you forget the __gc on the CreateMyObjClass above, the returned VntUnwrapped will be of type VT_RECORD (I suppose because since it isn't a __gc class, .NET considers it a struct??)
  • If you forget public on CreateMyObjClass above, you'll get back an IUnknown, not a IDispatch (since it isn't public, you can't call any methods anyway??)

Moving on, now you'll want to call a method on your object. Here is a basic function you can start with:

bool CallManagedFunction(CComPtr<IDispatch> pDisp, LPCTSTR szMethodName,
int iNoOfParams, VARIANT * pvArgs, VARIANT * pvRet)
{
DISPID dispid = 0;
_bstr_t bsMethodName = szMethodName;
LPOLESTR pbsMethodName = bsMethodName;

pDisp->GetIDsOfNames (IID_NULL, &pbsMethodName, 1, LOCALE_SYSTEM_DEFAULT, &dispid);

DISPPARAMS dispparamsArgs = {NULL, NULL, 0, 0};
dispparamsArgs.cArgs = iNoOfParams;
dispparamsArgs.rgvarg = pvArgs;

EXCEPINFO excep = {0};
UINT badArg = 0;

//Invoke the method on the Dispatch Interface
hr = pDisp->Invoke(dispid, IID_NULL, LOCALE_SYSTEM_DEFAULT, DISPATCH_METHOD, &dispparamsArgs, pvRet, &excep, &badArg);
//do something with return code...
}

Some more hard-won hints:

  • pvArgs are passed in in REVERSE order! I'll show a calling example below. If you forget this, the marshaler will have trouble marshaling (in our example above it will try to marshal a native string to a managed int and vice versa. You'll probably get E_NOINTERFACE back as a return code (cryptic! It took me 10 hours to finally track that one down!)
  • Again, use real BSTRs, not just LPOLESTR (_wchar_t*)

An example of calling our method above would be:

VARIANT varArgs[3];
ZeroMemory(varArgs, sizeof(varArgs));

BSTR returnValue = NULL; //remember to SysFreeString this when it is returned
VariantInit(&varArgs[0]);
varArgs[0].vt = VT_BSTR | VT_BYREF;
varArgs[0].pbstrVal = &returnValue; //this is the out param in our example .NET method above

VariantInit(&varArgs[1]);
varArgs[1].vt = VT_I4;
varArgs[1].intVal = myIntValue;

VariantInit(&varArgs[2]);
varArgs[2].vt = VT_BSTR;
_bstr_t bs = myLPCTSTRValue;
varArgs[2].bstrVal = bs;

VARIANT varRet; //this will hold the return code (the bool in our .NET method)
VariantInit(&varRet);

CallManagedFunction(gpMyObjDisp, _T("MyObjMethod"), 3, varArgs, &varRet)

REMEMBER REMEMBER REMEMBER that params are passed in reverse order!

Other notes: I read somewhere that the main thread should be pumping messages so finalizers can get called from the garbage collector. Not sure if that's true or not. I also read you don't need to do any kind of CoInitialize, but I did just for good measure :) I THINK you can use the IDispatch interface from any thread--I read that the CLR will take care of all marshalling for you (I'm now always calling it from one thread--part of the pain from trying to figure out why I was getting E_NOINTERFACE).

Best of luck!

Wow, you guys are GOOD! Go get yourselves some lattes or something. :-) I won't be able to try the update for a little while, but the effort alone makes me smile.

Jeremiah B., FLEXcon, USA ionicons-v5-b