Skip to content

Commit

Permalink
ComWrappers example using IDispatch (#2873)
Browse files Browse the repository at this point in the history
* ComWrappers example.
  • Loading branch information
AaronRobinsonMSFT committed May 5, 2020
1 parent 09cec99 commit e5db2d2
Show file tree
Hide file tree
Showing 8 changed files with 633 additions and 0 deletions.
173 changes: 173 additions & 0 deletions core/interop/comwrappers/IDispatch/AnyObjectProxy.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
#nullable enable

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Runtime.InteropServices.ComTypes;

public class AnyObjectProxy : ComWrappersImpl.IDispatch,
ICustomQueryInterface /* WORKAROUND for WinForms WebBrowser control API */
{
private static Lazy<ComWrappers> g_ComWrappers = new Lazy<ComWrappers>(() => new ComWrappersImpl(), true);

private const int DISP_E_UNKNOWNNAME = unchecked((int)0x80020006);

private readonly Type objType;
private readonly object obj;
private readonly IntPtr wrapperPtr;
private readonly Dictionary<string, int> nameToDispId = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase);
private readonly Dictionary<int, MemberInfo> dispIdToMemberInfo = new Dictionary<int, MemberInfo>();
public AnyObjectProxy(object obj)
{
this.obj = obj;
this.objType = this.obj.GetType();

// The cached string to int is the function name to desired DispID.
// The DispIDs can be stored but need to consistent. The Invoke() call will be passed the
// DispID and will use that to Invoke whatever member is desired.
//
// The below caching could instead be done using MethodInfo and PropertyInfo instances.
// This would enable the Invoke() implementation to be more efficient.
//
int dispIdNext = 100; // Starting from 100 is typical convention.
foreach (MemberInfo mi in this.objType.GetMembers())
{
this.dispIdToMemberInfo[dispIdNext] = mi;
this.nameToDispId[mi.Name] = dispIdNext++;
}

this.wrapperPtr = g_ComWrappers.Value.GetOrCreateComInterfaceForObject(this, CreateComInterfaceFlags.None);
}

~AnyObjectProxy()
{
if (this.wrapperPtr != IntPtr.Zero)
{
Marshal.Release(this.wrapperPtr);
}
}

void ComWrappersImpl.IDispatch.GetTypeInfoCount(out int pctinfo)
{
// Will always be called.
// Returning 0 is a completely acceptable return value.
pctinfo = 0;
}

void ComWrappersImpl.IDispatch.GetTypeInfo(int iTInfo, int lcid, out IntPtr info)
{
// If GetTypeInfoCount() returns 0, this function will not be called.
throw new NotImplementedException();
}

void ComWrappersImpl.IDispatch.GetIDsOfNames(ref Guid iid, string[] names, int cNames, int lcid, int[] rgDispId)
{
Debug.Assert(iid == Guid.Empty);
for (int i = 0; i < cNames; ++i)
{
int dispId = 0;
if (!this.nameToDispId.TryGetValue(names[i], out dispId))
{
throw new COMException(null, DISP_E_UNKNOWNNAME);
}

rgDispId[i] = dispId;
}
}

void ComWrappersImpl.IDispatch.Invoke(
int dispIdMember,
ref Guid riid,
int lcid,
INVOKEKIND wFlags,
ref DISPPARAMS pDispParams,
IntPtr VarResult,
IntPtr pExcepInfo,
IntPtr puArgErr)
{
MemberInfo? memberInfo;
if (!this.dispIdToMemberInfo.TryGetValue(dispIdMember, out memberInfo))
{
throw new COMException(null, DISP_E_UNKNOWNNAME);
}

BindingFlags invokeFlags = BindingFlags.Public | BindingFlags.Instance;
if (wFlags.HasFlag(INVOKEKIND.INVOKE_FUNC)
&& memberInfo.MemberType == MemberTypes.Method)
{
invokeFlags |= BindingFlags.InvokeMethod;
}
else
{
throw new NotImplementedException("Operation not implemented.");
}

// Use reflection to dispatch to the indicated function.
// Note that this is exactly what the internal implementation of IDispatch does so there
// isn't a lot of difference in cost.
var result = this.objType.InvokeMember(
memberInfo.Name,
invokeFlags,
null,
this.obj,
MarshalArguments(ref pDispParams));

if (result != null)
{
// Lots of special cases should be addressed here.
// * Arrays, IEnumerable
// * IDispatch/IUnknown instances
// * .NET object could be wrapped by ComWrappers
// * .NET objects that are already COM objects can be safely passed on
// * etc
Marshal.GetNativeVariantForObject(result, VarResult);
}
}

private static object[] MarshalArguments(ref DISPPARAMS pDispParams)
{
if (pDispParams.cArgs == 0)
{
return Array.Empty<object>();
}

// Lots of special cases should be addressed here.
// * Arrays
// * IDispatch/IUnknown instances
// * .NET objects passed back as arguments
// * etc
return Marshal.GetObjectsForNativeVariants(pDispParams.rgvarg, pDispParams.cArgs)!;
}

/* WORKAROUND for WinForms WebBrowser control API */
[ThreadStatic] bool inGetInterface = false; // Needed since the ComWrappers API calls this prior
// to calling user defined interfaces.
CustomQueryInterfaceResult ICustomQueryInterface.GetInterface(ref Guid iid, out IntPtr ppv)
{
Debug.Assert(this.wrapperPtr != IntPtr.Zero);
ppv = IntPtr.Zero;

// Return the ComWrappers IDispatch implementation
// instead of the one provided by the runtime.
if (!this.inGetInterface
&& iid == typeof(ComWrappersImpl.IDispatch).GUID)
{
try
{
this.inGetInterface = true;
int hr = Marshal.QueryInterface(this.wrapperPtr, ref iid, out ppv);
Debug.Assert(hr == 0);
}
finally
{
this.inGetInterface = false;
}
}

return (ppv == IntPtr.Zero)
? CustomQueryInterfaceResult.NotHandled
: CustomQueryInterfaceResult.Handled;
}
}
10 changes: 10 additions & 0 deletions core/interop/comwrappers/IDispatch/ComWrappersIDispatch.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<Project Sdk="Microsoft.NET.Sdk.WindowsDesktop">

