[译]在托管代码中使用“变体”类型

1。介绍

作者: LIM BIO LIONG  原文地址

1.1变体类型VARIANT是非托管COM开发中常用的结构。
1.2变体是指各种COM类型的通用容器(事实上,COM子系统的子集合中识别的每种类型称为OLE automation)。
1.3它在托管世界中的对应项是System.Object类型,它也是所有托管类型的通用容器。
1.4本博客是一篇改进的文章,它取代了早期的C#Interop:How to return a VARIANT from an Unmanaged Function。
1.5在本博客中,我旨在演示如何从非托管API接收变体。研究了两种技术(一种是低级的,另一种是高级的),并提供了一些确定潜在变体类型的技巧。
1.6为了演示目的,我们将使用C++实现非托管API,并使用C#来编码托管应用程序。

2。技术1:通过指针将非托管API中的VARIANT返回托管代码。

2.1我们要研究的第一种技术是低级技术。基本上,VARIANT是作为VARIANT指针返回的,API的等效C#声明必须指示IntPtr的返回类型。例如:

__declspec(dllexport) VARIANT* __stdcall MyFunction();

MyFunction()的C#声明如下:

[DllImport(@"MyDLL.dll", CallingConvention = CallingConvention.StdCall)]
static extern IntPtr MyFunction();

2.2在MyStury()的C++实现中,必须使用COTASKMeMalAcLoad() API或GualalLoCube()API来分配VARIANT的内存。然后返回指向此VARIANT的指针。下面是MySturk()的示例C++实现:

__declspec(dllexport) VARIANT* __stdcall MyFunction()
{
  VARIANT* pvarRet = (VARIANT*)CoTaskMemAlloc(sizeof(VARIANT));

  VariantInit(pvarRet);
  V_VT(pvarRet) = VT_BSTR;
  V_BSTR(pvarRet) = ::SysAllocString(L"MyFunction() return string.");

  return pvarRet;
}

上面的函数为VARIANT结构分配内存,然后将其设置为包含BSTR。
2.3注意,在C#侧,返回的IntPtr将永久指向VARIANT。但是,除非指向的VARIANT首先“转换”为托管对象,否则C#无法有意义地使用此IntPtr。
2.4请注意,我对转换使用了引号。实际上,IntPtr后面的VARIANT并没有被转换,而是从中创建了一个全新的托管对象。
2.5为此,我们使用Marshal.GetObjectForNativeVariant()函数实例化一个托管对象,该对象充当返回VARIANT中包含的数据的托管表示:

IntPtr pVariant = MyFunction();
object objRet = Marshal.GetObjectForNativeVariant(pVariant);

因此,基于上面点2.2列出的C++代码,“ObjReT”将包含托管字符串。
2.6现在注意MyFunction()返回一个指向C#客户机代码的VARIANT的指针。这意味着C#客户机代码拥有通过指针返回的VARIANT的内存。因此,客户端代码有责任确保最终释放该内存(请注意,释放该内存是必要的,因为它是非托管的,因此不受垃圾回收的影响)。
在这一点上,有一些重要的内存释放问题与VARIANT的使用相关,无论VARIANT是在托管代码中还是在非托管代码中。这些是:

  • 始终使用VARIANT clear()Win32 API清除返回的VARIANT中包含的数据。返回的VARIANT可能包含本身已分配内存的数据(例如BSTR、SAFEARRAY等)。如果是,VariantClear()将释放此内存。或者返回的VARIANT可能在内部包含指向COM对象的指针。如果是,则必须使用VariantClear()来执行适当的Release()。下面将提供用于此操作的实例代码。
  • IntPtr所指的VARIANT也必须释放内存。即,必须在C++代码中分配的先前内存的VARIANT结构本身必须被释放。为此,我们必须使用McSal.FrEcEdaskMeMe()函数,如果使用了MyTalk()和McSal.FrutHLULL()的C++实现,则使用COTASKMeMaskLoad()。

2.7因此,使用MyFunction()的完整C#代码示例应如下所示:

[DllImport(@"MyDLL.dll", CallingConvention = CallingConvention.StdCall)]
static extern IntPtr MyFunction();

[DllImport(@"oleaut32.dll", SetLastError = true, CallingConvention = CallingConvention.StdCall)]
static extern Int32 VariantClear(IntPtr pvarg);

