dotnet / runtime

.NET is a cross-platform runtime for cloud, mobile, desktop, and IoT apps.
https://docs.microsoft.com/dotnet/core/
MIT License
15.28k stars 4.74k forks source link

Question: How to access .NET Objects in C++ .NET Core Host code #12904

Closed RickStrahl closed 4 years ago

RickStrahl commented 5 years ago

I'm working on Core CLR Host integration code, where I create a runtime host and then pass back a .NET object to a COM client. However, I can't seem to figure out how to capture the returned object reference effectively and pass back an IDispatch pointer to the host application.

I'm probably not casting/marshalling correctly, but after trying a number of different combinations I can't seem to find the right incantation to make this work.

Apologies for my C++ hackery below - this is not my forte to say the least and I'm basing this on similar code that I've been using for years with Full Framework hosting.

I started with the CoreClr Hosting Sample and then essentially changed out the delegate to call my own .NET assembly that acts as a factory loader. I'm using initializeCoreClr and then a custom Managed Delegate to retrieve the .NET object. The call works - no errors and I can verify that method is called and returning the proper simple value. But I can't seem to pick up the resulting .NET Object in a usable way.

The .NET method I'm calling is very simple and just a factory for a specific class:

        public static uint CreatewwDotnetBridgeByRef([MarshalAs(UnmanagedType.LPStruct)] ref object instance)
        {
            StringUtils.LogString("CreatewwDotnetBridge called", "c:\\temp\\test.text");
            instance = new wwDotNetBridge();
            return 1;
        }

The relevant C++ code using CoreClrLoader is:

typedef DWORD(__stdcall* doWork_ptr)(CComPtr<_ObjectHandle>* ptr);

    // <Snippet5>
    doWork_ptr managedDelegate;

    // The assembly name passed in the third parameter is a managed assembly name
    // as described at https://docs.microsoft.com/dotnet/framework/app-domains/assembly-names
    hr = createManagedDelegate(
        hostHandle,
        domainId,
        "wwDotNetBridge, Version=7.6.0.0, Culture=neutral, PublicKeyToken=null",
        "Westwind.WebConnection.wwDotnetBridgeFactory",
        "CreatewwDotnetBridgeByRef",
        (VOID **)& managedDelegate);
    // </Snippet5>

    if (hr >= 0)
    {
        printf("Managed delegate created\n");
    }
    else
    {
        printf("coreclr_create_delegate failed - status: 0x%08x\n", hr);
        return -1;
    }

    CComPtr<_ObjectHandle> spObjectHandle;
    DWORD result = managedDelegate( (CComPtr<_ObjectHandle> *) &spObjectHandle);

    CComVariant VntUnwrapped;
    spObjectHandle->Unwrap(&VntUnwrapped);

        // this is what I want to end up passing back to the Host Application
        IDispatch * desiredResult = VntUnwrapped.pdispVal;
...

This code fails with an access exception trying to unwrap the value:

image

I can verify that the managedDelegate() call happens as I get the 1 result value and I see the logged entry on disk. I assume the object reference is set as well as I do get a value back in the reference. But the value is always 0x09 which seems like a base interface (IUnknown?). I've also returned the raw pointer (void **) which gives the same result.

image

So my question is this:

How should I cast the .NET object reference so I can capture it in the C++ code and... for bonus points how can I unwrap it properly into an IDispatch pointer value (an DWORD) I can pass back to my host application?

Any help would be appreciated.

danmoseley commented 5 years ago

cc @AaronRobinsonMSFT

AaronRobinsonMSFT commented 5 years ago

@RickStrahl Lots of good questions here. An alternative approach to activating a class is to use COM, which works in .NET Core 3.0 so you can consider that as well. A sample with various options can be found here.

As far as what is going wrong in this example returning a struct from the managed callback and converting it to an IUnknown based interface isn't going to work for a myriad of reasons. Instead I would suggest changing the managed callback to something similar to the following:

public static int Activate([MarshalAs(UnmanagedType.IDispatch)] out object t)
{
    t = null;
    try
    {
        t = new object();
    }
    catch (Exception e)
    {
        return e.HResult;
    }
    return 0;
}

The native side for consumption would be similar to the following:

HRESULT (__stdcall *activate)(IDispatch **) = nullptr;
// Get handle to managed delegate
...

CComPtr<IDispatch> obj;
HRESULT hr = activate(&obj);
if (FAILED(hr))
    throw hr;
RickStrahl commented 5 years ago

@AaronRobinsonMSFT - thank you. So close, yet oh so far :-). I had tried several of the Unmanaged types but totally missed the IUnknown setting (I was looking for IDispatch I guess). IAC it works with this mapping!

Just for reference here's what worked:

public static int CreatewwDotnetBridgeByRef([MarshalAs(UnmanagedType.IUnknown)] ref object instance)
{
    try
    {
        instance = new wwDotNetBridge();
    }
    catch (Exception ex)
    {
        instance = null;
        return ex.HResult;
    }

    return 0;
}

Notice I'm using IUnknown as you orginally had it - in .NET Core 3.0 Preview 6 I don't see an UnmanagedType.IDispatch.

Here's the native code (with the rest of the code from the CoreClrHostExample):

typedef HRESULT(__stdcall* createWwDotnetBridgeHandler)(CComPtr<IUnknown> * ptr);
    createWwDotnetBridgeHandler createWwDotnetBridge;

    // The assembly name passed in the third parameter is a managed assembly name
    // as described at https://docs.microsoft.com/dotnet/framework/app-domains/assembly-names
    hr = createManagedDelegate(
        hostHandle,
        domainId,
        "wwDotNetBridge, Version=7.6.0.0, Culture=neutral, PublicKeyToken=null",
        "Westwind.WebConnection.wwDotnetBridgeFactory",
        "CreatewwDotnetBridgeByRef",
        (VOID **)& createWwDotnetBridge);
    // </Snippet5>

    if (hr >= 0)
    {
        printf("Managed delegate created\n");
    }
    else
    {
        printf("coreclr_create_delegate failed - status: 0x%08x\n", hr);
        return -1;
    }

    CComPtr<IUnknown> obj;  
    DWORD result = createWwDotnetBridge((CComPtr<IUnknown> *) &obj);

    CComPtr<IDispatch> disp;
    hr = obj->QueryInterface(&disp);
    if (FAILED(hr))
        throw hr;

    // this is what I'm after
    IDispatch* p = disp;

    // Verifying COM instance: Call Com instance
    CComVariant result2;

    CComVariant parm1;
    parm1.bstrVal = CComBSTR(L"Test");
    hr = disp.Invoke1(L"Ping",&parm1,&result2);
    if (FAILED(hr))
        throw hr;