65.9K
CodeProject 正在变化。 阅读更多。
Home

通过 COM 互操作 .NET 和 C++

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.97/5 (23投票s)

2017 年 7 月 11 日

CPOL

15分钟阅读

viewsIcon

38112

downloadIcon

372

创建 C# 中的 COM 可见类型并在 C++ 中使用它们

引言

将原生组件与 .NET 组件协同工作是很常见的情况。当您需要在原生代码中消费托管组件时,基本有两种选择:通过 C++/CLI 编写的混合模式组件,或者通过 COM。本文将讨论后一种方法,并通过一些关键部分介绍其机制,帮助您入门。在本文中,您将学到:

  • 在 C# 中编写 COM 可见接口和类,并通过 COM 公开它们
  • 在 C++ 中导入类型库
  • 使用 COM 智能指针来消费 COM 组件
  • 理解过程中创建的各种类型库文件
  • 理解 C# 和 C++ 之间的类型封送
  • 处理封送的数组
  • 处理封送的接口

创建 .NET 进程内 COM 服务器

首先,我们需要创建一个 .NET 类库项目,它将代表一个进程内 COM 服务器。在此库中,我们将首先添加一个名为 ITest 的接口和一个实现它的名为 Test 的类。最简单的形式如下:

namespace ManagedLib
{
   [Guid("D3CE54A2-9C8D-4EA0-AB31-2A97970F469A")]
   [InterfaceType(ComInterfaceType.InterfaceIsIDispatch)]
   [ComVisible(true)]
   public interface ITest
   {
      [DispId(1)]
      void TestBool(bool b);
      
      [DispId(4)]
      void TestSignedInteger(sbyte b, short s, int i, long l);
   }
   
   [Guid("A7A5C4C9-F4DA-4CD3-8D01-F7F42512ED04")]
   [ClassInterface(ClassInterfaceType.None)]
   [ComVisible(true)]
   [ProgId("ManagedLib.Test")]
   public class Test : ITest
   {
      public void TestBool(bool b)
      {
         Console.WriteLine($"bool:   {b}");
      }
   
      public void TestSignedInteger(sbyte b, short s, int i, long l)
      {
         Console.WriteLine($"sbyte:  {b}");
         Console.WriteLine($"short:  {s}");
         Console.WriteLine($"int:    {i}");
         Console.WriteLine($"long:   {l}");
      }   
   }   
}

定义接口和实现类并没有什么特别之处,除了用于接口/类和方法的几个属性,这些属性控制着这些实体如何暴露给 COM。

GuidAttribute 指定定义接口或类唯一标识符的 GUID。
ComVisibleAttribute 控制类型或成员对 COM 的可访问性。通过将可见性参数设置为 true,我们指示该类型或成员对 COM 是可见的。
InterfaceTypeAttribute 指定托管接口暴露给 COM 时是什么类型的 COM 接口。在此示例中指定的选项是 InterfaceIsIDispatch,这意味着该接口是 dispinterface。这仅启用后期绑定,接口的方法和属性不包含在接口的 VTBL 中,只能通过 IDispatch::Invoke() 访问。
ClassInterfaceAttribute 指定为 COM 类生成哪种类型的接口。上面指定的 None 选项表示该类通过 IDispatch 接口提供后期绑定访问,并且不为该类生成类接口。
DispIdAttribute 指定方法、属性或字段的 COM 调度 ID。
ProgIdAttribute 允许指定编程 ID,这是一个 COM 类的用户友好名称,并且必须在系统中唯一(就像类 ID 一样)。

下一步是在 **项目属性** > **生成** 中注册类库以进行 COM 互操作。这可以通过 regasm.exe 手动完成,但通过选中项目设置中的此选项,Visual Studio 将使用 /tlb/codebase 选项运行此工具。regasm.exe 会执行 COM 库正常工作所需的所有注册。使用 /tlb 选项,它还会生成一个类型库文件(.tlb),其中包含 COM 可见程序集类型的定义。使用 /codebase 选项,它从您的项目目录而不是 GAC 执行注册。

在 C++ 中导入类型库

类型库是一个二进制文件,其中包含有关 COM 接口、方法和属性的信息。这些信息在运行时可供其他应用程序访问。在 VC++ 中,可以根据这些信息生成 C++ 类,从而为 COM 组件提供早期绑定。这可以通过使用 #import 指令来实现。

#import "ManagedLib.tlb"

通用形式是 #import filename [attributes],其中文件名可以是类型库(.tlb, .olb, .dll)、可执行文件、包含类型库资源的库(.ocx)、类型库中控件的编程 ID、类型库的库 ID,或者 LoadTypeLib 可以理解的任何其他格式。属性是可选的,它们控制结果头文件的内容。有关 import 指令工作方式的详细信息,请查阅其 MSDN 文档

