2

如何从派生类调用在基类中定义的私有 COM 接口的方法?

例如,这里是 COM 接口,IComInterface(IDL):

[
    uuid(9AD16CCE-7588-486C-BC56-F3161FF92EF2),
    oleautomation
]
interface IComInterface: IUnknown
{
    HRESULT ComMethod([in] IUnknown* arg);
}

BaseClass这是来自程序集的 C# 类OldLibrary,它的实现IComInterface方式如下(注意接口被声明为私有):

// Assembly "OldLibrary"
public static class OldLibrary
{
    [ComImport(), Guid("9AD16CCE-7588-486C-BC56-F3161FF92EF2")]
    [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
    private interface IComInterface
    {
        void ComMethod([In, MarshalAs(UnmanagedType.Interface)] object arg);
    }

    [ComVisible(true)]
    [ClassInterface(ClassInterfaceType.None)]
    public class BaseClass : IComInterface
    {
        void IComInterface.ComMethod(object arg)
        {
            Console.WriteLine("BaseClass.IComInterface.ComMethod");
        }
    }
}

最后,这是一个改进的版本,ImprovedClass它派生自BaseClass,但声明并实现了自己的版本IComInterface,因为基础OldLibrary.IComInterface是不可访问的:

// Assembly "NewLibrary"
public static class NewLibrary
{
    [ComImport(), Guid("9AD16CCE-7588-486C-BC56-F3161FF92EF2")]
    [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
    private interface IComInterface
    {
        void ComMethod([In, MarshalAs(UnmanagedType.Interface)] object arg);
    }

    [ComVisible(true)]
    [ClassInterface(ClassInterfaceType.None)]
    public class ImprovedClass : 
        OldLibrary.BaseClass, 
        IComInterface, 
        ICustomQueryInterface
    {
        // IComInterface
        void IComInterface.ComMethod(object arg)
        {
            Console.WriteLine("ImprovedClass.IComInterface.ComMethod");
            // How do I call base.ComMethod here, 
            // otherwise than via reflection?
        }

        // ICustomQueryInterface
        public CustomQueryInterfaceResult GetInterface(ref Guid iid, out IntPtr ppv)
        {
            if (iid == typeof(IComInterface).GUID)
            {
                ppv = Marshal.GetComInterfaceForObject(this, typeof(IComInterface), CustomQueryInterfaceMode.Ignore);
                return CustomQueryInterfaceResult.Handled;
            }
            ppv = IntPtr.Zero;
            return CustomQueryInterfaceResult.NotHandled;
        }   

    }
}

我如何BaseClass.ComMethodImprovedClass.ComMethod没有反射的情况下打电话?
我可以使用反射,但在实际用例IComInterface中是一个复杂的 OLE 接口,其中包含许多复杂签名的成员。

我认为因为两者BaseClass.IComInterface都是ImprovedClass.IComInterfaceCOM 接口,具有相同的 GUID 和相同的方法签名,并且 .NET 4.0+ 中存在COM 类型等效,所以必须有一种方法可以在没有反射的情况下完成我所追求的事情。

另一个要求是ImprovedClass必须从 派生BaseClass,因为 C# 客户端代码需要一个 的实例BaseClass,并将其传递给 COM 客户端代码。因此,BaseClass内部遏制ImprovedClass不是一种选择。

[已编辑]一个真实的场景,其中涉及衍生WebBrowser在此处WebBrowserSite进行了描述。

4

4 回答 4

2

我习惯于在 C++ 中执行此操作,所以我在这里从 C++ 转换为 C#。(即,您可能需要进行一些调整。)

COM 身份规则要求对象上的接口集是静态的。所以,如果你能得到一些明确由 实现的接口,BaseClass你可以关闭那个接口来获得.BaseClassIComInterface

所以,像这样:

type typeBaseIComInterface = typeof(OldLibrary.BaseClass).GetInterfaces().First((t) => t.GUID == typeof(IComInterface).GUID); 
IntPtr unkBaseIComInterface = Marshal.GetComInterfaceForObject(this, typeBaseIComInterface, CustomQueryInterfaceMode.Ignore);
dynamic baseptr = Marshal.GetTypedObjectForIUnknown(unkBaseIComInterface, typeof(OldLibrary.BaseClass);
baseptr.ComMethod(/* args go here */);
于 2013-11-01T23:32:54.720 回答
2

我通过使用一个包含帮助器的对象 ( BaseClassComProxy) 和一个聚合的 COM 代理对象,用Marshal.CreateAggregatedObject. 这种方法为我提供了一个具有单独标识的非托管对象,我可以将它 (with Marshal.GetTypedObjectForIUnknown) 转换为我自己的等效版本的BaseClass.IComInterface接口,否则无法访问。它适用于任何其他私有 COM 接口,由BaseClass.

@EricBrown 关于 COM 身份规则的观点对这项研究有很大帮助。谢谢埃里克!

这是一个独立的控制台测试应用程序。解决原始问题的代码WebBrowserSite发布在这里

using System;
using System.Diagnostics;
using System.Linq;
using System.Runtime.InteropServices;

namespace ManagedServer
{
    /*
    // IComInterface IDL definition
    [
        uuid(9AD16CCE-7588-486C-BC56-F3161FF92EF2),
        oleautomation
    ]
    interface IComInterface: IUnknown
    {
        HRESULT ComMethod(IUnknown* arg);
    }
    */

    // OldLibrary
    public static class OldLibrary
    {
        // private COM interface IComInterface
        [ComImport(), Guid("9AD16CCE-7588-486C-BC56-F3161FF92EF2")]
        [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
        private interface IComInterface
        {
            void ComMethod([In, MarshalAs(UnmanagedType.Interface)] object arg);
        }

        [ComVisible(true)]
        [ClassInterface(ClassInterfaceType.None)]
        public class BaseClass : IComInterface
        {
            void IComInterface.ComMethod(object arg)
            {
                Console.WriteLine("BaseClass.IComInterface.ComMethod");
            }
        }
    }

    // NewLibrary 
    public static class NewLibrary
    {
        // OldLibrary.IComInterface is inaccessible here,
        // define a new equivalent version
        [ComImport(), Guid("9AD16CCE-7588-486C-BC56-F3161FF92EF2")]
        [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
        private interface IComInterface
        {
            void ComMethod([In, MarshalAs(UnmanagedType.Interface)] object arg);
        }

        [ComVisible(true)]
        [ClassInterface(ClassInterfaceType.None)]
        public class ImprovedClass :
            OldLibrary.BaseClass,
            NewLibrary.IComInterface,
            ICustomQueryInterface,
            IDisposable
        {
            NewLibrary.IComInterface _baseIComInterface;
            BaseClassComProxy _baseClassComProxy;

            // IComInterface
            // we want to call BaseClass.IComInterface.ComMethod which is only accessible via COM
            void IComInterface.ComMethod(object arg)
            {
                _baseIComInterface.ComMethod(arg);
                Console.WriteLine("ImprovedClass.IComInterface.ComMethod");
            }

            // ICustomQueryInterface
            public CustomQueryInterfaceResult GetInterface(ref Guid iid, out IntPtr ppv)
            {
                if (iid == typeof(NewLibrary.IComInterface).GUID)
                {
                    // CustomQueryInterfaceMode.Ignore is to avoid infinite loop during QI.
                    ppv = Marshal.GetComInterfaceForObject(this, typeof(NewLibrary.IComInterface), CustomQueryInterfaceMode.Ignore);
                    return CustomQueryInterfaceResult.Handled;
                }
                ppv = IntPtr.Zero;
                return CustomQueryInterfaceResult.NotHandled;
            }

            // constructor
            public ImprovedClass()
            {
                // aggregate the CCW object with the helper Inner object
                _baseClassComProxy = new BaseClassComProxy(this);
                _baseIComInterface = _baseClassComProxy.GetComInterface<IComInterface>();   
            }

            ~ImprovedClass()
            {
                Dispose();
                Console.WriteLine("ImprovedClass finalized.");
            }

            // IDispose
            public void Dispose()
            {
                // we may have recicular COM references to itself
                // e.g., via _baseIComInterface
                // make sure to release all references

                if (_baseIComInterface != null)
                {
                    Marshal.ReleaseComObject(_baseIComInterface);
                    _baseIComInterface = null;
                }

                if (_baseClassComProxy != null)
                {
                    _baseClassComProxy.Dispose();
                    _baseClassComProxy = null;
                }
            }

            // for testing
            public void InvokeComMethod()
            {
                ((NewLibrary.IComInterface)this).ComMethod(null);
            }
        }

        #region BaseClassComProxy
        // Inner as aggregated object
        class BaseClassComProxy :
            ICustomQueryInterface,
            IDisposable
        {
            WeakReference _outer; // avoid circular refs between outer and inner object
            Type[] _interfaces; // the base's private COM interfaces are here
            IntPtr _unkAggregated; // aggregated proxy

            public BaseClassComProxy(object outer)
            {
                _outer = new WeakReference(outer);
                _interfaces = outer.GetType().BaseType.GetInterfaces();
                var unkOuter = Marshal.GetIUnknownForObject(outer);
                try
                {
                    // CreateAggregatedObject does AddRef on this 
                    // se we provide IDispose for proper shutdown
                    _unkAggregated = Marshal.CreateAggregatedObject(unkOuter, this); 
                }
                finally
                {
                    Marshal.Release(unkOuter);
                }
            }

            public T GetComInterface<T>() where T : class
            {
                // cast an outer's base interface to an equivalent outer's interface
                return (T)Marshal.GetTypedObjectForIUnknown(_unkAggregated, typeof(T));
            }

            public void GetComInterface<T>(out T baseInterface) where T : class
            {
                baseInterface = GetComInterface<T>();
            }

            ~BaseClassComProxy()
            {
                Dispose();
                Console.WriteLine("BaseClassComProxy object finalized.");
            }

            // IDispose
            public void Dispose()
            {
                if (_outer != null)
                {
                    _outer = null;
                    _interfaces = null;
                    if (_unkAggregated != IntPtr.Zero)
                    {
                        Marshal.Release(_unkAggregated);
                        _unkAggregated = IntPtr.Zero;
                    }
                }
            }

            // ICustomQueryInterface
            public CustomQueryInterfaceResult GetInterface(ref Guid iid, out IntPtr ppv)
            {
                // access to the outer's base private COM interfaces
                if (_outer != null)
                {
                    var ifaceGuid = iid;
                    var iface = _interfaces.FirstOrDefault((i) => i.GUID == ifaceGuid);
                    if (iface != null && iface.IsImport)
                    {
                        // must be a COM interface with ComImport attribute
                        var unk = Marshal.GetComInterfaceForObject(_outer.Target, iface, CustomQueryInterfaceMode.Ignore);
                        if (unk != IntPtr.Zero)
                        {
                            ppv = unk;
                            return CustomQueryInterfaceResult.Handled;
                        }
                    }
                }
                ppv = IntPtr.Zero;
                return CustomQueryInterfaceResult.Failed;
            }
        }
        #endregion

    }

    class Program
    {
        static void Main(string[] args)
        {
            // test
            var improved = new NewLibrary.ImprovedClass();
            improved.InvokeComMethod(); 

            //// COM client
            //var unmanagedObject = (ISimpleUnmanagedObject)Activator.CreateInstance(Type.GetTypeFromProgID("Noseratio.SimpleUnmanagedObject"));
            //unmanagedObject.InvokeComMethod(improved);

            improved.Dispose();
            improved = null;

            // test ref counting
            GC.Collect(generation: GC.MaxGeneration, mode: GCCollectionMode.Forced, blocking: false);
            Console.WriteLine("Press Enter to exit.");
            Console.ReadLine();
        }

        // COM test client interfaces
        [ComImport(), Guid("2EA68065-8890-4F69-A02F-2BC3F0418561")]
        [InterfaceType(ComInterfaceType.InterfaceIsDual)]
        internal interface ISimpleUnmanagedObject
        {
            void InvokeComMethod([In, MarshalAs(UnmanagedType.Interface)] object arg);
            void InvokeComMethodDirect([In] IntPtr comInterface);
        }

    }
}

输出:

BaseClass.IComInterface.ComMethod
改进的Class.IComInterface.ComMethod
按 Enter 退出。
BaseClassComProxy 对象最终确定。
改进类最终确定。
于 2013-11-02T00:26:49.697 回答
2

这是我的解决方案。好的,它使用反射,但是我看不出问题出在哪里,因为它更简单,最终的用法实际上只是一行代码,如下所示:

// IComInterface
void IComInterface.ComMethod(object arg)
{
    InvokeBaseMethod(this, "ComMethod", typeof(OldLibrary.BaseClass), typeof(IComInterface), arg);
}

实用方法(可用于任何类)是这样的:

public static object InvokeBaseMethod(object obj, string methodName, Type baseType, Type equivalentBaseInterface, params object[] arguments)
{
    Type baseInterface = baseType.GetInterfaces().First((t) => t.GUID == equivalentBaseInterface.GUID);
    ComMemberType type = ComMemberType.Method;
    int methodSlotNumber = Marshal.GetComSlotForMethodInfo(equivalentBaseInterface.GetMethod(methodName));
    MethodInfo baseMethod = (MethodInfo)Marshal.GetMethodInfoForComSlot(baseInterface, methodSlotNumber, ref type);
    return baseMethod.Invoke(obj, arguments);
}
于 2013-11-03T08:27:58.750 回答
1

你需要使用ICustomMarshaler. 我刚刚制定了这个解决方案,它比你所拥有的要简单得多,而且没有反射。据我所知,ICustomMarshaler这是显式控制托管对象(例如 RCW 代理)的神奇能力的唯一方法,在这些对象中,它们可以动态转换为它们不会出现的托管接口指针明确实施。

对于我将演示的完整场景,粗体项目指的是我示例的相关部分。

设想

您正在通过函数(例如MFCreateMediaSession )将非托管接口指针( pUnk )接收您的托管代码中,可能之前使用了出色的互操作属性来接收托管接口(IMFMediaSession)。您想通过提供您自己的托管类( session )来“改进”(如您所说)在这种情况下获得的支持对象:COM interop([MarshalAs(UnmanagedType.Interface)] out IMFMediaSession pSess, ...__COM

  1. 可能会添加一些额外的接口(例如IMFAsyncCallback);
  2. 不需要你转发或重新实现你已经得到的接口;
  3. 将非托管接口的生命周期与托管接口的生命周期整合到单个 RCW 中
  4. 不存储任何无关的接口指针...

关键是更改获取非托管对象的函数上的编组指令,以便它使用自定义编组器。如果p/Invoke定义在您无法控制的外部库中,您可以制作自己的本地副本。这就是我在这里所做的,我[Out, MarshalAs(UnmanagedType.Interface)]用新属性替换了:

    [DllImport("mf.dll", ExactSpelling = true), SuppressUnmanagedCodeSecurity]
    static extern HResult MFCreateMediaSession(
        [In] IMFAttributes pConfiguration,
        [Out, MarshalAs(UnmanagedType.CustomMarshaler, MarshalTypeRef = typeof(MFSessionMarshaler))] out IMFMediaSession ppMediaSession
        );

要部署您自己的具有我上面提到的“神奇”接口行为的类,您需要两个类:一个必须标记的抽象基类[ComImport](即使它实际上不是)以提供 RCW 管道,以及另一个我展示的属性(创建您自己的 GUID),然后是派生类,您可以在其中放置您喜欢的任何增强功能。

这里要注意的是,基类(在我的示例中为_session )和派生类session)都不能显式列出您希望它从非托管代理的接口IUnknown。任何复制QueryInterface版本的“正确”接口定义都将具有优先权,并破坏您通过强制转换轻松调用非托管“基”方法的能力。您将回到 COM 插槽和 _vtbl 土地。

这也意味着,在派生类的实例上,您只能通过强制转换访问导入的接口。派生类可以以通常的方式实现其他“额外”接口。顺便说一下,这些也可以是导入的 COM 接口。

这是我刚刚描述的两个类,您的应用程序内容的去向。请注意,如果您必须通过一个或多个成员变量(您必须初始化和清理等)转发一个巨大的接口,它们与它们相比是多么整洁。

[ComImport, SuppressUnmanagedCodeSecurity, Guid("c6646f0a-3d96-4ac2-9e3f-8ae2a11145ce")]
[ClassInterface(ClassInterfaceType.None)]
public abstract class _session
{
}

public class session : _session, IMFAsyncCallback
{
    HResult IMFAsyncCallback.GetParameters(out MFASync pdwFlags, out MFAsyncCallbackQueue pdwQueue)
    {
        /// add-on interfaces can use explicit implementation...
    }

    public HResult Invoke([In, MarshalAs(UnmanagedType.Interface)] IMFAsyncResult pAsyncResult)
    {
        /// ...or public.
    }
}

接下来是ICustomMarshaler实现。因为我们标记为使用 this 的参数是一个out参数,所以永远不会调用此类的托管到本机函数。要实现的主要功能是MarshalNativeToManaged,我使用GetTypedObjectForIUnknown指定我定义的派生类(会话)。即使该类没有实现IMFMediaSession,您也可以通过强制转换获得该非托管接口。

打电话ReleaseCleanUpNativeData目前我最好的猜测。(如果有错,我会回来编辑这篇文章)。

class MFSessionMarshaler : ICustomMarshaler
{
    static ICustomMarshaler GetInstance(String _) => new MFSessionMarshaler();

    public Object MarshalNativeToManaged(IntPtr pUnk) => Marshal.GetTypedObjectForIUnknown(pUnk, typeof(session));

    public void CleanUpNativeData(IntPtr pNativeData) => Marshal.Release(pNativeData);

    public int GetNativeDataSize() => -1;
    IntPtr ICustomMarshaler.MarshalManagedToNative(Object _) => IntPtr.Zero;
    void ICustomMarshaler.CleanUpManagedData(Object ManagedObj) { } }

在这里,我们看到了 .NET 中少数几个地方之一,我知道您可以(暂时)违反类型安全。因为请注意ppMediaSession作为一个成熟的强类型参数从编组器中弹出到您的代码中,但它肯定不会在自定义编组代码中立即out IMFMediaSession ppMediaSession执行此操作(即没有强制转换)。

现在你准备好了。以下是一些示例,展示了如何使用它,并证明事情按预期工作:

IMFMediaSession pI;
MFCreateMediaSession(null, out pI);  // get magical RCW

var rcw = (session)pI;   // we happen to know what it really is

pI.ClearTopologies();    // you can call IMFMediaSession members...

((IMFAsyncCallback)pI).Invoke(null);  // and also IMFAsyncCallback.
rcw.Invoke(null);        // same thing, via the backing object
于 2017-01-30T05:25:39.377 回答