<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net5.0</TargetFramework>
<UseWindowsForms>true</UseWindowsForms>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>

</Project>
205 changes: 205 additions & 0 deletions core/interop/comwrappers/IDispatch/ComWrappersImpl.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,205 @@
#nullable enable

using System;
using System.Collections;
using System.Diagnostics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;

using ComTypes = System.Runtime.InteropServices.ComTypes;

internal unsafe class ComWrappersImpl : ComWrappers
{
private static readonly ComInterfaceEntry* wrapperEntry;

// This class only exposes IDispatch and the vtable is always the same.
// The below isn't the most efficient but it is reasonable for prototyping.
// If additional interfaces want to be exposed, add them here.
static ComWrappersImpl()
{
GetIUnknownImpl(out IntPtr fpQueryInteface, out IntPtr fpAddRef, out IntPtr fpRelease);

var vtbl = new IDispatchVtbl()
{
IUnknownImpl = new IUnknownVtbl()
{
QueryInterface = fpQueryInteface,
AddRef = fpAddRef,
Release = fpRelease
},
GetTypeInfoCount = Marshal.GetFunctionPointerForDelegate(IDispatchVtbl.pGetTypeInfoCount),
GetTypeInfo = Marshal.GetFunctionPointerForDelegate(IDispatchVtbl.pGetTypeInfo),
GetIDsOfNames = Marshal.GetFunctionPointerForDelegate(IDispatchVtbl.pGetIDsOfNames),
Invoke = Marshal.GetFunctionPointerForDelegate(IDispatchVtbl.pInvoke)
};
var vtblRaw = RuntimeHelpers.AllocateTypeAssociatedMemory(typeof(IDispatchVtbl), sizeof(IDispatchVtbl));
Marshal.StructureToPtr(vtbl, vtblRaw, false);

wrapperEntry = (ComInterfaceEntry*)RuntimeHelpers.AllocateTypeAssociatedMemory(typeof(IDispatchVtbl), sizeof(ComInterfaceEntry));
wrapperEntry->IID = typeof(IDispatch).GUID;
wrapperEntry->Vtable = vtblRaw;
}

protected override unsafe ComInterfaceEntry* ComputeVtables(object obj, CreateComInterfaceFlags flags, out int count)
{
Debug.Assert(obj is IDispatch);
Debug.Assert(wrapperEntry != null);

// Always return the same table mappings.
count = 1;
return wrapperEntry;
}

protected override object CreateObject(IntPtr externalComObject, CreateObjectFlags flags)
{
throw new NotImplementedException();
}

protected override void ReleaseObjects(IEnumerable objects)
{
throw new NotImplementedException();
}

[Guid("00020400-0000-0000-C000-000000000046")]
public interface IDispatch
{
void GetTypeInfoCount(out int pctinfo);

void GetTypeInfo(int iTInfo, int lcid, out IntPtr info);

void GetIDsOfNames(
ref Guid iid,
string[] names,
int cNames,
int lcid,
int[] rgDispId);

void Invoke(
int dispIdMember,
ref Guid riid,
int lcid,
ComTypes.INVOKEKIND wFlags,
ref ComTypes.DISPPARAMS pDispParams,
IntPtr VarResult,
IntPtr pExcepInfo,
IntPtr puArgErr);
}

public struct IUnknownVtbl
{
public IntPtr QueryInterface;
public IntPtr AddRef;
public IntPtr Release;
}

public struct IDispatchVtbl
{
public IUnknownVtbl IUnknownImpl;
public IntPtr GetTypeInfoCount;
public IntPtr GetTypeInfo;
public IntPtr GetIDsOfNames;
public IntPtr Invoke;

public delegate int _GetTypeInfoCount(IntPtr thisPtr, out int i);
public delegate int _GetTypeInfo(IntPtr thisPtr, int itinfo, int lcid, out IntPtr i);
public delegate int _GetIDsOfNames(
IntPtr thisPtr,
ref Guid iid,
[MarshalAs(UnmanagedType.LPArray, ArraySubType = UnmanagedType.LPWStr, SizeParamIndex = 3)]
string[] names,
int namesCount,
int lcid,
[Out][MarshalAs(UnmanagedType.LPArray, ArraySubType = UnmanagedType.I4, SizeParamIndex = 3)]
int[] dispIds);
public delegate int _Invoke(
IntPtr thisPtr,
int dispIdMember,
ref Guid riid,
int lcid,
ComTypes.INVOKEKIND wFlags,
ref ComTypes.DISPPARAMS pDispParams,
IntPtr VarResult,
IntPtr pExcepInfo,
IntPtr puArgErr);

public static _GetTypeInfoCount pGetTypeInfoCount = new _GetTypeInfoCount(GetTypeInfoCountInternal);
public static _GetTypeInfo pGetTypeInfo = new _GetTypeInfo(GetTypeInfoInternal);
public static _GetIDsOfNames pGetIDsOfNames = new _GetIDsOfNames(GetIDsOfNamesInternal);
public static _Invoke pInvoke = new _Invoke(InvokeInternal);

public static int GetTypeInfoCountInternal(IntPtr thisPtr, out int i)
{
i = 0;
try
{
var inst = ComInterfaceDispatch.GetInstance<IDispatch>((ComInterfaceDispatch*)thisPtr);
inst.GetTypeInfoCount(out i);
}
catch (Exception e)
{
return e.HResult;
}
return 0; // S_OK;
}

public static int GetTypeInfoInternal(IntPtr thisPtr, int itinfo, int lcid, out IntPtr i)
{
i = IntPtr.Zero;
try
{
var inst = ComInterfaceDispatch.GetInstance<IDispatch>((ComInterfaceDispatch*)thisPtr);
inst.GetTypeInfo(itinfo, lcid, out i);
}
catch (Exception e)
{
return e.HResult;
}
return 0; // S_OK;
}

public static int GetIDsOfNamesInternal(
IntPtr thisPtr,
ref Guid iid,
[MarshalAs(UnmanagedType.LPArray, ArraySubType = UnmanagedType.LPWStr, SizeParamIndex = 3)]
string[] names,
int namesCount,
int lcid,
[Out][MarshalAs(UnmanagedType.LPArray, ArraySubType = UnmanagedType.I4, SizeParamIndex = 3)]
int[] dispIds)
{
try
{
var inst = ComInterfaceDispatch.GetInstance<IDispatch>((ComInterfaceDispatch*)thisPtr);
inst.GetIDsOfNames(ref iid, names, namesCount, lcid, dispIds);
}
catch (Exception e)
{
return e.HResult;
}
return 0; // S_OK;
}

public static int InvokeInternal(
IntPtr thisPtr,
int dispIdMember,
ref Guid riid,
int lcid,
ComTypes.INVOKEKIND wFlags,
ref ComTypes.DISPPARAMS pDispParams,
IntPtr VarResult,
IntPtr pExcepInfo,
IntPtr puArgErr)
{
try
{
var inst = ComInterfaceDispatch.GetInstance<IDispatch>((ComInterfaceDispatch*)thisPtr);
inst.Invoke(dispIdMember, ref riid, lcid, wFlags, ref pDispParams, VarResult, pExcepInfo, puArgErr);
}
catch (Exception e)
{
return e.HResult;
}
return 0; // S_OK;
}
}
}
Loading

0 comments on commit e5db2d2

Please sign in to comment.