private static void UseMyFunction()
{
  IntPtr pVariant = MyFunction();
  object objRet = Marshal.GetObjectForNativeVariant(pVariant);

  VariantClear(pVariant);
  Marshal.FreeCoTaskMem(pVariant);
  pVariant = IntPtr.Zero;
}

3。技术2:使用直接托管对象转换从非托管API返回VARIANT。

3.1第2节中给出的示例要求C#编码器完成相当低级的工作。

3.2除了调用MyFunction()API之外,UseMyFunction()还必须调用Marshal.GetObjectForNativeVariant()以从返回的IntPtr创建托管对象。之后,它必须负责清除VARIANT(通过VariantClear()),然后通过Marshal.FreeCoTaskMem()清除VARIANT本身占用的内存。

3.3这种低级方法的替代方法是使用interop封送拆收器的服务来执行对象转换和返回VARIANT的内存清除。

3.4我们假设下面的C++ API:

__declspec(dllexport) void __stdcall MyFunctionReturnObject(/*[out]*/ VARIANT* pVariantReceiver)
{
  VariantInit(pVariantReceiver);
  V_VT(pVariantReceiver) = VT_BSTR;
  V_BSTR(pVariantReceiver) = ::SysAllocString(L"MyFunction() return string.");
}

3.5 MyFunctionReturnObject()的C#声明如下:

[DllImport(@"MyDLL.dll", CallingConvention = CallingConvention.StdCall)]
static extern void MyFunctionReturnObject([Out] [MarshalAs(UnmanagedType.Struct)] out object pVariantAsObjectReceiver);

注意MarshalAsAttribute的用法:UnmanagedType.Struct参数表示在API调用的非托管端,需要一个VARIANT。由于参数被声明为“out”参数,所以VARIANT必须表示为指针。
3.6下面是对MyFunctionReturnObject()API的调用示例:

private static void MyFunctionReturnObject()
{
  object obj = null;

  MyFunctionReturnObject(out obj);

  Console.WriteLine("Object type : {0:S}.", obj.GetType().ToString());
  Console.WriteLine("Object value : {0:S}.", obj.ToString());
}

注意这个调用有多简单。在封面下,interop封送拆收器将执行以下操作:

  • 为VARIANT结构分配足够大的缓冲区。此结构不需要从非托管内存分配。它也可以从interop封送拆收器已经获取的某个临时工作缓冲区中获取。
  • 指向该缓冲区的指针随后传递给MyFunctionReturnObject()API。
  • API执行它的任务,初始化指向它认为是VARIANT的指针,并为它分配一个新的BSTR。
  • 当API返回时,interop封送拆收器将注意到 VARIANT 包含BSTR。
  • 它将使用BSTR的值构造一个托管字符串,然后释放这个BSTR。
  • 然后,新的托管字符串将被装箱在System.Object中。
  • 然后将恢复用于包含非托管VARIANT的缓冲区。请注意,如果这个缓冲区来自某个预先获取的临时工作内存,那么用于VARIANT的空间只会标记为可重用。

3.7 MyFunctionReturnObject() C#函数将生成以下预期的控制台输出:

Object type : System.String.
Object value : MyFunction() return string..

四。一个重要的限制。

4.1注意一个重要限制。无法将VT_RECORD变量返回到托管代码。
4.2如果对指向此类VARIANT的返回IntPtr调用了marshall.GetObjectForNativeVariant(),则将引发ArgumentException。
4.3因此,不可能返回包含用户定义类型(即结构)的变体。


5。托管对象独立于VARIANT。

5.1请注意,一旦从VARIANT创建了托管对象,托管对象就完全独立于从中构造它的VARIANT。
5.2 VARIANT中的数据仅用于创建等效的托管对象。VARIANT中的数据是并且将始终是非托管的。

6。确定VARIANT的VARIANT类型。

6.1检测VARIANT类型的能力是一种有用的编码结构。
6.2不幸的是,没有封送处理类方法可以帮助我们确定这一点。
6.3我个人采用了两种方法来实现这一点:

  • 通过非托管API。
  • 通过定义模仿VARIANT的托管结构。

这些技术将在下面的章节中阐述。

7。通过非托管API确定VARIANT类型。

7.1要通过非托管API确定VARIANT类型,我们可以使用以下简单的API:

__declspec(dllexport) VARTYPE __stdcall ReturnVariantType (/*[in]*/ VARIANT* pVariant)
{
  return V_VT(pVariant);
}

