C# ATLCOM 互操作代码片段 - 第一部分






4.66/5 (18投票s)
代码片段详细介绍 C# 和 ATLCOM 中的互操作编程
引言
这里没有介绍。我不敢给 Interop 写“介绍”,怕暴露我知识的贫乏。
请用谷歌搜索 Interop,你会找到比我能提供的更好的解释。
背景
在我最近的一些项目中工作时,我需要广泛地使用 C# 和 COM Interop。
我不得不在托管和非托管世界之间传递各种各样的信息,自然地,
我求助于谷歌,以便我能从各种来源重用(也就是复制/粘贴)代码。
令我不安的是,我找到了我想要的代码,但它们并不在同一个地方。它们散布在世界各地,而我
想把所有信息汇集到一个网页上,供任何人参考。
以下是我打算写的关于 C#/ATL COM Interop 系列文章的第一篇。随着我学习到越来越多的信息,
我将修订这些文章。(至少,我希望如此。但上帝作证,我是一个懒惰的家伙!!!)
起初,有一个 bug……
当我开始项目工作时,我的经理知道我在简历中关于
“丰富的 .Net 经验”是假的。所以,他很友好地给了我一个小任务,这个任务涉及到
调试一个现有的 Interop 代码,而不是编写新的代码。
我遇到的第一个问题是*如何调试代码???* COM 代码写在
一个项目中,而 C# 代码写在另一个项目中。它们有不同的 .sln
文件,当我启动
其中一个进行调试时,在另一个解决方案中设置的断点根本不会触发!!!
DLL 加载正常,代码流程也按编写的方式进行。但如果断点不触发,我该如何
调试问题呢!!!
我尝试的一个解决方案是,在需要调试 C# 端代码时,在调试器中运行 C# 解决方案文件,
而在调试非托管代码时,运行 VC++ 解决方案文件。这只帮了我一小段时间。很快就需要将它们一起调试,问题
又出现了。
The solution is to set the Debugger Type in Visual Studio. In the project properties >
解决方案是在 Visual Studio 中设置调试器类型。在项目属性 >
调试中,有一个小选项叫做“启用非托管代码调试”。勾选此框
并重新运行解决方案。瞧!!!你现在可以从 C# 解决方案调试 COM 代码了。请看下面的
图片进行说明。
如果你从非托管的 VC++ 解决方案进行调试,那么你需要设置一个不同的选项。
请看下图找出它。
这些选项有缺点。在 C# 中启用“非托管调试”会让你失去
在 C# 中进行“编辑并继续”的功能。幸运的是,每当你尝试这样做时,你都会收到一条消息。
请查看此链接。
在 VC++ 中情况略有不同。在这里,默认选项是“自动”,它告诉
调试器去调试 EXE 文件构建时所在的环境。如果 EXE 文件是在
非托管环境中构建的,那么你可以单步调试 COM 代码。如果它是在托管
环境中构建的,那么你可以单步调试 C# 代码,但不能调试 COM 代码(即使你
正是从那个项目启动的调试器)。将其设置为“混合”可以帮助你在
这些世界之间穿梭,但你只能编辑和继续 COM 代码。
完成这些后,我能够在托管和非托管世界中单步调试,并修复了 bug。我的
经理看到了我的工作,很高兴。
第二天,我的经理说……
“你已经准备好接受下一个任务了,”他说。“你将学习更多激动人心的事情,”他说。他没说的是
他正用更多的工作来奖励我的工作。唉……生活。我振作起来。这一次,我应该在 COM 组件中编写一个函数,
并从 C# 调用它。乐趣开始了……
在两个世界之间发送数据的过程称为封送(Marshalling)和解封送(Unmarshalling)。我想封送一个
一维实数数组。
封送一维实数数组(不好的方式)
我的 IDL 文件声明如下所示。
[id(1), helpstring("method NotGoodInterop")]
HRESULT NotGoodInterop([in]long nAraySize, [in]float *RealNumbersList);
头文件中的原型是
STDMETHOD(NotGoodInterop)(long nAraySize, float *RealNumbersList);
现在在 C# 中调用这个函数非常简单,就像
float[] Numbers = new float[_NumbersCount];
_SomeClassObject = new ATLSimpleObjectDemoLib.SomeClassClass();
// I want to marshal the array starting from position 3.
_SomeClassObject.NotGoodInterop(2, ref Numbers[3]);
// I want to marshal the array starting from position 25.
_SomeClassObject.NotGoodInterop(3, ref Numbers[25]);
就这么简单。我们调用函数,然后就完成了。数据像企鹅
穿越海洋一样跨越了世界。
将这种方式称为 NotGoodInterop
纯粹是个人原因。我不喜欢这种指定
数组大小和数组起始位置的方式。这可能很有用,但对我来说很困惑。所以,在写这个
例子时我这样命名了,但它可能并不坏。我留给你来决定。我个人更喜欢
下面描述的方式。
封送一维实数数组(好的方式)
IDL 声明如下
[id(2), helpstring("method PutRealNumbers")]
HRESULT PutRealNumbers([in] long nAraySize, [in,size_is(nAraySize)]float RealNumbersList[]);
[id(3), helpstring("method GetRealNumbers")]
HRESULT GetRealNumbers([in] long nAraySize, [ref,size_is(nAraySize)]float RealNumbersList[]);
请注意在 IDL 文件中使用了 size_is
属性。请在此处和此处阅读更多相关信息。
第二个链接来自 Adam Nathan,我感谢他出色的文章。头文件中的原型
如下
STDMETHOD(PutRealNumbers)(long nArraySize,float RealNumbersList[]);
STDMETHOD(GetRealNumbers)(long nArraySize,float RealNumbersList[]);
从 C# 调用这个函数和 NotGoodInterop 的情况一样。在这里重复一下
_SomeClassObject.PutRealNumbers(_NumbersCount - 3, ref Numbers[2]);
_SomeClassObject.GetRealNumbers(2, ref Numbers[4]);
我的经理看到了我的工作,很高兴。他用更多的工作奖励了我。
封送多维实数数组
现在我必须封送一个二维实数数组。这变得棘手了。封送
多维数组与封送一维数组不同。这是因为,
当我们发送数组指针时,非托管世界对
数组的维度一无所知。在 C++ 中,多维数组实际上存储在单个内存
序列中。编译器做了一些小魔法,当我们使用 arr[][] 表示法时,它会传递
适当的内存位置。为了正确地做到这一点,编译器强制开发者
明确指定数组的列大小。也就是说,在 C++ 中你不能声明类似
float fltArr[][] = new float[10][20]; // This is not possible
的东西。甚至不要考虑将数据作为 float ** 发送,并在
两个显式变量中指定数组大小。如果我们在同一个函数中有超过3个数组要封送,这
会变得非常非常麻烦。如果这些数组的维度都不同,那简直就是一场
折磨。
Kim Kartavyam???(这是梵语,意为‘解决方案是什么’?)使用 SAFEARRAYs
SafeArray 是一种非常优雅的方式,可以在各种
编程语言编写的函数之间封送数据。它与 CLR 结合得很好,在语言
边界上稍费点力,我们就可以实现一种很酷的数据封送方式。请在此处阅读更多关于 SafeArray 的信息。尽管在接下来的讨论中我不会假设你熟悉 SafeArray,但我也
不会详尽地讨论它。
使用 SafeArrays,我的 IDL 声明变得简单了
请特别注意这里的两个事实
[id(4), helpstring("method PutMultiDimensionalArray")]
HRESULT PutMultiDimensionalArray([in] SAFEARRAY(float) saNumbers);
[id(5), helpstring("method GetMultiDimensionalArray")]
HRESULT GetMultiDimensionalArray([out] SAFEARRAY(float) *saNumbers);
SAFEARRAY 需要指定一个类型。在这种情况下是 float。
- Put 的 IDL 声明中 SAFEARRAY 没有‘*’,但 Get 有一个。
- 注意到这一点后,请观察头文件中所需的原型
这里没有为 SAFEARRAY 指定数据类型。此外,Put 有一个‘*’,而 Get 有‘**’。
STDMETHOD(PutMultiDimensionalArray)(SAFEARRAY* saNumbers);
STDMETHOD(GetMultiDimensionalArray)(SAFEARRAY **saNumbers);
比 IDL 文件多一个。从 C# 调用它看起来很简单。
有趣的是 C++ 方法。我们必须深入这个 SAFEARRAY 变量来获取
float[,] TwoDimNumbers = new float[2, 3]; // This is a two dimensional array
_SomeClassObject.PutMultiDimensionalArray(TwoDimNumbers);
Array TwoDimNumbers1; // Note the difference when I am Get() data
_SomeClassObject.GetMultiDimensionalArray(out TwoDimNumbers1);
发送给我们的数据。正如我所说,这可能有点麻烦。
访问 SAFEARRAY 中的数据可以通过三种方式完成。
从 SAFEARRAY 创建一个 C++ 数组。
- 直接访问 SAFEARRAY 内容。
- 使用 CComSafeArray<> 模板
- 第一种方式,我们获得了速度。在后一种方式中,我们牺牲了一些性能,但可以确保
我们没有访问非法数据。第三种是两者的最佳结合。
两种方法都有一些共同的基础工作要做。我将首先介绍这个共同领域,
然后再转向各个方法。
我将首先验证数组的维度。从 C#,我发送了一个二维数组。下面的
代码在 C++ 方法中。nDimensions
必须等于 2。
nDimensions = SafeArrayGetDim(saNumbers);
从 C# 我发送了一个浮点数组。vt
必须等于 VT_R4
SafeArrayGetVartype(saNumbers,&vt);
现在获取数组边界
LowerBounds = new LONG[nDimensions];
UpperBounds = new LONG[nDimensions];
for(int inx=1;inx<=nDimensions;inx++) // <------- Note: the loop begins with 1 here.
{
_com_util::CheckError(SafeArrayGetLBound(saNumbers, inx, &LowerBounds[inx-1]));
_com_util::CheckError(SafeArrayGetUBound(saNumbers, inx, &UpperBounds[inx-1]));
}
访问安全数组数据的方法 1:使用 SafeArrayAccessData()
m_Dimension1Length = UpperBounds[0]-LowerBounds[0]+1;
m_Dimension2Length = UpperBounds[1]-LowerBounds[1]+1;
我创建一个 C++ 数组,并将 SafeArray 中的数据复制到这个数组中。这为我节省了
性能,尤其是在我需要重复访问数组内容时。
请阅读此
注意:没有必要将 Safe Array 的副本创建到 C++ 数组中。
float *pfNumbers = NULL;
_com_util::CheckError(SafeArrayAccessData(saNumbers,(void HUGEP* FAR*)&pfNumbers));
float **CppArr = NULL;
CppArr = (float **)malloc(sizeof(float*)*m_Dimension1Length);
for(int inx=0; inx<m_Dimension1Length; inx++)
{
CppArr[inx] = new float[m_Dimension2Length];
for(int jnx=0; jnx<m_Dimension2Length; jnx++)
{
long SafeArrayIndex = jnx*m_Dimension1Length + inx;
long CppArrayIndex = inx*m_Dimension2Length + jnx;
// In SafeArray, the rank is reversed when storing. So, when we
// construct our Cpp array, we have to calculate the appropriate
// array index. That is what the above two lines do. This can be
// avoided in method 2. But it carries a performance overhead.
float f;
f = pfNumbers[SafeArrayIndex];
CppArr[inx][jnx] = f;
m_vecFloatingNumbers.push_back(f);
}
}
_com_util::CheckError(SafeArrayUnaccessData(saNumbers));
这样做只是为了演示目的。如果你想直接访问 pfNumbers 进行下游计算,那
是完全可以的。只是,要记住适当地计算数组索引。否则,你最终会
访问错误的数组位置。
访问安全数组数据的方法 2:使用 SafeArrayGetElement()
在这里,我们将通过 safearray 访问数组元素。我们不会接触
原始内存。这种方法安全,并提供了适当的错误处理机制,但是会消耗
调用 SafeArrayGetElement() 时锁定和解锁 SafeArray 的时间。这可能是一个
性能瓶颈。
这是用于 Put() 数据。获取数据是与此相关的一个推论。 请看我
for(int inx=0; inx<m_Dimension1Length; inx++)
{
for(int jnx=0; jnx<m_Dimension2Length; jnx++)
{
// LowerBound "can" be non Zero. Especially if the caller is written
// in a language where arrays dont begin with Zero.
// So, we add the LowerBounds[] to the index. And we are done.
long ArrayIndex[2] = {LowerBounds[0]+inx,LowerBounds[1]+jnx};
// Here we are unconcerned with the internal storage of SafeArray. Simply call it with
// the index number and we get our data.
// In method 1, we have to do the array index calculation ourselves.
float f;
_com_util::CheckError(SafeArrayGetElement(saNumbers,ArrayIndex,(void*)&f));
}
}
附在本篇文章中的示例项目。 它为你提供了完整的带注释的代码。
非常重要的注意:为什么在访问
数据时我们需要关心数组索引计算?因为 SAFEARRAYs 是为了在所有语言之间封送数据而设计的。而一些
语言的数组是行主序(Row-Major),而另一些则是列主序(Column-Major)。
SafeArray 有一种标准的访问方式,即列主序。不幸的是,
SafeArray 存储数组的方式与 C++ 的行主序不同。所以,我们必须
担心数组索引的计算。
访问安全数组数据的方法 3:使用 CComSafeArray<>
我将在封送字符串时更详细地介绍这个。单独处理它的唯一原因是,
我是在封送字符串时学会使用这个类的,并且它成了我处理字符串时的一个习惯。
所以,我的示例代码是这样写的,因此我
在那里解释它。
封送字符串数组
我被要求将从 C# UI 表单收集的用户 ID 数组发送到用 COM 编写的数据库访问
组件。这几乎与封送浮点数数组完全相同。
唯一的区别是 IDL 文件原型包含一个 BSTR 作为 SAFEARRAY 的
数据类型。
IDL 声明将是
头文件中的原型是
[id(6), helpstring("method PutStrings")]
HRESULT PutStrings([in] SAFEARRAY(BSTR) Strings);
[id(7), helpstring("method GetStrings")]
HRESULT GetStrings([out] SAFEARRAY(BSTR) *Strings);
请将此与封送多维数组部分的代码进行比较,你将看到
STDMETHOD(PutStrings)(SAFEARRAY * Strings);
STDMETHOD(GetStrings)(SAFEARRAY **Strings);
相似之处和不同之处。
从 C# 调用也是同样的方式
然后我们就完成了。在 C++ 端,处理过程与封送
string[] Strings = new string[5];
_SomeClassObject.PutStrings(Strings);
多维实数数组相同,只是我们处理的是字符串。我不会
在这里再次讨论它们。你可以自己尝试。我附上的示例项目
包含了这些方法,你可以尽情尝试。
就这么简单!!!这提供了数组式数据访问的简单性和优雅性,
我将在封送字符串时更详细地介绍这个。单独处理它的唯一原因是,
std::vector<BSTR> vecStrings2;
CComSafeArray<BSTR> saBSTRs;
saBSTRs.CopyFrom(Strings);
vecStrings2.clear();
for (long inx=0; inx<cElements; inx++)
{
vecStrings2.push_back(saBSTRs[inx]);
}
并避免了所有关于下界和上界的麻烦。我
不确定性能影响,但就个人而言,在这种情况下我不在乎。
代码的简洁性对我来说意义重大,而且我确信微软已经融入了
所有必要的性能调整。许多与 SAFEARRAY 相关的编码可以通过
使用 CComSafeArray 包装器来避免。如果我们正在使用
C 风格代码中的 SAFEARRAY,那么方法 1 和 2 是必需的。
封送结构和枚举
尽管我把标题定为封送结构和枚举,但我将要
讨论的内容很少。我将首先解释一般情况,然后列出
我没有涵盖的要点以及这样做的原因。
我需要封送一个数据结构和枚举值。这些,我将在我的 IDL 文件中声明,如下
所示
我还声明了一个接受这些作为输入的函数。
typedef enum MyEnum
{
Good,
Bad,
Ugly
} MyEnum;
typedef struct SData
{
int Id;
BSTR Name;
MyEnum eEnumVal;
} Data;
头文件中的原型变成了
// Enum and structure
[id(8), helpstring("method SampleEnumAndStruct")]
HRESULT SampleEnumAndStruct([in] MyEnum enumVal,[in]Data data);
这是任何 IDL 文件中的标准声明方法。现在从 C# 调用它简直易如反掌。实际上,
STDMETHOD(SampleEnumAndStruct)(MyEnum enumVal,Data data);
它感觉完全没有任何不同。
C++ 中的实现部分是
ATLSimpleObjectDemoLib.SData data = new ATLSimpleObjectDemoLib.SData();
data.eEnumVal = ATLSimpleObjectDemoLib.MyEnum.Bad;
data.Id = 0;
data.Name = "Lee Van Cleef";
_SomeClassObject.SampleEnumAndStruct(ATLSimpleObjectDemoLib.MyEnum.Bad, data);
相当简单明了。不是吗???
STDMETHODIMP CSomeClass::SampleEnumAndStruct(MyEnum enumVal,Data data)
{
if(enumVal == Bad)
{
MessageBox(NULL,L"The Baddies was Lee Van Cleef",
L"Did you know that?",MB_OK|MB_ICONQUESTION);
}
return S_OK;
}
待办事项
在封送结构时,我们可以指定如何对单个成员进行封送。
- 我们可以使用像 MarshalAs、MarshalAsAttribute 等属性。我
从未使用过这个,也不知道如何使用。如果有人能在这里添加它,我将
非常感谢。
在一些网站上,提到了编辑从 COM - 组件生成的 tlbimp 文件。我不知道这该如何做。我也想知道这种方法
会有多大帮助,特别是在大型项目中,夜间构建会不断重建 COM
组件并重新生成 tlbimp 文件。但这些可能是一个
无知的头脑简单的人的问题。所以,请不要太当真。当我亲手实践
这些时,我会在这里添加它们。
跨 C 风格函数封送数据
你可能会问,还剩下什么。如果方法 1 和方法 2 是用于 C 风格访问
SAFEARRAY 的,那我们还剩下什么?实际上,很少了。我将在这里快速浏览
它们,因为这里是做这件事的合适地方。
考虑一个示例函数,它传入一个一维整数数组。
为了在 C# 中调用它,我们首先需要告诉编译器在哪里可以找到
extern "C" void SamplePutFunction(int nArraySize,int * Arrays);
运行时的 DLL,以及函数在 C 中是如何声明的。我们通过
在 C# 文件中使用 DllImport 属性来做到这一点。
现在我们可以从任何需要的地方调用这个函数了。请注意,C#
[DllImport("CStyleDLL.dll")]
public static unsafe extern void
SamplePutFunction(int nArraySize, int* InputArray);
编译器不会检查你所写的函数原型的有效性,
即 C# 文件中的原型是否与 DLL 匹配。如果存在不匹配,那么你将得到一个异常或
更糟的情况。如果你从同一个 DLL 导入多个函数,请
记住,你必须为每个函数写一个 DllImport。漏掉
一个函数不会导致编译错误。你只会在
运行时得到一个异常。
封送指针
事情就是这样。请密切注意此代码片段中的 unsafe
和 fixed
[DllImport("CStyleDLL.dll")]
public static unsafe extern void
SamplePutFunction(int nArraySize, int* InputArray);
private void btnPutCStyleArray_Click(object sender, EventArgs e)
{
int[] IntArray = new int[100];
unsafe // <---- In 2008, this requires an explicit compiler option
{
fixed (int* pArray = IntArray) // <--- fixes the pointer during GC.
{
SamplePutFunction(IntArray.Length, pArray);
}
}
}
关键字。它们可能在编码中造成严重破坏,我曾为追逐
那个难以捉摸的 bug 而彻夜不眠。
尽管从上面的代码来看这似乎没有必要,但 unsafe
和
fixed
代码在我们需要将指向结构的指针封送到 C 函数时特别有用。
始终建议使用 fixed,因为它
可以防止指针在垃圾回收期间被重新定位。
如果你不 fixed
你的指针变量,你将面临潜在的
页面错误和访问冲突的风险——这是黑客的完美入口点。
在 VS 2008 中,在代码中使用 unsafe 关键字需要一个编译器选项
在 UI 中显式设置。请看下图
未提及的代码片段
我在这里没有提到的是——对于所有要从 C# 调用的 COM 组件,
我们需要一个 AxInterop 和 Interop DLL。我相信这些 DLL 负责
执行必要的操作,将 C# 数组转换为 SAFEARRAYS,然后
将它们传递给 COM 函数。这些 DLL 在我们将 COM 组件添加到项目引用时
会自动生成。请看下面的屏幕截图,了解
如何将 COM 组件添加为引用。
从 C# 项目的“引用”选项中选择“添加引用”。
选择 COM 标签。选择所需的 COM 组件并单击“确定”。
这些步骤会创建 AxInterop 和 Interop DLL,并将它们放在适当的
文件夹中。但通常(在大型软件项目中)需要将这些 DLL 放置
在默认位置以外的其他位置,并进行适当的签名。
为实现这一点,请使用 TlbImp.exe 和 axImp.exe 实用程序。请参阅
此处这些实用程序的示例用法。在此处和此处了解更多信息。
对于我们一直在讨论的示例,我们不需要 aximp。如果我们正在创建一个
TlbImp /silent /nologo /sysarray /publickey:"PublicKey.snk"
/delaysign /out:"Interop.ComComponent.dll"
/namespace:ComInteropDemo "InteropDemo.dll"
"/asmversion:1.0.0.0"
aximp /silent " InteropDemo.dll"
/out:"AxInterop.InteropDemo.dll"
/rcw:"InteropDemo.dll"
/publickey:"PublicKey.snk"
/delaysign /silent /nologo
ActiveX 组件,那么 aximp 是必需的。我们上面看到的片段不是 ActiveX 组件。
所以,我们不需要它。我将尝试在本系列的第 2 部分中处理它。
我的经理非常高兴。他做了什么呢?请阅读下一篇文章,了解他
结语
做了什么 :-)
My manager was very happy. And what did he do? Please read next article to find out what he did :-)