Skip to content

Commit

Permalink
rewrote ComWrapperEnumerator to allow control over underlying IEnumVA…
Browse files Browse the repository at this point in the history
…RIANT RCW
  • Loading branch information
WaynePhillipsEA committed Jan 17, 2018
1 parent 4a6db52 commit 9e772a0
Showing 1 changed file with 116 additions and 12 deletions.
128 changes: 116 additions & 12 deletions Rubberduck.VBEEditor/SafeComWrappers/ComWrapperEnumerator.cs
@@ -1,39 +1,143 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices;
using ComTypes = System.Runtime.InteropServices.ComTypes;

namespace Rubberduck.VBEditor.SafeComWrappers
{
// The CLR automagically handles COM enumeration by custom marshalling IEnumVARIANT to a managed class (see EnumeratorToEnumVariantMarshaler)
// But as we need explicit control over all COM objects in RD, this is unacceptable. We need to obtain the explicit IEnumVARIANT interface
// and ensure this RCW is destroyed in a timely fashion, using Marshal.ReleaseComObject.
// The automatic custom marshalling of the enumeration getter method (DISPID_ENUM) prohibits access to the underlying IEnumVARIANT interface.
// To work around it, we must call the IDispatch:::Invoke method directly (instead of using CLRs normal late-bound method calling ability).

[Guid("00020400-0000-0000-C000-000000000046")]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
interface IDispatch
{
[PreserveSig] int GetTypeInfoCount([Out] out uint pctinfo);
[PreserveSig] int GetTypeInfo([In] uint iTInfo, [In] uint lcid, [Out] out ComTypes.ITypeInfo pTypeInfo);
[PreserveSig] int GetIDsOfNames([In] ref Guid riid, [In] string[] rgszNames, [In] uint cNames, [In] uint lcid, [Out] out int[] rgDispId);

[PreserveSig]
int Invoke([In] int dispIdMember,
[In] ref Guid riid,
[In] uint lcid,
[In] uint dwFlags,
[In, Out] ref ComTypes.DISPPARAMS pDispParams,
[Out] out Object pVarResult,
[In, Out] ref ComTypes.EXCEPINFO pExcepInfo,
[Out] out uint pArgErr);
}

class IDispatchHelper
{
public enum StandardDispIds : int
{
DISPID_ENUM = -4
}

public enum InvokeKind : int
{
DISPATCH_METHOD = 1,
DISPATCH_PROPERTYGET = 2,
DISPATCH_PROPERTYPUT = 4,
DISPATCH_PROPERTYPUTREF = 8,
}

public static object PropertyGet_NoArgs(IDispatch obj, int memberId)
{
var pDispParams = new ComTypes.DISPPARAMS();
object pVarResult;
var pExcepInfo = new ComTypes.EXCEPINFO();
uint ErrArg;
Guid guid = new Guid();

int hr = obj.Invoke(memberId, ref guid, 0, (uint)(InvokeKind.DISPATCH_METHOD | InvokeKind.DISPATCH_PROPERTYGET),
ref pDispParams, out pVarResult, ref pExcepInfo, out ErrArg);

if (hr < 0)
{
// could expand this to handle DISP_E_EXCEPTION
throw new ArgumentException(string.Format("CallSimpleMethodNoArgs Error invoking DispId {0}: COM error code is {1}", memberId, hr));
}

return pVarResult;
}
}

[Guid("00020404-0000-0000-C000-000000000046")]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
public interface IEnumVARIANT
{
// rgVar is technically an unmanaged array here, but we only ever call with celt=1, so this is compatible.
[PreserveSig] int Next([In] uint celt, [Out] out object rgVar, [Out] out uint pceltFetched);

[PreserveSig] int Skip([In] uint celt);
[PreserveSig] int Reset();
[PreserveSig] int Clone([Out] out IEnumVARIANT retval);
}

public class ComWrapperEnumerator<TWrapperItem> : IEnumerator<TWrapperItem>
where TWrapperItem : class
{
private readonly Func<object, TWrapperItem> _itemWrapper;
private readonly IEnumerator _internal;
private readonly IEnumVARIANT _enumeratorRCW;
private TWrapperItem _currentItem;

public ComWrapperEnumerator(IEnumerable source, Func<object, TWrapperItem> itemWrapper)
public ComWrapperEnumerator(object source, Func<object, TWrapperItem> itemWrapper)
{
_itemWrapper = itemWrapper;
_internal = source?.GetEnumerator() ?? Enumerable.Empty<TWrapperItem>().GetEnumerator();

if (source != null)
{
_enumeratorRCW = (IEnumVARIANT)IDispatchHelper.PropertyGet_NoArgs((IDispatch)source, (int)IDispatchHelper.StandardDispIds.DISPID_ENUM);
((IEnumerator)this).Reset(); // precaution
}
}

public void Dispose()
{
// nothing to dispose here
if (!IsWrappingNullReference) Marshal.ReleaseComObject(_enumeratorRCW);
}

public bool MoveNext()
void IEnumerator.Reset()
{
return _internal.MoveNext();
if (!IsWrappingNullReference)
{
int hr = _enumeratorRCW.Reset();
if (hr < 0)
{
throw new ArgumentException(string.Format("Error invoking IEnumVARIANT::Reset(). COM error code is {0}", hr));
}
}
}

public bool IsWrappingNullReference => _enumeratorRCW == null;

public TWrapperItem Current => _currentItem;
object IEnumerator.Current => _currentItem;

public void Reset()
bool IEnumerator.MoveNext()
{
_internal.Reset();
}
if (IsWrappingNullReference) return false;

_currentItem = null;

public TWrapperItem Current => _itemWrapper.Invoke(_internal.Current);
object currentItemRCW;
uint celtFetched;
int hr = _enumeratorRCW.Next(1, out currentItemRCW, out celtFetched);
// hr == S_FALSE (1) or S_OK (0), or <0 means error

object IEnumerator.Current => Current;
_currentItem = _itemWrapper.Invoke(currentItemRCW); // creates a null wrapped reference even on end/error, just as a precaution

if (hr < 0)
{
throw new ArgumentException(string.Format("Error invoking IEnumVARIANT::Next(). COM error code is {0}", hr));
}

return (celtFetched == 1); // celtFetched == 0 at end
}
}
}

0 comments on commit 9e772a0

Please sign in to comment.