它接受指向VARIANT的指针,并使用V_VT()宏简单地返回VARIANT类型。
7.2下面是我们如何在C#语言中声明API

[DllImport(@"MyDLL.dll", CallingConvention = CallingConvention.StdCall)]
private static extern UInt16 ReturnVariantType([In] IntPtr pVariant);

7.3下面是一个示例函数,演示如何使用ReturnVariantType()。它改编自C# 方法UseMyFunction(),我们之前已经看到过:

private static void UseMyFunction_and_DetermineVariantType()
{
  IntPtr pVariant = MyFunction();
  object objRet = Marshal.GetObjectForNativeVariant(pVariant);

  VarEnum vt = (VarEnum)ReturnVariantType(pVariant);

  VariantClear(pVariant);
  Marshal.FreeCoTaskMem(pVariant);
  pVariant = IntPtr.Zero;
}

当上述代码运行时,vt将被设置为VarEnum.vt_BSTR。

8。通过托管VARIANT结构确定VARIANT类型。

8.1此技术依赖于我们在C#代码中定义类似于非托管变体的结构:

[StructLayout(LayoutKind.Sequential)]
public struct Variant
{
  public ushort vt;
  public ushort wReserved1;
  public ushort wReserved2;
  public ushort wReserved3;
  public Int32 data01;
  public Int32 data02;
}

就我们的目的而言,此VARIANT类型的托管版本的最重要字段是vt。
8.2其他字段对我们并不重要,因此我们使用blittable类型来表示它们。使用blittable类型可以确保,无论它们拥有什么值,它们都将始终被视为非引用类型。因此,即使它们的值实际上可以解释为指向bstr或safearray等的指针,它们也不需要释放内存。
8.3以下是一个示例C#函数,它使用托管VARIANT结构来确定VARIANT类型:

private static void UseMyFunction_and_DetermineVariantType2()
{
  IntPtr pVariant = MyFunction();
  object objRet = Marshal.GetObjectForNativeVariant(pVariant);

  Variant v = (Variant)Marshal.PtrToStructure(pVariant, typeof(Variant));
  VarEnum vt = (VarEnum)(v.vt);

  VariantClear(pVariant);
  Marshal.FreeCoTaskMem(pVariant);
  pVariant = IntPtr.Zero;
}

在上面的代码中,指向非托管VARIANT pVariant的指针用于创建托管VARIANT结构。这是通过调用Marshal.PtrToStructure()方法实现的。
8.4将使用非托管VARIANT结构中的值实例化托管VARIANT结构。因为Variant结构的所有字段都是blittable,所以非托管VARIANT的字节被blittable到这个托管结构。不需要解释非托管VARIANT的字段。
8.5因此,当托管VARIANT结构v完全初始化时,v.vt将包含VARIANT类型,就像非托管VARIANT一样。在上述示例中,v.vt将像往常一样设置为VarEnum.vt_BSTR。
8.6现在,如果我们看一下v结构的其他字段值,它的data01字段实际上将保存一个整数值,它实际上是指向在MyFunction()中分配的BSTR的指针。但是,由于data01被视为Int32,因此在调用Marshal.PtrToStructure()时不会分配新的托管字符串。

9。总之。

9.1我希望读者已经从这个关于在托管代码中使用VARIANT的讨论中受益。我们研究了如何从非托管代码接收非托管VARIANT。
9.2我们还讨论了如何构造VARIANT的托管版本,以便获得VARIANT类型。
9.3在第下一部分中,我将讨论如何从托管对象中创建非托管VARIANT结构以传递给非托管API。


查看评论

1。介绍

1.1在第一部分中,我们讨论了如何从非托管代码接收本机VARIANT。
1.2在第二部分中,我们将探讨将本机VARIANT从托管代码传递到非托管代码的技术。
1.3将研究低级和高级技术。我们还将深入了解interop封送拆收器的低级活动。
1.4,正如我们在第一部分中所做的,我们将使用C++实现非托管API,并使用C#来编码托管应用程序。

2。技术1:通过指针将VARIANT传递给非托管API。

2.1我们要研究的第一项技术是低级技术。VARIANT作为VARIANT指针传递给非托管API。下面是一个C++ API的例子,它采用了这样的参数:

__declspec(dllexport) void __stdcall DisplayVariant(/*[in]*/ const VARIANT* pVariant);

