Partager via


ICustomQueryInterface and CLR V4

CLR V4 fixes an issue with COM-interop that’s been bothering me for a while. The problem is that unless you’re using a PIA, you can often have either your caller or callee be managed code, but not both

You can import the same COM-classic interface into managed code multiple times, and two components can naturally end up with two different .NET types representing the same single COM-classic interface. Furthermore, the types can be imported with different managed signatures because of things like [PreserveSig] attributes and different ways to marshal data types.

(PIAs are supposed to alleviate that by providing a single unified definition, but then getting multiple components to agree on that unified definition is its own problem. CLR V4 added support for to avoid requiring PIAs. see NoPia. )

When managed code calls a COM interface that is implemented by managed code (Managed –> Native –> Managed), the CLR detects that the COM-object is really a managed implementation and creates a direct managed call (Managed –> Managed). So if your caller and callee are bound to different .NET types for the interface, the CLR won’t realize it’s a COM-interface call and will just fail on the .NET type mismatch.

CLR V4 fixes this by adding the ICustomQueryInterface that lets a managed object really act as if it’s native code when being called through COM-interop.

 

Where I hit this…

I hit this as I was writing debugger code in Managed. We had a managed application (MDbg) that used COM-interop to call into a native implementation of ICorDebug. That worked great (managed calling native). Later, we had some cases of creating a managed implementation of certain ICorDebug interfaces. But that failed from Mdbg because they had different COM-interop import definitions for ICorDebug.

This also just naturally starts showing up as people are porting more and more of their legacy systems to managed code.

 

Code sample demonstrating the problem

Say you have a COM-classic interface IFoo, imported by 2 different components:

 [ComImport, InterfaceType(1), ComConversionLoss, Guid("FC3E287D-D659-4E1D-81D5-9D29398C7237")]
interface IFoo1
{
    [PreserveSig]
    int Thing(int x);
}

[ComImport, InterfaceType(1), ComConversionLoss, Guid("FC3E287D-D659-4E1D-81D5-9D29398C7237")]
interface IFoo2
{        
    void Thing(int x);
}

 

Now suppose you have a class C1 that implements IFoo1. IFoo1 and IFoo2 represent the same COM interface and have the same GUID, so you’d like to be able to use them interchangeably. However, they’re 2 different .NET types. typeof(IFoo1) != typeof(IFoo2).

Ideally, you’d like the following snippet to succeed and call Thing(5) and Thing(3) on the object instance obj.

 static void Main(string[] args)
{
    Console.WriteLine("Hi!");

    object obj = new C1();
    obj = GetRCW(obj); // get a COM object for C1

    IFoo1 f1 = (IFoo1)obj;
    IFoo2 f2 = (IFoo2)obj; // Fails!!! obj is really a C1, and can’t cast to a IFoo2
    f1.Thing(5);
    f2.Thing(3);
}

Run that and you get an invalid cast exception because it can’t cast C1 to a IFoo2. It doesn’t understand that IFoo2 and IFoo1 are the same interface..

 C:\TEMP> b.exeHi!Unhandled Exception: System.InvalidCastException: Unable to cast object of type 'ConsoleApplication4.C1' to type 'ConsoleApplication4.IFoo2'.   at ConsoleApplication4.Program.Main(String[] args)

 

ICustomQueryInterface to the rescue.

CLR 4 allows an object to hook ICustomQueryInterface and have fine grain control over QI calls. The CLR detects that C1 is really a managed object because of a QI for a secret interface, IManagedObject. C1 can intercept this QI call and fail it (by returning CustomQueryInterfaceResult.Failed), and thus look like a native object.  It passes all other QI calls through to the RCW’s QI handling (via returning CustomQueryInterfaceResult.NotHandled)

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

namespace ConsoleApplication4
{
    [ComImport, InterfaceType(1), ComConversionLoss, Guid("FC3E287D-D659-4E1D-81D5-9D29398C7237")]
    interface IFoo1
    {
        [PreserveSig]
        int Thing(int x);
    }

    [ComImport, InterfaceType(1), ComConversionLoss, Guid("FC3E287D-D659-4E1D-81D5-9D29398C7237")]
    interface IFoo2
    {        
        void Thing(int x);
    }


    class C1 : IFoo1, ICustomQueryInterface
    {

        static readonly Guid IID_IMarshal = new Guid("00000003-0000-0000-C000-000000000046");
        static readonly Guid IID_IManagedObject = new Guid("C3FCC19E-A970-11d2-8B5A-00A0C9B7C9C4");

        CustomQueryInterfaceResult ICustomQueryInterface.GetInterface(ref Guid iid, out IntPtr ppv)
        {
            if (iid == IID_IMarshal ||
                iid == IID_IManagedObject
                )
            {
                ppv = IntPtr.Zero;
                return CustomQueryInterfaceResult.Failed;
            }

            ppv = IntPtr.Zero;
            return CustomQueryInterfaceResult.NotHandled;
        }



        #region IFoo1 Members

        public int Thing(int x)
        {
            Console.WriteLine("Inside C1={0}", x);
            return 0;
        }

        #endregion
    }


   
    class Program
    {
        // Convert it to a RCW
        static object GetRCW(object o)
        {
            IntPtr ip = IntPtr.Zero;
            try
            {
                ip = Marshal.GetIUnknownForObject(o);
                return Marshal.GetObjectForIUnknown(ip);
            }
            finally
            {
                Marshal.Release(ip);
            }
        }

        static void Main(string[] args)
        {
            Console.WriteLine("Hi!");

            object c1 = new C1();
            object obj = GetRCW(c1);

            Console.WriteLine(c1.GetType().FullName); // ConsoleApplication4.C1
            Console.WriteLine(obj.GetType().FullName); // System.__ComObject

            IFoo1 f1 = (IFoo1)obj;
            IFoo2 f2 = (IFoo2)obj;
            f1.Thing(5);
            f2.Thing(3);
        }
    }
}

We call GetRCW() to convert the object to a Runtime Callable Wrapper (RCW) so that the CLR will actually do COM-interop dispatch. You can observe the GetType() calls on c1 vs. obj.

Notice now both calls via Thing(5) and Thing(3) succeed. And they’re even going through different import signatures.

 C:\TEMP> a.exeHi!ConsoleApplication4.C1System.__ComObjectInside C1=5Inside C1=3

 

 

Thanks to Paul Harrington (a dev on the Visual Studio Platform team) and Misha Shneerson  for pointing me at the new functionality and code snippet for C1.GetInterface()  Misha has a lot more information about CLR V4 COM-interop advances on his blog at https://blogs.msdn.com/mshneer/.