导入类型库的结果是两个头文件,其名称与类型库文件相同,但扩展名不同

  • .TLH (类型库头文件) 包含一个头和尾、前向引用和 typedefs、智能指针声明、typeinfo 声明、#include 语句或辅助头文件,以及其他部分。
  • .TLI (类型库实现) 包含编译器生成的成员函数和属性的实现。

上面针对前面 C# 代码的 import 指令的结果是以下头文件

  • managedlib.tlh
    // Created by Microsoft (R) C/C++ Compiler Version 14.00.24215.1 (d73829de).
    //
    // c:\comdemo\nativeclient\debug\managedlib.tlh
    //
    // C++ source equivalent of Win32 type library ManagedLib.tlb
    // compiler-generated file created 07/04/17 at 19:04:17 - DO NOT EDIT!
    
    #pragma once
    #pragma pack(push, 8)
    
    #include <comdef.h>
    
    namespace ManagedLib {
    
    //
    // Forward references and typedefs
    //
    
    struct __declspec(uuid("56418cab-6e5e-41c7-b477-e3b5c250d879"))
    /* LIBID */ __ManagedLib;
    struct __declspec(uuid("d3ce54a2-9c8d-4ea0-ab31-2a97970f469a"))
    /* dispinterface */ ITest;
    struct /* coclass */ Test;
    
    //
    // Smart pointer typedef declarations
    //
    
    _COM_SMARTPTR_TYPEDEF(ITest, __uuidof(ITest));
    
    //
    // Type library items
    //
    
    struct __declspec(uuid("d3ce54a2-9c8d-4ea0-ab31-2a97970f469a"))
    ITest : IDispatch
    {
        //
        // Wrapper methods for error-handling
        //
    
        // Methods:
        HRESULT TestBool (
            VARIANT_BOOL b );
        HRESULT TestSignedInteger (
            char b,
            short s,
            long i,
            __int64 l );
    };
    
    struct __declspec(uuid("a7a5c4c9-f4da-4cd3-8d01-f7f42512ed04"))
    Test;
        // interface _Object
        // [ default ] dispinterface ITest
    
    //
    // Wrapper method implementations
    //
    
    #include "c:\comdemo\nativeclient\debug\managedlib.tli"
    
    } // namespace ManagedLib
    
    #pragma pack(pop)
  • managedlib.tli
    // Created by Microsoft (R) C/C++ Compiler Version 14.00.24215.1 (d73829de).
    //
    // c:\comdemo\nativeclient\debug\managedlib.tli
    //
    // Wrapper implementations for Win32 type library ManagedLib.tlb
    // compiler-generated file created 07/04/17 at 19:04:17 - DO NOT EDIT!
    
    #pragma once
    
    //
    // dispinterface ITest wrapper method implementations
    //
    
    inline HRESULT ITest::TestBool ( VARIANT_BOOL b ) {
        return _com_dispatch_method(this, 0x1, DISPATCH_METHOD, VT_EMPTY, NULL, 
            L"\x000b", b);
    }
    
    inline HRESULT ITest::TestSignedInteger ( char b, short s, long i, __int64 l ) {
        return _com_dispatch_method(this, 0x4, DISPATCH_METHOD, VT_EMPTY, NULL, 
            L"\x0011\x0002\x0003\x0014", b, s, i, l);
    }

.tlh 头文件中,在我们的例子中最重要的是两件事

  • 以形式声明的智能指针
    _COM_SMARTPTR_TYPEDEF(ITest, __uuidof(ITest));
    
    _COM_SMARTPTR_TYPEDEF 是一个宏,它展开为以下内容
    typedef _com_ptr_t<_com_IIID<ITest, __uuidof(ITest)> > ITestPtr; 

    _com_ptr_t 是一个智能指针实现,它隐藏了调用 CoCreateInstance() 来创建 COM 对象,封装了接口指针,并消除了调用 AddRef()Release()QueryInterface() 函数的需要。

  • ITest 类,这是一个 C++ 类,它模拟了 ITest COM 接口。 ITestPtr 是一个智能指针,应该用来代替 ITest*

另一方面,.tli 头文件包含所有 COM 接口方法的实现。这些方法使用 _com_dispatch_method()_com_dispatch_propget()_com_dispatch_method(),它们在内部调用 IDispath::Invoke() 以及可能的 comdef.h 中的其他函数。我们在此示例中看到的 _com_dispatch_method() 函数具有以下签名

HRESULT __cdecl
    _com_dispatch_method(IDispatch*, DISPID, WORD, VARTYPE, void*,
                         const wchar_t*, ...) ;

参数如下面的表格所示。考虑的示例是函数 TestSignedInteger()