2.2 DisplayVariant()的C#声明如下:

[DllImport(@"MyDLL.dll", CallingConvention = CallingConvention.StdCall)]
private static extern void DisplayVariant([In] IntPtr pVariant);

2.3下面是DePiaValueTAN()的简单C++实现:

__declspec(dllexport) void __stdcall DisplayVariant(/*[in]*/ const VARIANT* pVariant)
{
  if (V_VT(pVariant) == VT_BSTR)
  {
    printf ("BSTR contained inside variant : [%S].\r\n", V_BSTR(pVariant));
  }
}

此函数将指向VARIANT的指针作为“in”参数。输入VARIANT必须视为只读。因此使用const关键字。

函数测试输入VARIANT是否包含BSTR。如果是,它将在控制台输出上显示BSTR。

2.4下面是调用DisplayVariant()的示例C#客户机代码:

const Int32 SizeOfNativeVariant = 16;

private static void DisplayVariant()
{
  string str = "Hello World";
  IntPtr pVariant = IntPtr.Zero;

  // Allocate in unmanaged memory a block of space
  // the size of a native VARIANT.
  pVariant = Marshal.AllocHGlobal(SizeOfNativeVariant);

  // Use the string inside str to construct
  // a native VARIANT that contains a BSTR
  // with the same value as str.
  Marshal.GetNativeVariantForObject(str, pVariant);

  // Call the API.
  DisplayVariant(pVariant);

  // Clear the contents of the unmanaged VARIANT.
  VariantClear(pVariant);
  // Free the space occuppied by the unmanaged VARIANT.
  Marshal.FreeHGlobal(pVariant);
  pVariant = IntPtr.Zero;
}

注意,我包含了一个常量整数声明SizeOfNativeVariant(等于16),用于为VARIANT结构分配字节数。
以下是与上述代码相关的几点:

  • 创建并初始化托管字符串str。
  • 使用Marshal.AllocHGlobal()在非托管内存中分配本机VARIANT。
  • str中的字符串用于初始化本机VARIANT以包含与str具有相同值的BSTR。这是通过使用Marshal.GetNativeVariantForObject()完成的。
  • 然后调用DisplayVariant() API,并将指向VARIANT的指针作为参数传递。
  • 注意,VARIANT作为“in”只读参数传递给API。因此,预计API不会修改它,也不会释放它。VARIANT仍归调用方所有,调用方负责释放它。
  • 在调用API之后,我们使用VARIANT clear()清除非托管VARIANT的内容。变体持有的BSTR现在将被释放。
  • 此后,使用Marshal.FreeHGlobal()释放非托管VARIANT占用的内存空间。

2.5上面的代码演示了我们如何控制VARIANT处理的每个步骤:从内存分配(Marshal.AllocHGlobal())到初始化(Marshal.GetNativeVariantForObject())到内容清除(VariantClear())再到最终释放(Marshal.FreeHGlobal())。
2.6下一节将演示一种通过使用高级方法实现相同目的的技术。

3。技术2:通过直接VARIANT转换将System.Object传递给非托管API。

3.1在本节中,我们提供了一种将VARIANT传递给非托管API的简单方法。下面是这样一个C++ API的例子:

__declspec(dllexport) void __stdcall DisplayVariantAsObject(/*[in]*/ const VARIANT var)
{
  if (V_VT(&var) == VT_BSTR)
  {
    printf ("BSTR contained inside variant : [%S].\r\n", V_BSTR(&var));
  }
}

在脚本中,API执行的操作与前面的DisplayVariant() API相同。但是,这次,VARIANT参数不是指针。
3.2现在我们如何在C#中声明这样的API?下面是一个例子:

[DllImport(@"MyDLL.dll", CallingConvention = CallingConvention.StdCall)]
private static extern void DisplayVariantAsObject([In] [MarshalAs(UnmanagedType.Struct)] object var);

注意,参数实际上是一个System.Object类型。问题在于,VARIANT仍然会传递给非托管API。
神奇之处在于将MarshalAsAttribute与非托管类型.Struct参数一起使用,该参数指示interop封送拆收器使用System.Object参数var构造非托管VARIANT,然后将其作为参数传递给API。稍后将详细介绍。
3.3以下是调用DisplayVariantAsObject() API的示例C#代码:

