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

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

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

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

这是来自OldLibrary程序集的C#类BaseClass ,它实现了这样的IComInterface (注意接口被声明为private):

 // 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; } } } 

如何在不reflection的情况下从ImprovedClass.ComMethod调用BaseClass.ComMethod
我可以使用reflection,但在实际用例中, IComInterface是一个复杂的OLE接口,具有许多复杂签名的成员。

我认为,因为BaseClass.IComInterfaceImprovedClass.IComInterface都是具有相同GUID和相同方法签名的COM接口,并且.NET 4.0+中存在COM类型等价 ,所以必须有一种方法来做我想要的事情没有反思。

另一个要求是ImprovedClass必须从BaseClass派生,因为C#客户端代码需要一个BaseClass实例,它传递给COM客户端代码。 因此,在ImprovedClass包含BaseClass不是一种选择。

[编辑] 此处描述了涉及从WebBrowserWebBrowserSite派生的真实场景。

我习惯用C ++做这件事,所以我在这里心理上从C ++转换到C#。 (即,你可能需要做一些调整。)

COM标识规则要求对象上的接口集是静态的。 因此,如果您可以获得一些明确由BaseClass实现的接口,您可以关闭该接口以获得IComInterface BaseClass实现。

所以,像这样:

 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 */); 

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

 // 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); } 

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

@ 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(); } ~ImprovedClass() { Dispose(); Console.WriteLine("ImprovedClass finalized."); } // IDispose public void Dispose() { // we may have recicular COM references to itself // eg, 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() 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(out T baseInterface) where T : class { baseInterface = GetComInterface(); } ~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
 ImprovedClass.IComInterface.ComMethod
按Enter退出。
 BaseClassComProxy对象已完成。
 ImprovedClass定稿。

您需要使用ICustomMarshaler 。 我刚刚制定了这个解决方案,而且你所拥有的东西要复杂得多,而且没有任何反映。 据我所知, ICustomMarshaler是显式控制托管对象的神奇能力的唯一方法 – 例如RCW代理 – 它们可以在运行时被投射到托管界面指针中,而不是似乎明确实施。

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

脚本

您正在通过COM interop函数(例如MFCreateMediaSession )在托管代码中收到一个非托管接口指针( pUnk ),也许之前使用了优秀的互操作属性([MarshalAs(UnmanagedType.Interface)] out IMFMediaSession pSess, ...以便([MarshalAs(UnmanagedType.Interface)] out IMFMediaSession pSess, ...接收一个托管接口( IMFMediaSession )。你想通过提供你自己的托管类( 会话 )来“改进”(正如你所说)你在这种情况下得到的支持__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),然后是派生类,您可以在其中放置您喜欢的任何增强function。

这里需要注意的是, 基类 (我的示例中的_session和派生类会话 )都没有明确地列出您希望它从非托管IUnknown代理的接口。 任何复制QueryInterface版本的“正确”接口定义都将优先,并破坏您通过强制转换毫不费力地调用非托管“基础”方法的能力。 你将回到COM插槽和_vtbl land。

这也意味着,在派生类的实例上,您只能通过强制转换访问导入的接口。 派生类可以通常的方式实现其他“额外”接口。 顺便说一下,那些也可以是导入的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实现。 因为我们标记为使用它的参数是out参数,所以永远不会调用此类的托管到本机函数。 要实现的主要function是MarshalNativeToManaged ,其中我使用GetTypedObjectForIUnknown指定我定义的派生类( 会话 )。 即使该类没有实现IMFMediaSession ,您也可以通过强制转换获取该非托管接口。

CleanUpNativeData调用中调用Release是目前我最好的猜测。 (如果错了,我会回来编辑这篇文章)。

 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一个完整的,强类型的参数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