参数类型 来自示例 注释
IDispatch* this 指向 IDispatch 接口的指针
DISPID 0x4 接口成员的调度标识符
WORD DISPATCH_METHOD 描述 Invoke() 调用上下文的标志
VARTYPE VT_EMPTY 返回值的类型
void* NULL 指向用于存储结果的位置的指针,如果不需要结果则为 NULL
const wchar_t* L"\x0011\x0002\x0003\x0014" 指向宽字符数组的指针,表示每个输入参数的类型。每个值都有一个以 \x 开头的十六进制值的字符串表示。例如,\x0011 是十进制 17,即 VT_UI1(即无符号 8 位整数),而 0x0002 是十进制 2,即 VT_I2(即有符号 16 位整数)。
... (省略号) b, s, i, l COM 接口函数的输入参数的可变列表。

从 C++ 消费 COM 组件

有了通过导入类型库创建的辅助代码,从 C++ 消费 COM 组件就相对简单了。我们需要做的是:

  • 初始化当前线程的 COM 库(并在不再需要时正确地取消初始化)。
  • 创建智能指针实例。
  • 通过 COM 指针实例化 COM coclass。为此,我们可以使用类 ID(以 GUID 或由 {} 分隔的字符串形式,例如 L"{A7A5C4C9-F4DA-4CD3-8D01-F7F42512ED04}")或编程 ID。
  • 调用 COM 接口中的方法。
  • 妥善处理可能传播到客户端的 COM 错误,这些错误被封装在 _com_error 异常中。

以下示例演示了所有这些步骤,通过实例化 Test coclass 并通过 ITest COM 接口调用方法。

#include <iostream>
#import "ManagedLib.tlb"

struct COMRuntime
{
   COMRuntime() { CoInitialize(NULL); }
   ~COMRuntime() { CoUninitialize(); }
};

int main()
{
   COMRuntime runtime;
   ManagedLib::ITestPtr ptr;
   //ptr.CreateInstance(L"{A7A5C4C9-F4DA-4CD3-8D01-F7F42512ED04}");
   ptr.CreateInstance(L"ManagedLib.Test");
   if (ptr != nullptr)
   {
      try
      {   
         ptr->TestBool(true);
         ptr->TestSignedInteger(CHAR_MAX, SHRT_MAX, INT_MAX, MAXLONGLONG);
      }
      catch (_com_error const & e)
      {
         std::wcout << (wchar_t*)e.ErrorMessage() << std::endl;
      }      
   }
   
   return 0;
}

请注意,在这种情况下,调用 ptr.CreateInstance(L"{A7A5C4C9-F4DA-4CD3-8D01-F7F42512ED04}")ptr.CreateInstance(L"ManagedLib.Test") 是等效的。

上面程序的输出如下:

bool:   True
sbyte:  127
short:  32767
int:    2147483647
long:   9223372036854775807

映射 .NET 和 C++ 类型

下表显示了 C# 类型、其等效的 .NET Framework 类型以及到 COM 和 C++ 类型的映射。

C# .NET Framework 大小(比特) COM/C++ 大小(比特) VARENUM
bool System.Boolean 8 VARIANT_BOOL 16 VT_BOOL
char System.Char 8 unsigned short 16 VT_UI2
sbyte System.SByte 8 char 8 VT_UI1
byte System.Byte 8 unsigned char 8 VT_UI1
short System.I16 16 short 16 VT_I2
ushort System.UInt16 16 unsigned short 16 VT_UI2
int System.Int32 32 long 32 VT_I4
uint System.UInt32 32 unsigned long 32 VT_UI4
long System.Int64 64 __int64 64 VT_I8
ulong System.UInt64 64 unsigned __int64 64 VT_UI8
float System.Single 32 float 32 VT_R4
double System.Double 64 double 64 VT_R8
decimal System.Decimal 128 DECIMAL 128 VT_DECIMAL
字符串 System.String   _bstr_t (BSTR)   VT_BSTR
object System.Object   _variant_t (VARIANT)   VT_VARIANT
  System.DateTime   DATE   VT_DATE
  System.Array   SAFEARRAY   VT_ARRAY