private static void DisplayVariantAsObject()
{
  string str = "Hello World";

  DisplayVariantAsObject(str);
}

看它有多优雅简单。
3.4以下是对整个API调用过程中发生的背后事件的总结:

  • 因为API需要一个完整的VARIANT参数(而不是指向它的指针),所以这个参数在堆栈上传递。
  • 因此,interop封送拆收器使用堆栈内存构造所需的VARIANT结构。
  • interop封送拆收器使用str字符串并在其他一些内存区域中从中构造BSTR。这是使用SysAllocStringLen() API完成的。
  • 然后,使用其VARIANT type字段设置为VT_BSTR并将其内部数据指向分配的BSTR来初始化VARIANT。然后调用DisplayVariantAsObject() API。
  • 当API返回时,interop封送拆收器对VARIANT调用VariantClear()。因此,通过调用SysFreeString()可以清除BSTR。
  • 然后,因为VARIANT是在堆栈上构造的,所以它的内存会自动恢复。

3.5此技术干净、优雅,使用interop封送拆收器的服务自动处理低级活动。

4。对用户定义的类型有什么限制吗?

4.1对于通过VARIANT指针或对象通过VARIANT将结构(用户定义类型或UDT)从托管代码传递到非托管代码没有限制。
4.2有关此示例,请参阅:互操作COM结构。
4.3但是,请注意,不可能从非托管代码返回VARIANT(包含UDT)。这在第一部分中已经演示过。

5。总之。

5.1在这个博客中,我介绍了两种将VARIANT传递给非托管代码的技术。
5.2低级方法是不直观的,但模式是可以观察到的。非托管VARIANT与其原始托管对象是一个单独的实体,这是理解其工作原理的关键。
5.3高级技术在简单性方面很好。大部分的艰苦工作都是由互操作封送拆收器完成的。明智地使用MarshalAsAttribute为其成功铺平了道路。
5.4在第三部分中,我将通过引用说明如何将VARIANT与非托管代码交换。也就是说,VARIANT被传递到非托管代码中,然后可能在返回托管代码之前被修改。


查看评论

1。介绍

1.1在第一部分中,我们讨论了如何从非托管代码接收本机VARIANT。
1.2在第二部分中,我们探讨了将本机VARIANT从托管代码传递到非托管代码的技术。
1.3在第三部分中,我们将研究通过引用将本机VARIANT(从托管代码创建)传递到非托管代码的技术。
1.4这样的VARIANT将其原始值封送至非托管代码。然后,它将被还原回具有可能修改的值的托管代码。
1.5正如我们在第1部分和第2部分中所做的,我们将探索实现这一目标的低级和高级技术。我们还将继续使用C++实现非托管API,并使用C语言对托管应用程序进行编码。

2。技术1:通过指针通过引用向非托管API传递VARIANT。

2.1再次,我们从低级技术开始。VARIANT作为VARIANT指针通过引用传递给非托管API。下面是一个C++ API的例子,它采用了这样的参数:

__declspec(dllexport) void __stdcall ChangeVariant(/*[in, out]*/ VARIANT* pVariant, /*[in]*/ BOOL bChangeType);

请注意,pVariant参数不是用“const”关键字声明的,这意味着允许API对其进行更改。bcchangetype参数指示是否也要更改VARIANT的VARIANT类型,从而使VARIANT保持与原始 VARIANT 不同的类型(参见下面的第2.3点)。

2.2 ChangeVariant()的C#声明如下:

[DllImport(@"MyDLL.dll", CallingConvention = CallingConvention.StdCall)]
private static extern void ChangeVariant(IntPtr pVariant, [MarshalAs(UnmanagedType.Bool)] bool bChangeType);

以下是关于上述声明的一些重要说明:

  • 第一个参数pVariant被声明为没有marshalsattributes的IntPtr。因此,互操作封送拆收器在没有解释的情况下传递pVariant。
  • pVariant上也没有使用“ref”关键字,这意味着pVariant的值作为一个数字按原样传递给API。
  • 如果使用了“ref”关键字,则意味着我们正在传递指向可能需要取消分配的VARIANT的指针。然后,需要用新的值重新分配一个全新的变体结构。
  • 在这种情况下,第2.1点中的ChangeVariant()API声明需要将其第一个参数声明为指向VARIANT(即VARIANT**)的双指针。
  • 这不是必需的,因为我们只想更改原始变体的内容。因此,只需要一个指向VARIANT的指针。以及非ref IntPtr对应的C#参数声明。

