8

I have a pair of libraries that both use the same COM interface. In one library I have a class that implements that interface. The other library requires an object that implements the interface.

However both libraries have their own definition of the interface. Both are slightly different but essentially the same interface.

So I try to case between them as follows:

 Library2.Interface intf = (Library2.Interface)impl;

but this raises as an exception. If I do the following:

 Library1.Interface intf = (Library1.Interface)impl;

Then it casts without problem but I am no longer able to pass the class to Library2.

I naively assumed that both interfaces having the same GUID would prevent this being a problem but I appear to be wrong on that. Does anyone have any idea how I can convert between the 2 libraries? Perhaps via a Marshal of some sort?

noseratio
  • 59,932
  • 34
  • 208
  • 486
Goz
  • 61,365
  • 24
  • 124
  • 204
  • 1
    `Library1.Interface` and `Library2.Interface` are *not* considered the same interface by the CLR (no matter whether their definitions look the same, or whether they somehow map to the same COM interface). – O. R. Mapper Nov 05 '14 at 13:26
  • 2
    Could you expand on "Both are slightly different but essentially the same". If they are different how can they be compatible? Interfaces state that class implementing MUST support. If they are different how will that work? – Belogix Nov 05 '14 at 13:26
  • 2
    Why not creating a common library and reference the same interface from both libraries? – Matias Cicero Nov 05 '14 at 13:27
  • @Belogix: One interface uses IntPtr's where the other interface uses actual interface definitions. – Goz Nov 05 '14 at 13:28
  • @MatiCicero: Because neither library is under my control, sadly ... – Goz Nov 05 '14 at 13:28
  • @O.R.Mapper: Thanks for that. I had worked that out. There must, however, be a way to get Library1.Interface to be interpreted as Library2.Interface ... surely? – Goz Nov 05 '14 at 13:30
  • Why don't you just map from `Library1` into `Library2` and pass in the expected one into `Library2`? Not sure, but maybe look at Adaptor Pattern etc? – Belogix Nov 05 '14 at 13:31
  • @Belogix: Could you give an example of that? – Goz Nov 05 '14 at 13:31
  • Would [Conversion operators](http://msdn.microsoft.com/en-us/library/85w54y0a.aspx) work for you? In this case you would need an `explicit` conversion, since you are casting. – Srikanth Venugopalan Nov 05 '14 at 13:34
  • @SrikanthVenugopalan: I don't see how as this would require me to re-create the entire object and the implementation is hidden from me ... – Goz Nov 05 '14 at 13:36
  • This is the kind of scenario where you require a PIA, a Primary Interop Assembly that ensures that both apps use the exact same .NET wrapper so that type identity rules are observed. PIAs became obsolete with .NET 4.0/VS2010 thanks to the "Embed Interop Types" feature. – Hans Passant Nov 05 '14 at 13:36
  • Well, unless you can change those libraries - you're screwed and you'll have to write an adapter from one interface to another (reimplement `OneInterface` by forwarding calls to `AnotherInterface`). – sharptooth Nov 05 '14 at 14:36
  • @sharptooth: Even though they are the same interface. Its bizarre, tbh. There really ought to be a solution to this problem ... – Goz Nov 05 '14 at 14:45
  • The root of the problem is that interface definition is duplicated. An easy solution could be a native code helper that takes one interface, reinterprets to the other and returns to managed caller. The forwarding wrapper, suggested by sharptooh above is a universal solution, but if methods actually match then simple cast in native code domain will do the trick. – Roman R. Nov 05 '14 at 15:06
  • @RomanR. Yeah I've been thinking along the same lines. Do you think its possible to do directly in c# using unsafe code? – Goz Nov 05 '14 at 15:10
  • I suppose there could be a way to do it right in C#, but I am not aware of it. C++ solution is straightforward and I suppose it is not worth mentioning - you can easily do it. – Roman R. Nov 05 '14 at 15:16
  • @RomanR. Absoloutely just seems a shame to create an entire new module to do one such simple thing ... – Goz Nov 05 '14 at 15:18
  • @Goz: Come on, you're trying to subvert .NET type safety. This cannot be easy, otherwise that wouldn't be called type safety in the first place. – sharptooth Nov 05 '14 at 15:20
  • @sharptooth: But they ARE the same type!! Thats why I think there ought to be a simpler way to do it ... – Goz Nov 05 '14 at 15:22
  • @Goz: I feel your pain but let's face the truth: having them duplicated on 2+ libraries is already a violation. And then workaround does not have to be easy. I totally agree with you about need to introduce a separate module, however it is what it is: you have to fool CLR and it attempts to keep its integrity. – Roman R. Nov 05 '14 at 15:35
  • Comments are not for extended discussion; this conversation has been [moved to chat](http://chat.stackoverflow.com/rooms/64332/discussion-on-question-by-goz-converting-between-2-different-libraries-using-the). – Taryn Nov 05 '14 at 15:36
  • Can conversion to `IUnknown` and then to the target interface possibly work? http://stackoverflow.com/a/3941684/57428 – sharptooth Nov 05 '14 at 15:36
  • I assume that you use `Marshal.GetTypedObjectForIUnknown()` for the second step. – sharptooth Nov 05 '14 at 15:41
  • @sharptooth: Nice thought, but had already tried that one :( – Goz Nov 05 '14 at 15:50
  • @Goz: And what exactly is the error message and where does it come from when you try that? – sharptooth Nov 06 '14 at 09:39
  • @sharptooth: Its an invalid cast exception saying that it can't cast from Library1.Implementation to Library2.Interface. ie exactly the same error as if i try to directly cast to Library2.Interface. – Goz Nov 06 '14 at 10:08
  • @Goz: Is that from `Marshal.GetTypedObjectForIUnknown()`? How do you call the latter? – sharptooth Nov 06 '14 at 10:12
  • @sharptooth: `(Library2.Interface)Marshal.GetTypedObjectForIUnknown( pUnk, typeof( Library2.Interface ) );` – Goz Nov 06 '14 at 10:16
  • What's the type of the object returned which you then cast? What if you use `Marshal.GetComInterfaceForObject()` instead of the cast? – sharptooth Nov 06 '14 at 10:58
  • And also what if you try `GetUniqueObjectForIUnknown()`? I'd guess all problems come from the fact that the same RCW is returned to you and of course it cannot be proper cast. – sharptooth Nov 06 '14 at 11:09
  • Let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/64388/discussion-between-goz-and-sharptooth). – Goz Nov 06 '14 at 11:12

1 Answers1

8

This is a very interesting problem, and I think I may have an interesting solution for it. So, although Library1.Interface and Library2.Interface are the two binary compatible ComImport interfaces, they're still two different .NET interfaces and cannot be cast to each other.

To make the casting possible, we need to somehow hide the identity of the managed Library1.Interface .NET object behind a COM-callable wrapper (CCW), then make sure this CCW doesn't get marshaled to the same .NET object. So that the .NET marshaller would create a separate RCW proxy which then could be cast to Library2.Interface as a plain vanilla COM object.

Besides using separate COM apartments for Library1.Interface and Library2.Interface objects, I can only think of one other way of doing this: COM aggregation. Any .NET object can be aggregated via Marshal.CreateAggregatedObject as an inner object. The trick is to construct the unmanaged IUnknown COM identity object to serve as an outer (parent) object for aggregation. Such outer object will be given a separate RCW proxy when accessed from .NET.

Below is my take on this:

var server = ComWrapper.Create<Library2.Interface>(() => new Library1.Server());
var client = new Library2.Client();
client.CallMethod(server);

The whole logic as a console app (certain knowledge of COM binary protocols is required to understand this code):

using System;
using System.Runtime.InteropServices;

namespace Library1
{
    [ComImport, InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
    [Guid("4C08A691-5D61-4E9A-B16D-75BAD2834BAE")]
    public interface Interface
    {
        void TestMethod();
    }

    [ComVisible(true)]
    public class Server : Interface
    {
        public Server() { }

        public void TestMethod()
        {
            Console.WriteLine("TestMethod called");
        }
    }
}

namespace Library2
{
    [ComImport, InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
    [Guid("4C08A691-5D61-4E9A-B16D-75BAD2834BAE")]
    public interface Interface
    {
        void TestMethod();
    }

    public class Client
    {
        public void CallMethod(Library2.Interface server)
        {
            server.TestMethod();
        }
    }
}

namespace TestApp
{
    class Program
    {
        static void Main(string[] args)
        {
            // convert Library1.Server to Library2.Interface 
            var server = ComWrapper.Create<Library2.Interface>(() => new Library1.Server());
            var client = new Library2.Client();
            client.CallMethod(server);

            Marshal.ReleaseComObject(server);
            Console.ReadLine();
        }
    }

    /// <summary>
    /// ComWrapper - http://stackoverflow.com/q/26758316/1768303
    /// by Noseratio
    /// </summary>
    public class ComWrapper
    {
        readonly Guid IID_IUnknown = new Guid("00000000-0000-0000-C000-000000000046");
        const int S_OK = 0;
        const int E_FAIL = unchecked((int)0x80004005);

        delegate int QueryInterfaceMethod(IntPtr pUnk, ref Guid iid, out IntPtr ppv);
        delegate int AddRefMethod(IntPtr pUnk);
        delegate int ReleaseMethod(IntPtr pUnk);

        [StructLayout(LayoutKind.Sequential)]
        struct UnkObject
        {
            public IntPtr pVtable;
        }

        [StructLayout(LayoutKind.Sequential)]
        struct UnkVtable
        {
            public IntPtr pQueryInterface;
            public IntPtr pAddRef;
            public IntPtr pRelease;
        }

        int _refCount = 0;
        IntPtr _pVtable;
        IntPtr _outerObject;
        IntPtr _aggregatedObject;
        GCHandle _gcHandle;

        QueryInterfaceMethod _queryInterfaceMethod;
        AddRefMethod _addRefMethod;
        ReleaseMethod _releaseMethod;

        private ComWrapper()
        {
        }

        ~ComWrapper()
        {
            Console.WriteLine("~ComWrapper");
            Free();
        }

        private IntPtr Initialize(Func<object> createInnerObject)
        {
            try
            {
                // implement IUnknown methods
                _queryInterfaceMethod = delegate(IntPtr pUnk, ref Guid iid, out IntPtr ppv)
                {
                    lock (this)
                    {
                        // delegate anything but IID_IUnknown to the aggregated object
                        if (IID_IUnknown == iid)
                        {
                            ppv = _outerObject;
                            Marshal.AddRef(_outerObject);
                            return S_OK;
                        }
                        return Marshal.QueryInterface(_aggregatedObject, ref iid, out ppv);
                    }
                };

                _addRefMethod = delegate(IntPtr pUnk)
                {
                    lock (this)
                    {
                        return ++_refCount;
                    }
                };

                _releaseMethod = delegate(IntPtr pUnk)
                {
                    lock (this)
                    {
                        if (0 == --_refCount)
                        {
                            Free();
                        }
                        return _refCount;
                    }
                };

                // create the IUnknown vtable
                var vtable = new UnkVtable();
                vtable.pQueryInterface = Marshal.GetFunctionPointerForDelegate(_queryInterfaceMethod);
                vtable.pAddRef = Marshal.GetFunctionPointerForDelegate(_addRefMethod);
                vtable.pRelease = Marshal.GetFunctionPointerForDelegate(_releaseMethod);

                _pVtable = Marshal.AllocCoTaskMem(Marshal.SizeOf(vtable));
                Marshal.StructureToPtr(vtable, _pVtable, false);

                // create the IUnknown object
                var unkObject = new UnkObject();
                unkObject.pVtable = _pVtable;
                _outerObject = Marshal.AllocCoTaskMem(Marshal.SizeOf(unkObject));
                Marshal.StructureToPtr(unkObject, _outerObject, false);

                // pin the managed ComWrapper instance
                _gcHandle = GCHandle.Alloc(this, GCHandleType.Normal);

                // create and aggregate the inner object
                _aggregatedObject = Marshal.CreateAggregatedObject(_outerObject, createInnerObject());

                return _outerObject;
            }
            catch
            {
                Free();
                throw;
            }
        }

        private void Free()
        {
            Console.WriteLine("Free");
            if (_aggregatedObject != IntPtr.Zero)
            {
                Marshal.Release(_aggregatedObject);
                _aggregatedObject = IntPtr.Zero;
            }
            if (_pVtable != IntPtr.Zero)
            {
                Marshal.FreeCoTaskMem(_pVtable);
                _pVtable = IntPtr.Zero;
            }
            if (_outerObject != IntPtr.Zero)
            {
                Marshal.FreeCoTaskMem(_outerObject);
                _outerObject = IntPtr.Zero;
            }
            if (_gcHandle.IsAllocated)
            {
                _gcHandle.Free();
            }
        }

        public static T Create<T>(Func<object> createInnerObject)
        {
            var wrapper = new ComWrapper();
            var unk = wrapper.Initialize(createInnerObject);
            Marshal.AddRef(unk);
            try
            {
                var comObject = Marshal.GetObjectForIUnknown(unk);
                return (T)comObject;
            }
            finally
            {
                Marshal.Release(unk);
            }
        }
    }
}
noseratio
  • 59,932
  • 34
  • 208
  • 486