整数和浮点类型之间的封送非常直接,不需要额外的说明。但是,还有其他几种内置类型需要进一步讨论。

  • C# 中的 boolSystem.Bool)类型不映射到 C++ 的 bool 类型,而是映射到 Microsoft 自动化特定的类型 VARIANT_BOOL。这实际上是 short 的一个 typedef,因此有 16 位(与 .NET 布尔类型在 8 位上表示不同)。对于 VARIANT_BOOL 变量的可能值,有两个 typedefVARIANT_TRUE0xFFFF)和 VARIANT_FALSE0)。此类型在 wtypes.h 头文件中可用。
  • C# 中的 charSystem.Char)类型不映射到 C++ 的 char 类型,而是映射到 unsigned short。原因是 .NET 中的字符代表 16 位 UNICODE 字符,而在 C++ 中 char 代表 8 位 ANSI 字符。
  • C# 的 decimalSystem.Decimal)类型没有内置的 C++ 类型等价物。此类型封送为 Microsoft 特有的 DECIMAL 类型,在 wtypes.h 中定义。
    typedef struct tagDEC {
      USHORT wReserved;
      union {
        struct {
          BYTE scale;
          BYTE sign;
        };
        USHORT signscale;
      };
      ULONG  Hi32;
      union {
        struct {
          ULONG Lo32;
          ULONG Mid32;
        };
        ULONGLONG Lo64;
      };
    } DECIMAL;

    这是一个复合类型,它存储一个 96 位无符号整数值和一个表示 10 的幂的精度。这实际上是小数点右边的数字数量,其值可以为 0 到 28。例如,十进制数 42.12345 存储为整数 4212345,精度为 5。

    DECIMAL dm{0};
    dm.scale = 5;
    dm.Lo32 = 4212345;
  • C# 中的 stringSystem.String)类型封送为 _bstr_t(在 comutil.h 中可用),这是一个 COM 实用程序类,它是 BSTR 的包装器,负责 BSTR 的分配和释放以及其他功能。 BSTR 类型(也来自 wtypes.h 头文件)表示指向宽字符字符串的指针。但是,BSTR 类型实际上是一个复合类型,它由一个前缀长度、数据字符串和两个终止 null 字符组成。数据字符串由 16 位 UNICODE 字符表示,并且可能包含多个嵌入的 null 字符。数据字符串的长度(不包括终止 null 字符)由一个 32 位整数表示,该整数出现在内存中数据字符串的第一个字符之前。 BSTR 是一个指向数据字符串第一个字符的指针,而不是长度。 BSTR 使用 SysAllocString() 分配,并使用 SysFreeString() 销毁。
    BSTR str = SysAllocString(L"sample");
    // use str
    SysFreeString(str);
    
    _bstr_t str(L"sample");
    // use str
  • System.DateTime 类型封送为 Microsoft 特有的 DATE 类型(来自 wtypes.h),它是一个 double 的 typedef。日期信息由整数增量表示,以 **1899 年 12 月 30 日午夜** 作为时间零点。时间信息由自前一个午夜以来的日分数表示。例如,1900 年 1 月 7 日下午 3:00 将表示为值 8.625。整数部分 8 代表自基准日期以来的天数,分数部分 .625 是自午夜以来的 24 小时中的一部分(15 小时 / 24 小时 = 0.625)。
  • C# 中的 objectSystem.Object)类型封送为 Microsoft 特有的 _variant_t 类型。这是一个 VARIANT 数据类型的包装器类,在 comutil.h 头文件中可用。 VARIANT 是一个可以保存多种数据类型的联合的容器(因此得名),而 _variant_t 是一个包装器类,它管理 VARIANT 变量的初始化、清理、资源分配和释放。
  • 数组数据类型封送为 Microsoft 特有的 SAFEARRAY 类型。这基本上是一个描述多维数组的结构,并包含指向实际数据存储位置的指针。稍后将进一步讨论这一点。