稍后第2.4点将对此进行详细说明。

2.3下面是C++的简单实现

__declspec(dllexport) void __stdcall ChangeVariant(/*[in, out]*/ VARIANT* pVariant, /*[in]*/ BOOL bChangeType)
{
  if (V_VT(pVariant) == VT_BSTR)
  {
    printf ("Before Change : V_VT(pVariant)   : [%d].\r\n", V_VT(pVariant));
    printf ("Before Change : V_BSTR(pVariant) : [%S].\r\n", V_BSTR(pVariant));

    VariantClear(pVariant);

    if (bChangeType)
    {
      V_VT(pVariant) = VT_I4;
      V_I4(pVariant) = 100;
    }
    else
    {
      V_VT(pVariant) = VT_BSTR;
      V_BSTR(pVariant) = ::SysAllocString(L"New string");
    }
  }
}

以下是关于此API的相关要点:

  • API测试输入VARIANT是否包含BSTR。如果是,则显示BSTR的值。
  • 然后清除变体。因此,最初持有的BSTR被释放。
  • 然后,根据boolean参数bChangeType的值,它要么完全更改VARIANT以保持值100的整数,要么让VARIANT保持BSTR保持器,但为其分配一个新字符串。

2.4以下是通过低级技术调用ChangeVariant()的示例C#客户机代码:

const Int32 SizeOfNativeVariant = 16;

private static void ChangeVariant()
{
  string str = "Old string";
  IntPtr pVariant = IntPtr.Zero;

  // Allocate in unmanaged memory a block of space
  // the size of a native VARIANT.
  pVariant = Marshal.AllocHGlobal(SizeOfNativeVariant);

  // Use the string inside str to construct
  // a native VARIANT that contains a BSTR
  // with the same value as str.
  Marshal.GetNativeVariantForObject(str, pVariant);

  // Call the API.
  ChangeVariant(pVariant, true);

  // Convert the contents of the VARIANT (which may
  // now have changed) back into a System.Object.
  object new_object = Marshal.GetObjectForNativeVariant(pVariant);

  Console.WriteLine("After Change : new_object type  : {0:S}.", new_object.GetType().ToString());
  Console.WriteLine("After Change : new_object value : {0:S}.", new_object.ToString());

  // Clear the contents of the unmanaged VARIANT.
  VariantClear(pVariant);
  // Free the space occuppied by the unmanaged VARIANT.
  Marshal.FreeHGlobal(pVariant);
  pVariant = IntPtr.Zero;
}

以下是与上述代码相关的要点:

  • 创建并初始化托管字符串str。
  • 使用Marshal.AllocHGlobal()在非托管内存中分配本机VARIANT。
  • str中的字符串用于初始化本机VARIANT以包含与str具有相同值的BSTR。这是通过使用Marshal.GetNativeVariantForObject()完成的。
  • 然后调用ChangeVariant()API。现在回想一下,从第2.2点开始,VARIANT按原样作为IntPtr传递,互操作封送拆收器不进行任何操作。
  • 因此,由于IntPtr是非托管VARIANT的地址,因此调用API时会将指向VARIANT的指针作为参数传递。
  • API显示原始类型和值,然后清除VARIANT。
  • 然后,因为我们已经将true作为第二个参数传递,所以API将VARIANT类型更改为VT_I4(4字节有符号整数值),并将100指定为该值。
  • 当API返回时,我们通过调用Marshal.GetObjectForNativeVariant()从现在更改的VARIANT构造一个全新的对象。注意:一个全新的对象(new_object)是从VARIANT创建的。
  • 不会以任何方式接触原始str字符串。
  • 将显示新对象的类型和值。
  • 然后,首先清除本机VARIANT的内容(使用VariantClear()),然后恢复其自身的内存空间(使用marshall.FreeHGlobal()),从而释放本机VARIANT。

请注意,字符串str和object new_对象是托管对象,因此会受到垃圾收集的影响。我们不需要担心他们的存储恢复。
2.5控制台将显示以下输出:

Before Change : V_VT(pVariant)   : [8].
Before Change : V_BSTR(pVariant) : [Old string].
After Change : new_object type  : System.Int32.
After Change : new_object value : 100.

