Passing an object from C++ to C# though COM

2019-06-28 03:07发布

问题:

hi have a COM visible API in C# which looks like the following:

public void DoSomething(string par1, string par2, object additionalParameter)

The idea is that based on the value of the string parameters I expect a different class as the third parameter and cast it appropriately in the implementation (I know this design is not optimal but I don't have much flexibility here).

Suppose that for some combination of the string parameters the type of the additional parameter is the following:

[ComVisible(true)]
[Serializable]
public class TestObject    
{
    public string String{ get; set; }

    public long Long { get; set; }
}

I need to my API method from some unmanaged code; however I am having difficulties in creating the a proper variant needed for the third parameter.

I am using the CComVariant(...) passing an IDispatch pointing to a TestObject that I have just built.

Suppose that pTestObject is an IDispatch pointer to my TestObject, I have something like the following:

CComVariant pObjectVariant(pTestObject);
DoSomething(BSTR(L"first"), BSTR(L"second"), pObjectVariant);

However, when the C# function is finally invoked, I see that the object has type bool instead of TestObject that I was expecting.

Any idea?

Stefano

回答1:

I have a couple of suggestions. First of all, create an interface for anything that you touch in COM, even if its just a bog-standard DTO that has no methods and only properties. COM loves interfaces. It loves them so much, that everything you touch in COM is an interface.

The other suggestion is that you place a GuidAttribute on anything that you touch in COM. This will ensure that your registry doesn't get crapped up when you register the managed assembly with COM. COM loves GUIDs more than it loves interfaces, and it gets confused easily if an interface is registered to more than one GUID, which can happen if you don't hard-fix the interface GUIDs in code. Concrete classes also need a GUIDAttribute.

I know it sucks, but that's why MS is trying so hard to get people away from using COM.

That being said, you probably want C# like this:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Runtime.InteropServices;

namespace ClassLibrary1
{

    [ComVisible(true)]
    [Guid("1CEB24F2-BF13-417F-B5BC-DB8E82A56EAE")]
    public interface ITestEntity1 //This is how TestEntity1 is visible to the COM code...so it needs a Guid.
    {
        bool Thing { get; set; }
    }


    [ComVisible(true)]
    [Guid("C8B5A7C2-F67C-4271-A762-3642754F2233")]
    public class TestEntity1 : ITestEntity1  //Created by the COM runtime...needs a Guid.
    {
        public bool Thing { get; set; }
    }

    [ComVisible(true)]
    [Guid("8904A7EC-D865-4533-91EC-1F68524651D0")]
    public interface ITestEntity2
    {
        string Description { get; set; }
    }

    [ComVisible(true)]
    [Guid("668EE2E8-5A60-468B-8689-D9327090AA44")]
    public class TestEntity2 : ITestEntity2
    {
        public string Description { get; set; }
    }

    [ComVisible(true)]
    [Guid("2791082F-F505-49C4-8952-80C174E4FE96")]
    public interface ITestGateway
    {
        //MarshalAsAttribute is somewhat important, it tells the tlbexp.exe tool to mark
        // the comInputValue parameter as IUnknown* in the COM interface.
        //This is good because VARIANTS kinda suck...You'll see what I mean in the C++
        // side.  It also keeps some jack-wagon from passing a VARIANT_BOOL in
        // on your object parameter.
        void DoSomething(string a, [MarshalAs(UnmanagedType.Interface)]object comInputValue);
    }

    [ComVisible(true)]
    [Guid("C3D079F3-7869-4B3E-A742-263775C6EA63")]
    public class TestGateway : ITestGateway
    {
        public void DoSomething(string a, object comInputValue)
        {
            if (a == "yes")
            {
                var entity = (TestEntity1)comInputValue;
            }
            else
            {
                var entity = (TestEntity2) comInputValue;
            }
            //OR

            if(comInputValue is TestEntity1)
            {
                //Do whatever here, and you don't need to test
                // a string input value.
            }
            else if(comInputValue is TestEntity2)
            {
                //Other stuff is done here.
            }
            else
            {
                //Error condition??
            }
        }
    }
}

That can be called by the following C++:

// ComClient.cpp : Defines the entry point for the console application.
//

#include "stdafx.h"
#import <mscorlib.tlb> raw_interfaces_only

//This creates the CLSID_ and IID_ constants, and
// some strongly-typed interfaces.
#import "..\Debug\ClassLibrary1.tlb" no_namespace named_guids


int _tmain(int argc, _TCHAR* argv[])
{
    ITestGateway* test = NULL;

    char buffer[50];
    gets(buffer);  //Just a pause to attach the debugger in Managed + Native mode...hit enter in the console.

    CoInitialize(NULL);

    HRESULT hr = CoCreateInstance(CLSID_TestGateway,
        NULL,
        CLSCTX_INPROC_SERVER,
        IID_ITestGateway,
        reinterpret_cast<void**>(&test));

    if(FAILED(hr)) {
        printf("Couldn't create the instance!... 0x%x\n", hr);
        gets(buffer);
    } else {
        _bstr_t someString("yes");

        //Instead of fooling with CComVariant,
        // just directly create a TestEntity1...which COM will return
        // as an ITestEntity1.
        ITestEntity1* testEntity1 = NULL;
        HRESULT hr = CoCreateInstance(CLSID_TestEntity1,
            NULL,
            CLSCTX_INPROC_SERVER,
            IID_ITestEntity1,
            reinterpret_cast<void**>(&testEntity1));

        if(FAILED(hr)) {
            printf("Entity was not created!... 0x%x\n", hr);
            gets(buffer);
            return 0;
        }

        //Set some kind of property just for show.
        testEntity1->PutThing(VARIANT_FALSE);



        //If you attached your debugger with Managed code & Native code,
        // you should be able to hit a C# break point during this call.
        //Also, notice that there is no cast for testEntity1.  All interfaces
        // in COM derive from IUnknown, so you can just pass it.
        //IDispatch also derives from IUnknown, so if that's what you already have,
        // you can just pass it as well, with no cast.
        test->DoSomething(someString, testEntity1);

        printf("Something was done.");

        testEntity1->Release(); //Release anything you make through CoCreateInstance()
    }

    test->Release();

    return 0;
}