下面是从附加源代码中摘录的一些片段,您可以在其中找到更多完整示例。

  • C# ITest 接口成员
    [DispId(1)] void TestBool(bool b);
    [DispId(2)] void TestChar(char c);
    [DispId(3)] void TestString(string s);
    [DispId(4)] void TestSignedInteger(sbyte b, short s, int i, long l);
    [DispId(5)] void TestUnsignedInteger(byte b, ushort s, uint i, ulong l);
    [DispId(6)] void TestReal(float f, double d);
    [DispId(7)] void TestDate(DateTime dt); 
    [DispId(8)] void TestDecimal(decimal d);
  • C# Test 类成员实现
    public void TestBool(bool b)
    {
       Console.WriteLine($"bool:   {b}");
    }
    
    public void TestChar(char c)
    {
       Console.WriteLine($"char:   {c}");
    }
    
    public void TestDate(DateTime dt)
    {
       Console.WriteLine($"date:   {dt}");
    }
    
    public void TestSignedInteger(sbyte b, short s, int i, long l)
    {
       Console.WriteLine($"sbyte:  {b}");
       Console.WriteLine($"short:  {s}");
       Console.WriteLine($"int:    {i}");
       Console.WriteLine($"long:   {l}");
    }
    
    public void TestUnsignedInteger(byte b, ushort s, uint i, ulong l)
    {
       Console.WriteLine($"byte:   {b}");
       Console.WriteLine($"ushort: {s}");
       Console.WriteLine($"uint:   {i}");
       Console.WriteLine($"ulong:  {l}");
    }
    
    public void TestReal(float f, double d)
    {
       Console.WriteLine($"float:  {f}");
       Console.WriteLine($"double: {d}");
    }
    
    public void TestString(string s)
    {
       Console.WriteLine($"string: {s}");
    }
    
    public void TestDecimal(decimal d)
    {
       Console.WriteLine($"decimal:{d}");
    }
  • C++ 客户端代码
    void TestInputParams(ManagedLib::ITestPtr ptr)
    {
       std::cout << "test input parameters..." << std::endl;
    
       ptr->TestBool(true);
       ptr->TestChar('A');
       ptr->TestString(L"test");
       ptr->TestSignedInteger(CHAR_MAX, SHRT_MAX, INT_MAX, MAXLONGLONG);
       ptr->TestUnsignedInteger(UCHAR_MAX, USHRT_MAX, UINT_MAX, MAXULONGLONG);
       ptr->TestReal(FLT_MAX, DBL_MAX);
       DECIMAL dm{0};
       dm.scale = 5;
       dm.Lo32 = 4212345;
       ptr->TestDecimal(dm);
       COleDateTime dt = COleDateTime::GetCurrentTime();
       ptr->TestDate(dt.m_dt);
    }
  • 程序输出
    test input parameters...
    bool:   True
    char:   A
    string: test
    sbyte:  127
    short:  32767
    int:    2147483647
    long:   9223372036854775807
    byte:   255
    ushort: 65535
    uint:   4294967295
    ulong:  18446744073709551615
    float:  3.402823E+38
    double: 1.79769313486232E+308
    decimal:42.12345
    date:   2017-07-07 09:55:52

在 C# 中,函数参数可以声明为带有 refout 修饰符。 ref 表示值已设置,函数可以读取和写入它。另一方面,out 表示值未设置,函数必须在返回之前设置它。在 COM 接口上使用时,这两者被封送为相同的。

让我们考虑 ITest 接口中的以下方法。

[DispId(52)]
void TestRefParams(ref int a, ref double d);

[DispId(53)]
void TestOutParams(out int a, out double d);

实际实现并不重要。但是,这两个函数会产生相同的 COM 接口方法。.tli 文件中包装器函数的 C++ 实现如下所示:

inline HRESULT ITest::TestRefParams ( long * a, double * d ) {
    return _com_dispatch_method(this, 0x34, DISPATCH_METHOD, VT_EMPTY, NULL, 
        L"\x4003\x4005", a, d);
}

inline HRESULT ITest::TestOutParams ( long * a, double * d ) {
    return _com_dispatch_method(this, 0x35, DISPATCH_METHOD, VT_EMPTY, NULL, 
        L"\x4003\x4005", a, d);
}

refout 参数都变成指针。\x4003 表示 VT_BYREF|VT_I4\x4005 表示 VT_BYREF|VT_DOUBLEref intout int 都封送为 long*,而 ref doubleout double 都封送为 double*

long i;
double d;
ptr->TestRefParams(&i, &d);
ptr->TestOutParams(&i, &d);

处理数组

C# 数组被封送为 SAFEARRAY。如前所述,SAFEARRAY 不是容器类,而是数组的描述符,其中包含数组的维度、每个维度的边界以及指向实际数据的指针。

typedef struct tagSAFEARRAY
{
   USHORT cDims;
   USHORT fFeatures;
   ULONG cbElements;
   ULONG cLocks;
   PVOID pvData;
   SAFEARRAYBOUND rgsabound[ 1 ];
} SAFEARRAY;

输入和返回的数组被封送为 SAFEARRAY*,而 refout 数组被封送为 SAFEARRAY**

将数组传递给 COM 函数时,您必须:

  • 使用 SAFEARRAYBOUND 定义数组的维度,包括元素的数量和基索引(每个维度)。
  • 使用 SafeArrayCreate() 创建数组,指定元素的类型和维度。
  • 使用 SafeArrayPutElement() 将元素放入数组。
  • 在不再需要数组后,使用 SafeArrayDestroy() 销毁它。

从 COM 函数获取数组(作为返回值或输出参数)时,您必须:

  • 使用 SAFEARRAYBOUND 定义数组的维度,而无需指定元素的数量和基索引。
  • 使用 SafeArrayCreate() 创建数组,指定元素的类型和维度。
  • 接收数组后,您可以检查元素的类型以确保它符合您的预期。
  • 使用 SafeArrayGetLBound()SafeArrayGetUBound() 获取每个维度的元素边界。
  • 遍历元素,使用 SafeArrayGetElement() 检索它们。
  • 在不再需要数组后,使用 SafeArrayDestroy() 销毁它。

