如何从派生类调用基类中定义的私有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.ComMethod
拨打ImprovedClass.ComMethod
?我可以使用反射,但在实际用例中IComInterface
是具有许多复杂签名成员的复杂OLE接口。
我认为这是因为BaseClass.IComInterface
和ImprovedClass.IComInterface
都是具有相同GUID和相同方法签名的COM接口,并且.NET 4.0+中有COM Type Equivalence,所以必须有一种方法来做我没有反思的事情。
另一个要求是ImprovedClass
必须从BaseClass
派生,因为C#客户端代码需要BaseClass
的实例,并将其传递给COM客户端代码。因此,BaseClass
内ImprovedClass
的遏制不是一种选择。
[已编辑] WebBrowser
描述了涉及从WebBrowserSite
和here派生的真实场景。
答案 0 :(得分:2)
我习惯在C ++中这样做,所以我在这里精神上从C ++转换为C#。 (即,你可能需要做一些调整。)
COM identity rules要求对象上的接口集是静态的。因此,如果您可以获得一些明确由BaseClass
实现的界面,您可以关闭该界面以获得BaseClass
的{{1}}实现。
所以,像这样:
IComInterface
答案 1 :(得分:2)
我通过使用包含帮助器的对象(BaseClassComProxy
)和使用Marshal.CreateAggregatedObject
创建的聚合COM代理对象来解决这个问题。这种方法为我提供了一个具有单独身份的非托管对象,我可以将其(使用Marshal.GetTypedObjectForIUnknown
)转换为我自己的等效版本的BaseClass.IComInterface
接口,否则无法访问。它适用于由BaseClass
实现的任何其他私有COM接口。
@ EricBrown关于COM身份规则的观点对这项研究有很大帮助。谢谢埃里克!
这是一个独立的控制台测试应用程序。使用WebBrowserSite
解决原始问题的代码已发布here。
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.
答案 2 :(得分: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);
}
答案 3 :(得分:1)
您需要使用ICustomMarshaler
。我刚刚制定出这个解决方案,它比你得到的要简单得多,而且没有任何反映。据我所知,ICustomMarshaler
是显式控制托管对象的神奇能力的唯一方法 - 例如RCW代理 - 在那里它们可以在运行中转换为托管接口指针它们似乎没有明确实施。
对于我将演示的完整场景,粗体项目参考我的示例的相关部分。
您通过COM interop
函数(例如 MFCreateMediaSession )在托管代码中收到一个非托管接口指针( pUnk ),也许以前使用过优秀的互操作属性([MarshalAs(UnmanagedType.Interface)] out IMFMediaSession pSess, ...
以便接收托管界面( IMFMediaSession )。您希望通过提供您自己的托管类(会话)对您在此情况下获得的后备__COM
对象进行“改进”(如您所说):
关键是在获取非托管对象的函数上更改编组指令,以便它使用custom marshaler。如果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 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
参数,所以永远不会调用此类的托管到本机函数。要实现的主要功能是MarshalNativeToManaged
,我使用GetTypedObjectForIUnknown
指定我定义的派生类(会话)。即使该类没有实现IMFMediaSession
,您也可以通过强制转换获得该非托管接口。
在Release
电话中拨打CleanUpNativeData
是我最好的猜测。 (如果错了,我会回来编辑这篇文章)。
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