VARIANT(VT_PTR) 的 COM 互操作和编组

COM interop and marshaling of VARIANT(VT_PTR)

我们使用第 3 方 COM 对象,其中一种方法在某些条件下 returns VARIANT VT_PTR 类型。这会扰乱默认的 .NET 封送拆收器,从而引发以下错误:

Managed Debugging Assistant 'InvalidVariant' : 'An invalid VARIANT was detected during a conversion from an unmanaged VARIANT to a managed object. Passing invalid VARIANTs to the CLR can cause unexpected exceptions, corruption or data loss.

方法签名:

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

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

有没有一种优雅的方法可以绕过这种编组器的行为并在托管端获取底层的非托管指针?

到目前为止我 considered/tried:

对于这种情况,我是否遗漏了一些其他可行的互操作选项?或者,也许有办法让 CustomMarshaler 在这里工作?

我要做的是定义一个简单的 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);
}

然后,像这样在静态 class 中的某处添加一个扩展方法,这样调用者就可以使用 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 公寓中时有效。

如果不是,您将返回 DISP_E_BADVARTYPE 错误,因为必须进行编组,默认情况下,它将由仅支持 [=14= 的 COM 通用编组器 (OLEAUT) 完成](就像 .NET 一样)。

在这种情况下,从理论上讲,您可以用另一个封送拆收器替换这个封送拆收器(在 COM 级别,而不是在 NET 级别),但这意味着要在 C++ 端添加一些代码,并且可能在注册表中添加一些代码 (proxy/stub、IMarshal 等)。

为了我自己的未来参考,以下是我最终使用问题中提到的第三个选项的方式:

[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);
    }
}