为了展示其工作原理,让我们考虑以下处理数组的 ITest 接口方法。

[DispId(27)] void TestIntArray(int[] i);
[DispId(36)] int[] TestIntArrayReturn();
[DispId(45)] void TestIntOutArray(out int[] o);

Test 类中的实现如下所示:

public void TestIntArray(int[] i)
{
   Console.Write("int arr:    ");
   foreach (var e in i) Console.Write($"{e} ");
   Console.WriteLine();
}

public int[] TestIntArrayReturn()
{
   return new int[] { 1, 2, 3 };
}

public void TestIntOutArray(out int[] o)
{
   o = new int[] { 1, 2, 3 };
}

C++ ITest 类中的包装器函数的声明如下:

HRESULT TestIntArray (SAFEARRAY * i);
SAFEARRAY * TestIntArrayReturn ( );
HRESULT TestIntOutArray (SAFEARRAY ** o);

对于这些函数,可以使用以下方式处理输入和输出数组,使用 SAFEARRAY

  • 将数组作为输入参数传递
    SAFEARRAYBOUND sab;                              // define an array bound 
                                                     // for one dimension with
    sab.cElements = 3;                               // 3 elements
    sab.lLbound = 0;                                 // starting from index 0
    
    SAFEARRAY* sa = SafeArrayCreate(VT_I4, 1, &sab); // create a one dimensional array 
                                                     // of 32-bit signed integers
    
    for(LONG i = 0; i < 3; ++i)
    {
      int value = i + 1;
      SafeArrayPutElement(sa, &i, &value);           // put elements in the array {1, 2, 3}
    }
    
    ptr->TestIntArray(sa);                           // use the array
    
    SafeArrayDestroy(sa);                            // destroy the array
  • 将数组作为返回值接收
    SAFEARRAY* sa = ptr->TestIntArrayReturn();       // get the array
    VARTYPE vt;
    SafeArrayGetVartype(sa, &vt);                    // check the type of its elements
    
    if (vt == VT_I4)                                 // make sure it's the expected type
    {
       LONG begin{ 0 };
       LONG end{ 0 };
       SafeArrayGetLBound(sa, 1, &begin);            // fetch the lower bound of 
                                                     // the array index
       SafeArrayGetUBound(sa, 1, &end);              // fetch the upper bound of 
                                                     // the array index
       for (LONG i = begin; i <= end; ++i)
       {
          int value;
          SafeArrayGetElement(sa, &i, &v);           // fetch the elements from the array
          
          // ...                                     // use value
       }
    }
    
    SafeArrayDestroy(sa);                            // destroy the array
  • 将数组作为输出参数传递
    SAFEARRAYBOUND sab{ 0, 0 };                      // define unspecified array bounds
    SAFEARRAY* sa = SafeArrayCreate(VT_I4, 1, &sab); // create an one dimensional array 
                                                     // of 32-bit signed integers with 
                                                     // defined bounds
    
    ptr->TestIntOutArray(&sa);                       // retrieve the array as an 
                                                     // output parameter
    
    LONG begin{ 0 };
    LONG end{ 0 };
    SafeArrayGetLBound(sa, 1, &begin);               // fetch the lower bound of the 
                                                     // array index
    SafeArrayGetUBound(sa, 1, &end);                 // fetch the upper bound of the 
                                                     // array index
    for (LONG i = begin; i <= end; ++i)
    {
       int value;
       SafeArrayGetElement(sa, &i, &value);          // fetch the elements from the array
       
       // ...                                        // use value
    }
    
    SafeArrayDestroy(sa);                            // destroy the array

