Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ComWrappers example using IDispatch #2873

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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