COM互操作和VARIANT(VT_PTR)的封送处理

3
我们使用第三方COM对象,其中一种方法在特定条件下返回类型为VT_PTRVARIANT。这会使默认的.NET封送处理器受到干扰,从而引发以下错误:

“InvalidVariant”托管调试助手:“在从非托管的VARIANT转换为托管对象期间检测到无效的VARIANT。将无效的VARIANT传递给CLR可能会导致意外的异常、损坏或数据丢失。”

方法签名:
// (Unmanaged) IDL:
HRESULT getAttribute([in] BSTR strAttributeName, [retval, out] VARIANT* AttributeValue);

// C#:
[return: MarshalAs(UnmanagedType.Struct)]
object getAttribute([In, MarshalAs(UnmanagedType.BStr)] string strAttributeName);

有没有一种优雅的方法来绕过这样的马歇尔行为,在托管端获取底层未托管指针?

到目前为止,我所考虑/尝试的:

  • A custom marshaler:

    [return: MarshalAs(UnmanagedType.CustomMarshaler, 
    MarshalTypeRef = typeof(IntPtrMarshaler))]
    object getAttribute([In, MarshalAs(UnmanagedType.BStr)] string strAttributeName);
    

    I did implement IntPtrMarshaler, just to find the interop layer crashing the process even before any of my ICustomMarshaler methods gets called. Perhaps, the VARIANT* argument type is not compatible with custom marshalers.

  • Rewrite (or clone) the C# interface definition with getAttribute method redefined (like below) and do all the marshaling for output VARIANT manually:

    void getAttribute(
        [In, MarshalAs(UnmanagedType.BStr)],
        string strAttributeName, 
        IntPtr result);
    

    This doesn't seem nice (the interface itself has 30+ other methods). It'd also break existing, unrelated pieces of code which already make use of getAttribute without issues.

  • Obtain an unmanaged method address of getAttribute from vtable (using Marshal.GetComSlotForMethodInfo etc), then do the manual invocation and marshaling against my own custom delegate type (using Marshal.GetDelegateForFunctionPointer etc).

    So far, I've taken this approach and it seem to work fine, but it feels as such an overkill for what should be a simple thing.

我是否忽略了其他可行的互操作选项?或者,也许有一种方法可以使 CustomMarshaler 在这里起作用吗?


它是一个双重/IDispatch接口吗? - Simon Mourier
1
因为它是双重接口(它们受到更严格的规则限制),在我看来,1)不会起作用,我也不知道你如何做2),所以如果3)运行良好,为什么不呢(你可以添加扩展方法到接口中,这是一种很好的方式来添加语法糖来使用它们)。我根本不认为这是一个“简单的事情”,因为第三方违反了VARIANT/Automation规则。https://msdn.microsoft.com/en-us/library/cc237562.aspx - Simon Mourier
@SimonMourier,我打算采取第三种。我很失望无法通过CustomMarshaler完成它,本应该是一件简单的事情。也许这是一个框架的错误。如果你有兴趣尝试一下,我很乐意提供一个自包含的C#/C++重现。 - noseratio - open to work
1
我其实已经尝试了 :-) 但我可以测试您的确切环境,事实上有一个重要的问题:您是否在相同的COM公寓中运行(因为如果存在封送处理,您基本上是注定要失败的,因为会崩溃的是通用封送处理程序oleaut)? - Simon Mourier
1
如果是同一间公寓,您可以像这样做:https://pastebin.com/a8qLBi1m - Simon Mourier
显示剩余6条评论
2个回答

2

我会定义一个简单的VARIANT结构,如下所示:

[StructLayout(LayoutKind.Sequential)]
public struct VARIANT
{
    public ushort vt;
    public ushort r0;
    public ushort r1;
    public ushort r2;
    public IntPtr ptr0;
    public IntPtr ptr1;
}

并且接口长这样;

[Guid("39c16a44-d28a-4153-a2f9-08d70daa0e22"), InterfaceType(ComInterfaceType.InterfaceIsDual)]
public interface MyInterface
{
    VARIANT getAttributeAsVARIANT([MarshalAs(UnmanagedType.BStr)] string strAttributeName);
}

接下来,在一个静态类中添加一个扩展方法,像这样,这样调用者就可以使用MyInterface获得相同的编码体验:

public static object getAttribute(this MyInterface o, string strAttributeName)
{
    return VariantSanitize(o.getAttributeAsVARIANT(strAttributeName));
}

private static object VariantSanitize(VARIANT variant)
{
    const int VT_PTR = 26;
    const int VT_I8 = 20;

    if (variant.vt == VT_PTR)
    {
        variant.vt = VT_I8;
    }

    var ptr = Marshal.AllocCoTaskMem(Marshal.SizeOf<VARIANT>());
    try
    {
        Marshal.StructureToPtr(variant, ptr, false);
        return Marshal.GetObjectForNativeVariant(ptr);
    }
    finally
    {
        Marshal.FreeCoTaskMem(ptr);
    }
}

这将对普通变量不起作用,但仅会为VT_PTR情况打补丁。
请注意,这仅在调用方和被调用方位于同一COM单元时才有效。
如果它们不在同一COM单元中,则会收到DISP_E_BADVARTYPE错误,因为必须进行封送处理,默认情况下,它将由COM通用封送程序(OLEAUT)执行,该程序仅支持自动化兼容数据类型 (就像.NET一样)。
在这种情况下,理论上,您可以通过另一个封送程序替换此封送程序(COM级别而非.NET级别),但这意味着需要在C++端添加一些代码,并可能在注册表(proxy/stub、IMarshal等)中进行一些设置。

1

为了日后参考,这是我最终使用问题中提到的第三个选项来完成它的方法:

[ComImport, Guid("75A67021-058A-4E2A-8686-52181AAF600A"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
public interface IInterface
{
    [return: MarshalAs(UnmanagedType.Struct)]
    object getAttribute([In, MarshalAs(UnmanagedType.BStr)] string strAttributeName);
}

private delegate int IInterface_getAttribute(
    IntPtr pInterface,
    [MarshalAs(UnmanagedType.BStr)] string name,
    IntPtr result);

public static object getAttribute(this IInterface obj, string name)
{
    var ifaceType = typeof(IInterface);
    var ifaceMethodInfo = ((Func<string, object>)obj.getAttribute).Method;
    var slot = Marshal.GetComSlotForMethodInfo(ifaceMethodInfo);
    var ifacePtr = Marshal.GetComInterfaceForObject(obj, ifaceType);
    try
    {
        var vtablePtr = Marshal.ReadIntPtr(ifacePtr);
        var methodPtr = Marshal.ReadIntPtr(vtablePtr, IntPtr.Size * slot);
        var methodWrapper = Marshal.GetDelegateForFunctionPointer<IInterface_getAttribute>(methodPtr);
        var resultVar = new VariantClass();
        var resultHandle = GCHandle.Alloc(resultVar, GCHandleType.Pinned);
        try
        {
            var pResultVar = resultHandle.AddrOfPinnedObject();
            VariantInit(pResultVar);
            var hr = methodWrapper(ifacePtr, name, pResultVar);
            if (hr < 0)
            {
                Marshal.ThrowExceptionForHR(hr);
            }
            if (resultVar.vt == VT_PTR)
            {
                return resultVar.ptr;
            }
            try
            {
                return Marshal.GetObjectForNativeVariant(pResultVar);
            }
            finally
            {
                VariantClear(pResultVar);
            }
        }
        finally
        {
            resultHandle.Free();
        }
    }
    finally
    {
        Marshal.Release(ifacePtr);
    }
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接