后两个示例非常相似。但是,如果数组是输出参数(从 C# 中的 refout 函数参数封送),则必须首先在调用方创建数组。但是,您只需要指定元素的类型和维数,而无需指定维度边界(元素的数量和基索引)。当数组从托管端封送回原生端时,这些将填充到 SAFEARRAY 中。

到目前为止展示的所有示例都使用了单维数组。但是,SAFEARRAY 可以用于多维数组。唯一不同的是,您必须在创建数组时为每个维度指定边界,或者在接收数组时读取边界。

再次,为了举例说明,让我们考虑以下处理 32 位有符号整数二维数组的 ITest 函数。

[DispId(42)] void TestInt2DArray(int [,] arr);
[DispId(43)] int[,] TestInt2DArrayReturn();

它们在 Test 类中的实现如下所示:

public void TestInt2DArray(int[,] arr)
{
   for(int i=0; i < arr.GetLength(0); ++i)
   {
      for(int j = 0; j < arr.GetLength(1); ++j)
      {
         Console.Write($"{arr[i,j]} ");
      }
      Console.WriteLine();
   }
}

public int[,] TestInt2DArrayReturn()
{
   return new int[3, 2] { { 1,2}, {3,4}, { 5,6} };
}

C++ ITest 类中这些函数的包装器函数的签名与之前处理一维数组的函数没有区别。

HRESULT TestInt2DArray (SAFEARRAY * arr);
SAFEARRAY * TestInt2DArrayReturn ( );

从 C++ 消费这些函数以及如何处理 SAFEARRAYs 的方法如下所示:

  • 二维输入数组
    SAFEARRAYBOUND sab[2];
    sab[0].cElements = 3;
    sab[0].lLbound = 0;
    
    sab[1].cElements = 2;
    sab[1].lLbound = 0;
    
    SAFEARRAY* sa = SafeArrayCreate(VT_I4, 2, sab);
    for (int i = 0; i < 3; i++)
    {
      for (int j = 0; j < 2; j++)
      {
         LONG index[2] = { i,j };
         int value = 1 + i * 2 + j;
         SafeArrayPutElement(sa, index, &value);
      }
    }
    
    ptr->TestInt2DArray(sa);
    
    SafeArrayDestroy(sa);
  • 二维返回数组
    SAFEARRAY* sa = ptr->TestInt2DArrayReturn();
    VARTYPE vt;
    SafeArrayGetVartype(sa, &vt);
    
    if (vt == VT_I4)
    {
       LONG begin[2]{ 0 };
       LONG end[2]{ 0 };
    
       SafeArrayGetLBound(sa, 1, &begin[0]);
       SafeArrayGetLBound(sa, 2, &begin[1]);
       SafeArrayGetUBound(sa, 1, &end[0]);
       SafeArrayGetUBound(sa, 2, &end[1]);
    
       for (LONG i = begin[0]; i <= end[0]; ++i)
       {
          for (LONG j = begin[1]; j <= end[1]; ++j)
          {
             LONG index[2]{ i,j };
             int value;
             SafeArrayGetElement(sa, index, &value);
          }
       }
    }
    
    SafeArrayDestroy(sa);

处理对象

在 .NET 中,System.Object(C# 中的 object)是所有内置和用户定义类型的基类。每个引用类型或值类型都隐式派生自此类型。object 类型可用于传递任何对象,无论是引用类型还是值类型。当在 COM 可见接口中使用时,该类型会封送为 VARIANT,它是一个结构,包含一个联合,可以存储多种内置类型的值,如整数、浮点数、decimal、日期、字符串、数组等。当导入类型库时生成的代码使用一个包装器类而不是 VARIANT,称为 _variant_t。它处理 VARIANT 变量的初始化和清理,并提供其他有用的功能。

struct tagVARIANT
    {
    union 
        {
        struct __tagVARIANT
            {
            VARTYPE vt;
            WORD wReserved1;
            WORD wReserved2;
            WORD wReserved3;
            union 
                {
                LONGLONG llVal;
                LONG lVal;
                BYTE bVal;
                SHORT iVal;
                FLOAT fltVal;
                DOUBLE dblVal;
                VARIANT_BOOL boolVal;
                _VARIANT_BOOL bool;
                SCODE scode;
                CY cyVal;
                DATE date;
                BSTR bstrVal;
                IUnknown *punkVal;
                IDispatch *pdispVal;
                SAFEARRAY *parray;
                BYTE *pbVal;
                SHORT *piVal;
                LONG *plVal;
                LONGLONG *pllVal;
                FLOAT *pfltVal;
                DOUBLE *pdblVal;
                VARIANT_BOOL *pboolVal;
                _VARIANT_BOOL *pbool;
                SCODE *pscode;
                CY *pcyVal;
                DATE *pdate;
                BSTR *pbstrVal;
                IUnknown **ppunkVal;
                IDispatch **ppdispVal;
                SAFEARRAY **pparray;
                VARIANT *pvarVal;
                PVOID byref;
                CHAR cVal;
                USHORT uiVal;
                ULONG ulVal;
                ULONGLONG ullVal;
                INT intVal;
                UINT uintVal;
                DECIMAL *pdecVal;
                CHAR *pcVal;
                USHORT *puiVal;
                ULONG *pulVal;
                ULONGLONG *pullVal;
                INT *pintVal;
                UINT *puintVal;
                struct __tagBRECORD
                    {
                    PVOID pvRecord;
                    IRecordInfo *pRecInfo;
                    } 	__VARIANT_NAME_4;
                } 	__VARIANT_NAME_3;
            } 	__VARIANT_NAME_2;
        DECIMAL decVal;
        } 	__VARIANT_NAME_1;
    } ;
typedef VARIANT *LPVARIANT;

为了展示其工作原理,我们将考虑一个接受 object 参数的函数和一个返回 object 的函数(实际实现将字符串作为 object 返回)。

[DispId(50)] void TestObject(object o);
[DispId(51)] object TestObjectReturn();

public void TestObject(object o)
{
   Console.WriteLine($"object: {o}");
}

public object TestObjectReturn()
{
   return "demo";
}

包装器 ITest 类中的 C++ 方法如下所示:

HRESULT TestObject (const _variant_t & o);
_variant_t TestObjectReturn ();

下面的示例中的客户端代码将一个 string 传递给 TestObject() 函数,并处理从 TestObjectReturn() 函数返回的 string

_variant_t vi(L"demo");
ptr->TestObject(vi);

_variant_t vr = ptr->TestObjectReturn();
if (vr.vt == VT_BSTR)
{
   std::wcout << "object: " << (wchar_t*)vr.bstrVal << std::endl;
}

处理 COM 可见接口

COM 可见接口也可以通过 COM 在托管和原生之间或反之进行双向封送。COM 可见接口可以是以下四种可能的类型之一:IUnknownIDispatch、dual 或 IInspectable(这些是 暴露为 Windows Runtime 接口的 COM 接口)。 IUnknownIInspectable 接口被封送为 IUnknown*VARENUM 类型 VT_UNKNOWN),而 IDispatch 和 dual 接口被封送为 IDispatch*VARENUM 类型为 VT_DISPATCH)。

