[英]How to invoke a method of a private COM interfaces, defined in a base class?
如何从派生类调用在基类中定义的私有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;
}
}
}
我如何在没有反射的情况下从ImprovedClass.ComMethod
调用BaseClass.ComMethod
?
我可以使用反射,但是在实际用例中, IComInterface
是一个复杂的OLE接口,其中包含许多复杂签名的成员。
我以为,因为BaseClass.IComInterface
和ImprovedClass.IComInterface
都是具有相同GUID和相同方法签名的COM接口,并且.NET 4.0+中存在COM类型等效项 ,因此必须有一种方法来执行我要执行的操作没有反思。
另一个要求是, ImprovedClass
必须从BaseClass
派生,因为C#客户端代码需要BaseClass
的实例,并将其传递给COM客户端代码。 因此,不能将BaseClass
在ImprovedClass
中。
[编辑] 此处描述了涉及从WebBrowser
和WebBrowserSite
派生的真实场景。
我已经习惯了在C ++中执行此操作,因此在这里我会从心理上将C ++转换为C#。 (即,您可能需要进行一些调整。)
COM身份规则要求对象上的接口集必须是静态的。 因此,如果您可以获得某些肯定由BaseClass
实现的接口,则可以关闭该接口的QI,以获取BaseClass
的IComInterface
实现。
因此,如下所示:
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 */);
通过使用包含的辅助对象( 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<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 ImprovedClass.IComInterface.ComMethod Press Enter to exit. BaseClassComProxy object finalized. ImprovedClass finalized.
这是我的解决方案。 好的,它使用反射,但是我看不出问题出在哪里,因为它要简单得多,最终的用法实际上只是一行代码,如下所示:
// 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);
}
您需要使用ICustomMarshaler
。 我只是想出了这个解决方案,它比您拥有的解决方案复杂得多,并且没有任何思考。 据我所知, ICustomMarshaler
是显式控制托管对象的神奇功能(如RCW代理)的唯一方法,在这种情况下,可以将它们动态地转换为它们不提供的托管接口指针似乎明确实施。
对于我将演示的完整场景,黑体字项引用了示例的相关部分。
您是通过接收非托管接口指针( 朋克 )到您的托管代码COM interop
功能(如MFCreateMediaSession),也许是以前使用的出色互操作属性([MarshalAs(UnmanagedType.Interface)] out IMFMediaSession pSess, ...
以接收一个托管接口( IMFMediaSession )。您想通过提供自己的托管类( session )在这种情况下获得的__COM
对象的“改进”(如您所说):
关键是更改获取非托管对象的函数上的封送处理指令,以使其使用自定义封送处理器 。 如果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
实现。 因为我们标记为使用此参数的参数是out
参数,所以永远不会调用此类的托管本机函数。 要实现的主要功能是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
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.