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

时间:2018-03-16 14:42:02

标签: c# .net com interop com-interop

我们使用第三方COM对象,其中一种方法在某些条件下返回VARIANT VT_PTR类型。这会破坏默认的.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);

是否有一种优雅的方法可以绕过此类封送程序的行为并获取托管端的基础非托管指针?

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

  • 自定义封送程序:

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

    我确实实现了IntPtrMarshaler,只是为了找到互操作层甚至在调用任何ICustomMarshaler方法之前崩溃了。也许,VARIANT*参数类型与自定义封送程序不兼容。

  • 重新定义(或克隆)C#接口定义并重新定义getAttribute方法(如下所示)并手动完成输出VARIANT的所有编组:

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

    这看起来不太好(界面本身有30多种其他方法)。它还会破坏已经使用getAttribute而没有问题的现有的,不相关的代码片段。

  • 从vtable获取getAttribute的非托管方法地址(使用Marshal.GetComSlotForMethodInfo等),然后对我自己的自定义委托类型进行手动调用和封送(使用Marshal.GetDelegateForFunctionPointer等)。

    到目前为止,我已经采用了这种方法,它似乎工作得很好,但对于应该是一件简单的事情感觉有点过分。

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

2 个答案:

答案 0 :(得分: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区域时才有效。

如果不是,则会返回DISP_E_BADVARTYPE错误,因为必须完成编组,默认情况下,它将由仅支持Automation compatible data types的COM通用封送程序(OLEAUT)完成(就像.NET一样) )。

在这种情况下,理论上,你可以用另一个替换这个封送程序(在COM级别,而不是在.NET级别),但这意味着在C ++端添加一些代码,可能在注册表中添加一些代码(代理/存根,IMarshal)等等。)。

答案 1 :(得分: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);
    }
}