在下面的示例中,IBarIDispatch 类型的 COM 可见接口。它有几个属性(一个整数 ID 和一个字符串名称)和一个返回字节数组的方法。

[Guid("7FA115C0-C1D3-49B8-B0B7-B7155CE307C5")]
[InterfaceType(ComInterfaceType.InterfaceIsIDispatch)]
[ComVisible(true)]
public interface IBar
{
  [DispId(1)]
  int Id { get; set; }

  [DispId(2)]
  string Name { get; set; }

  [DispId(3)]
  byte[] GetData();
}

Bar 是实现 IBar 接口的类。

[Guid("564ADB07-434F-4ED3-A138-B5E41976F099")]
[ClassInterface(ClassInterfaceType.None)]
[ComVisible(true)]
class Bar : IBar
{
  public int Id { get; set; }

  public string Name { get; set; }

  public byte [] GetData()
  {
     return new byte[] { 1, 2, 3 };
  }
}

ITest 接口中,有一个方法返回对 IBar 接口的引用,还有一个方法接受 IBar 引用作为参数。

[DispId(46)] IBar TestInterfaceReturn();
[DispId(47)] void TestInterface(IBar bar);
public IBar TestInterfaceReturn()
{
   return new Bar() { Id = 1, Name = "Test" };
}

public void TestInterface(IBar bar)
{
   Console.Write($"Bar({bar.Id}, {bar.Name})=");
   foreach (var e in bar.GetData()) Console.Write($"{e} ");
   Console.WriteLine();
}

ManagedLib.tlh 头文件中,IBar 包装器类的定义如下:

struct __declspec(uuid("7fa115c0-c1d3-49b8-b0b7-b7155ce307c5"))
IBar : IDispatch
{
    //
    // Property data
    //

    __declspec(property(get=GetId,put=PutId))
    long Id;
    __declspec(property(get=GetName,put=PutName))
    _bstr_t Name;

    //
    // Wrapper methods for error-handling
    //

    // Methods:
    long GetId ( );
    void PutId (
        long _arg1 );
    _bstr_t GetName ( );
    void PutName (
        _bstr_t _arg1 );
    SAFEARRAY * GetData ( );
};

ITest 包装器类中对应的 C++ 方法如下所示:

IBarPtr TestInterfaceReturn ( );
HRESULT TestInterface (struct IBar * bar );

下面展示了一个使用这两个方法的示例:

ManagedLib::IBarPtr bar = ptr->TestInterfaceReturn();
if (bar != nullptr)
{
   std::wcout << "Bar(" << bar->Id << ", " << (wchar_t*)bar->Name << ")=";

   SAFEARRAY* data = bar->GetData();
   LONG begin{ 0 };
   LONG end{ 0 };
   SafeArrayGetLBound(data, 1, &begin);
   SafeArrayGetUBound(data, 1, &end);
   for (LONG i = begin; i <= end; ++i)
   {
      unsigned char v;
      SafeArrayGetElement(data, &i, &v);
      std::cout << (int)v << ' ';
   }
   std::cout << std::endl;
}

bar->Name = "Test2";
ptr->TestInterface(bar);

封送接口引用的其他示例可在附带的源代码中找到。

另请参阅

历史

  • 2017 年 7 月 11 日:初始版本
© . All rights reserved.