How can I invoke a method of a private COM interface, defined in a base class, from a derived class?
For example, here is the COM interface, IComInterface
Here is my solution. Ok, it uses reflection, but I don't see where is the problem since it's much simpler, and the final usage is really just one line of code, like this:
// IComInterface
void IComInterface.ComMethod(object arg)
{
InvokeBaseMethod(this, "ComMethod", typeof(OldLibrary.BaseClass), typeof(IComInterface), arg);
}
and the utility method (reusable for any class) is this:
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);
}
You need to use ICustomMarshaler
. I just worked out this solution and it is much less complex that what you've got, and there's no reflection. As far as I can tell, ICustomMarshaler
is the only way to explicitly control that magical ability of managed objects--such as RCW proxies--where they can be cast, on-the-fly, into managed interface pointers that they don't appear to explicitly implement.
For the complete scenario I'll demonstrate, the bold-face items refer to the relevant parts of my example.
You are receiving an unmanaged interface pointer (pUnk) into your managed code via a COM interop
function (e.g. MFCreateMediaSession), perhaps previously using the excellent interop attribute ([MarshalAs(UnmanagedType.Interface)] out IMFMediaSession pSess, ...
in order to receive a managed interface (IMFMediaSession). You'd like to "improve" (as you say) upon the backing __COM
object that you get in this situation by providing your own managed class (session) which:
The key is to change your marshaling directive on the function that obtains the unmanaged object so that it uses a custom marshaler. If the p/Invoke
definition is in an external library you don't control, you can make your own local copy. That's what I did here, where I replaced [Out, MarshalAs(UnmanagedType.Interface)]
with the new attribute:
[DllImport("mf.dll", ExactSpelling = true), SuppressUnmanagedCodeSecurity]
static extern HResult MFCreateMediaSession(
[In] IMFAttributes pConfiguration,
[Out, MarshalAs(UnmanagedType.CustomMarshaler, MarshalTypeRef = typeof(MFSessionMarshaler))] out IMFMediaSession ppMediaSession
);
To deploy your own class that has the 'magical' interface behavior I mentioned above, you'll need two classes: an abstract base class which must be marked with [ComImport]
(even if it's not, really) to provide the RCW plumbing, plus the other attributes I show (create your own GUID), and then a derived class, where you can put whatever enhanced functionality you like.
The thing to note here is that neither the base class (_session in my example) nor the derived class (session) may explicitly list the interface that you expect it to proxy from an unmanaged IUnknown
. Any "proper" interface definition that duplicates a QueryInterface
version will take precedence and ruin your ability to effortlessly call the unmanaged "base" methods via casting. You'll be back to COM slots and _vtbl land.
This also means that, on instances of the derived class you will only be able to access the imported interface by casting. The derived class can implement the other, "extra" interfaces in the usual way. Those can be imported COM interfaces also, by the way.
Here are the two classes I just described where your app content goes. Notice how uncluttered they are compared to if you had to forward a gigantic interface through one or more member variables (which you'd have to initialize, and clean up, etc.)
[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.
}
}
Next is the ICustomMarshaler
implementation. Because the argument we tagged to use this is an out
argument, the managed-to-native functions of this class will never be called. The main function to implement is MarshalNativeToManaged
, where I use GetTypedObjectForIUnknown
specifying the derived class I defined (session). Even though that class doesn't implement IMFMediaSession
, you'll be able to obtain that unmanaged interface via casting.
Calling Release
in the CleanUpNativeData
call is currently my best guess. (If it's wrong, I'll come back to edit this post).
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) { } }
Here we see one of the few places in .NET I am aware of that you are allowed to (temporarily) violate type safety. Because notice that ppMediaSession pops out of the marshaler into your code as a full-blown, strongly-typed argument out IMFMediaSession ppMediaSession
, but it certainly wasn't acting as such (i.e. without casting) immediately beforehand in the custom marshaling code.
Now you're ready to go. Here are some examples showing how you can use it, and demonstrating that things work as expected:
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
I figured it out, by using a helper contained object (BaseClassComProxy
) and an aggregated COM proxy object, created with Marshal.CreateAggregatedObject. This approach gives me an unmanaged object with separate identity, which I can cast (with Marshal.GetTypedObjectForIUnknown) to my own equivalent version of BaseClass.IComInterface
interface, which is not otherwise accessible. It works for any other private COM interfaces, implemented by BaseClass
.
@EricBrown's points about COM identity rules have helped a lot with this research. Thanks Eric!
Here's a standalone console test app. The code solving the original problem with WebBrowserSite
is posted 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);
}
}
}
Output:
BaseClass.IComInterface.ComMethod ImprovedClass.IComInterface.ComMethod Press Enter to exit. BaseClassComProxy object finalized. ImprovedClass finalized.
I'm used to doing this in C++, so I'm mentally translating from C++ to C# here. (I.e., you may have to do some tweaking.)
COM identity rules require the set of interfaces on an object to be static. So, if you can get some interface that's definitely implemented by BaseClass
, you can QI off that interface to get BaseClass
'es implementation of IComInterface
.
So, something like this:
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 */);