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






4.97/5 (23投票s)
创建 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# 中的
bool
(System.Bool
)类型不映射到 C++ 的bool
类型,而是映射到 Microsoft 自动化特定的类型VARIANT_BOOL
。这实际上是short
的一个typedef
,因此有 16 位(与 .NET 布尔类型在 8 位上表示不同)。对于VARIANT_BOOL
变量的可能值,有两个typedef
:VARIANT_TRUE
(0xFFFF
)和VARIANT_FALSE
(0
)。此类型在 wtypes.h 头文件中可用。 - C# 中的
char
(System.Char
)类型不映射到 C++ 的char
类型,而是映射到unsigned short
。原因是 .NET 中的字符代表 16 位 UNICODE 字符,而在 C++ 中char
代表 8 位 ANSI 字符。 - C# 的
decimal
(System.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# 中的
string
(System.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# 中的
object
(System.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# 中,函数参数可以声明为带有 ref
或 out
修饰符。 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);
}
ref
和 out
参数都变成指针。\x4003
表示 VT_BYREF|VT_I4
,\x4005
表示 VT_BYREF|VT_DOUBLE
。 ref int
和 out int
都封送为 long*
,而 ref double
和 out 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*
,而 ref
和 out
数组被封送为 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# 中的 ref
或 out
函数参数封送),则必须首先在调用方创建数组。但是,您只需要指定元素的类型和维数,而无需指定维度边界(元素的数量和基索引)。当数组从托管端封送回原生端时,这些将填充到 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++ 消费这些函数以及如何处理 SAFEARRAY
s 的方法如下所示:
- 二维输入数组
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 可见接口可以是以下四种可能的类型之一:IUnknown
、IDispatch
、dual 或 IInspectable
(这些是 暴露为 Windows Runtime 接口的 COM 接口)。 IUnknown
和 IInspectable
接口被封送为 IUnknown*
(VARENUM
类型 VT_UNKNOWN
),而 IDispatch
和 dual 接口被封送为 IDispatch*
(VARENUM
类型为 VT_DISPATCH
)。
在下面的示例中,IBar
是 IDispatch
类型的 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);
封送接口引用的其他示例可在附带的源代码中找到。
另请参阅
- 公开 .NET 组件给 COM
- 将 .NET Framework 组件公开给 COM
- 构建和部署 .NET COM 程序集
- C++ - 介绍 SAFEARRAY 数据结构
- C++ - 使用 CComSafeArray 简化 C++ 中的 Safe Array 编程
历史
- 2017 年 7 月 11 日:初始版本