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:
自定义编组器:
[return: MarshalAs(UnmanagedType.CustomMarshaler,
MarshalTypeRef = typeof(IntPtrMarshaler))]
object getAttribute([In, MarshalAs(UnmanagedType.BStr)] string strAttributeName);
我确实实现了 IntPtrMarshaler
,只是为了发现互操作层 甚至在我的任何 ICustomMarshaler
方法被调用 之前就崩溃了。也许,VARIANT*
参数类型与自定义封送拆收器不兼容。
使用 getAttribute
方法重新定义(如下所示)重写(或克隆)C# 接口定义,并手动执行输出 VARIANT
的所有封送处理:
void getAttribute(
[In, MarshalAs(UnmanagedType.BStr)],
string strAttributeName,
IntPtr result);
这看起来不太好(接口本身有 30 多个其他方法)。它还会破坏现有的、不相关的代码片段,这些代码片段已经毫无问题地使用 getAttribute
。
从 vtable(使用 Marshal.GetComSlotForMethodInfo
等)获取 getAttribute
的非托管方法地址,然后针对我自己的自定义委托类型(使用 Marshal.GetDelegateForFunctionPointer
等)。
到目前为止,我采用了这种方法,它似乎工作正常,但对于本应简单的事情来说,感觉太过分了。
对于这种情况,我是否遗漏了一些其他可行的互操作选项?或者,也许有办法让 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);
}
}
我们使用第 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:
自定义编组器:
[return: MarshalAs(UnmanagedType.CustomMarshaler, MarshalTypeRef = typeof(IntPtrMarshaler))] object getAttribute([In, MarshalAs(UnmanagedType.BStr)] string strAttributeName);
我确实实现了
IntPtrMarshaler
,只是为了发现互操作层 甚至在我的任何ICustomMarshaler
方法被调用 之前就崩溃了。也许,VARIANT*
参数类型与自定义封送拆收器不兼容。使用
getAttribute
方法重新定义(如下所示)重写(或克隆)C# 接口定义,并手动执行输出VARIANT
的所有封送处理:void getAttribute( [In, MarshalAs(UnmanagedType.BStr)], string strAttributeName, IntPtr result);
这看起来不太好(接口本身有 30 多个其他方法)。它还会破坏现有的、不相关的代码片段,这些代码片段已经毫无问题地使用
getAttribute
。从 vtable(使用
Marshal.GetComSlotForMethodInfo
等)获取getAttribute
的非托管方法地址,然后针对我自己的自定义委托类型(使用Marshal.GetDelegateForFunctionPointer
等)。到目前为止,我采用了这种方法,它似乎工作正常,但对于本应简单的事情来说,感觉太过分了。
对于这种情况,我是否遗漏了一些其他可行的互操作选项?或者,也许有办法让 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);
}
}