注意,数字8是VT_BSTR的值(即在VT_BSTR VARIANT 上使用时V_V()的输出)。
2.6注意,由于返回的pVariant可能会从保持BSTR变为整数,因此我们不能使用如下代码:

// Do not do this :
str = (string)Marshal.GetObjectForNativeVariant(pVariant);

如果pVariant更改为保存VT_I4,则将抛出System.InvalidCastException。

3。技术2:通过引用具有直接VARIANT转换的非托管API传递System.Object。

3.1在本节中,我们提供了通过引用非托管API传递VARIANT的一种非常简单的高级方法。实际上,这个API与我们在第2节中使用的API相同。

__declspec(dllexport) void __stdcall ChangeVariant(/*[in, out]*/ VARIANT* pVariant, /*[in]*/ BOOL bChangeType);

3.2区别在于我们在C#代码中声明此API的方式:

[DllImport(@"MyDLL.dll", CallingConvention = CallingConvention.StdCall, EntryPoint = "ChangeVariant")]
private static extern void ChangeVariantAsObject
(
  [In][Out][MarshalAs(UnmanagedType.Struct)] ref object var,
  [MarshalAs(UnmanagedType.Bool)] bool bChangeType
);

以下是对于声明的要点总结:

  • 因为我们将使用相同的API(尽管其参数使用不同的类型),所以我们使用DllImportAttribute的EntryPoint参数向interop封送拆收器指示要调用的API实际上是ChangeVariant()。
  • 第一个参数现在是要通过引用传递的System.Object类型。
  • 这意味着对象VARIANT可能会被封送到API中,也可能被封送出API。
  • 与UnmanagedType.Struct参数一起使用的MarshalAsAttribute指示interop封送拆收器将对象VARIANT封送为VARIANT。然后,因为var将通过引用传递,所以该VARIANT将作为指针传递。

3.3以下是调用ChangeVariantAsObject() API的示例C#代码:

private static void ChangeVariantAsObject()
{
  object obj = (string)("Old string");

  ChangeVariantAsObject(ref obj, false);

  Console.WriteLine("After Change : obj type  : {0:S}.", obj.GetType().ToString());
  Console.WriteLine("After Change : obj value : {0:S}.", obj.ToString());
}

3.4以下是对整个API调用过程中发生的背后事件的总结:

  • interop封送拆收器将为非托管VARIANT结构分配内存空间。
  • 这个内存不需要总是新分配的。它可能来自预先分配的工作内存缓存。
  • 然后,interop封送拆收器将使用来自对象obj的数据初始化VARIANT。
  • 因为obj包含一个字符串,所以将分配一个BSTR(通过SysAllocStringLen())并且VARIANT将指向这个BSTR。
  • 然后将调用API。与第2节中给出的示例一样,指向VARIANT的指针将作为参数传递。
  • API执行其显示当前BSTR、清除VARIANT的例程,然后,由于将false作为第二个参数传递,VARIANT被重新分配为BSTR的持有者,并为其分配新的BSTR。
  • 当API返回时,interop封送拆收器将使用VARIANT中包含的BSTR重新构造对象obj。
  • 然后,interop封送拆收器将对VARIANT调用VariantClear(),因为它包含一个BSTR,所以将调用SysFreeString()来释放它。

3.5控制台将显示以下预期输出:

Before Change : V_VT(pVariant)   : [8].
Before Change : V_BSTR(pVariant) : [Old string].
After Change : obj type  : System.String.
After Change : obj value : New string.

4。限制。

4.1通过VARIANT传递结构(用户定义类型或UDT)的限制适用于通过引用传递VARIANT的情况。
4.2如果使用Marshal.AllocHGlobal()动态分配本机VARIANT,则调用Marshal.GetObjectForNativeVariant()将本机VARIANT转换回结构时将引发异常。
4.3如果API接受用MarshalAsAttribute修饰的按引用对象VARIANT,例如:

private static extern void ChangeRecordVariantAsObject([In][Out][MarshalAs(UnmanagedType.Struct)] ref object var);

5。总之。

5.1因此,我通过引用介绍了在托管和非托管代码之间交换本机变体的各种技术。

5.2在整个低级示例中,我试图模仿interop封送拆收器执行其外观工作的方式。

5.3一旦牢牢掌握了底层概念,就可以放心地使用高层代码结构。


发表评论

邮箱地址不会被公开。 必填项已用*标注

10 − 3 =