如何正确地在C++(C++/CLI)中使用COM组件

今天遇到个十分纠结的问题:某COM组件调用时,会把字符串看做GB2312(中文系统默认编码),并试图来一发格式转换转成UTF8。然而对于C#(默认字符串格式都是Unicode,参见CLR via C#)来说,如何才能向这个COM组件提供一个GB2312格式的字符串呢?

首先容易想到的是,我们先取得GB2312编码的字节数组,然后变成C#字符串(此时显示为乱码)提供给COM。然而事实上……

最后的结果是……字符串中如果有奇数个汉字,那么最后一个很可能会变成乱码……

这个问题网上讨论的人也很多了,原因就是UTF8中的汉字基本上都是3个字节,而GB2312中汉字为2个字节。于是:

 UTF8

字节

镇1 镇2 镇3 命1 命2 命3 歌1 歌2 歌3

GB2312

字节 

乱码字

1-1

乱码字

1-2

乱码字

2-1

乱码字

2-2

乱码字

3-1

乱码字

3-2

乱码字

4-1

乱码字

4-2

字节不足,该字节被丢弃,

末尾补了问号(?)


于是最后一个字节被丢了,还多了个问号。(然而对于偶数个汉字,基本上就可以正常显示。对于奇数个汉字也并不一定全都会不能显示,比如“学习一”是一个能够正常显示的例子。)

CHU巨教导说这种情况无解。

于是接下来我就在研究如何用C++(实际上是我更有自信的C++/CLI)来调用这个COM。

首先,我们手头有的只有一个DLL文件。在.NET中,自然可以直接引入,并且自动生成Interop库,调用起来跟调用.NET类库几乎无异。而在C++中我们当然要手动引���。怎么引入呢?

网上大部分是说这种方式:

#import "COM.dll" no_namespace

不过我试了下,提示找不到.tlh文件。我们手头只有dll,并没有tlh、tlb这些,看来此路不通。

然后我发现Windows Kits中有OLE/COM Object Viewer(OLEView.exe),用它可以查看COM DLL的信息。

在主界面选择File——View TypeLib,选择对应的DLL:


在这个界面,有一个保存选项(先选中左侧树顶部的DLL)。可以保存成三种格式:.IDL、.h、.c。显然.h和.c正是我们需要的。然而尝试保存成.h和.c都不成功,cmd框一闪而过,大概可以看出是有什么路径上的问题。保存成IDL文件倒是成功了。

那么接着想办法把IDL用起来。一搜,可以用MIDL(同样位于Windows Kits中)来编译IDL。那么我们来试着编译一下。

找不到cl.exe。这个文件我都知道在哪,编译C的编译器嘛,VS中就有。然而并不知道怎么告诉midl,可能是环境变量哪里有问题。

再找找别的方法,后来发现把IDL文件直接引入到当前的VC++工程里,右键单击它,有一个“编译”。点它,成功编译出了COM_i.c和COM_h.h!(应该还是用的midl)

编译成功后,把这两个文件加入工程,然后要注意把IDL文件移出工程,否则在VC++工程编译的时候,IDL还会再编译一遍,会将你对.c和.h的修改全部覆盖掉。

引入我这里的COM之后,在.h里报了一个错误,大意是xxx struct应该先定义。仔细一看,这个struct的定义在.h的末尾,而前面已经用到了。这里可不是C#,写在前后都能用。把struct的定义手动移到开头部分即可(这midl好不智能啊)。

接下来写COM调用的代码:(参考此文

首先我们得引入几个头文件:

#include "atlcomcli.h" //定义了CoInitialize等调用COM需要使用的方法
#include "COM_h.h" //编译IDL生成的头文件
#include <comdef.h> //定义了BSTR的相关方法,COM中的字符串都是BSTR
        std::string str = "Oh, it's you!";
_bstr_t bstr_t(str.c_str()); 
BSTR bstr = bstr_t.GetBSTR(); //得到BSTR   

方法1:

 

	CoInitialize(NULL);
CLSID clsid;
CLSIDFromProgID(OLESTR("GLaDOS.ICore"), &clsid);
{
CComPtr<ICore> pGetRes;//智能指针
pGetRes.CoCreateInstance(clsid);
pGetRes->SayHello(bstr);
}
        CoUninitialize();

 

 

方法2:

	CoInitialize(NULL);
CLSID clsid;
HRESULT hr = CLSIDFromProgID(OLESTR("GLaDOS.ICore"), &clsid);
ICore *ptr;
hr = CoCreateInstance(clsid, NULL, CLSCTX_INPROC_SERVER,
__uuidof(ICore), (LPVOID*)&ptr);
long r = ptr->SayHello(bstr);
CoUninitialize();
 

两种方法应该都没有问题。如果出现“内存访问冲突”(System.AccessViolationException)之类的错误,还是要检查一下有没有未实例化的对象或者错误地使用了NULL之类的。

 

然后编译……由于我用的是C++/CLI,接下来连续遇到了三个关于CLI的编译问题:

1. “IServiceProvider”: 不明确的符号

这个错误看的我真是目瞪口呆.jpg。终于见识了什么叫“大水冲了龙王庙”,.NET国mscorlib家族的System.IServiceProvider和VC国windows.h家的IServiceProvider重名冲突了!这完全独立的两家也能冲突,也只有在C++/CLI这个神奇的地方才有可能了。解决方法上面的文章里说的也很明确,把#include<windows.h>放在using namespace System;之前。最好把.NET的命名空间引入放到你自己的命名空间底下,比如这样:

#include <Windows.h>
namespace ApertureScience{
using namespace System;
        public ref class GLaDOS{} }

 

 

2.无法使用 /clr 选项编译 C 文件“file”

这意思是说C++/CLI只支持*.cpp,不能支持*.c。解决方案上面链接也很明确,在编译选项里加入一个开关“/TP”即可。

 

3.ERROR LNK 2022 由于元数据错误,编译失败

看了上面的文章,大意是这个工程用的.NET版本/平台工具集太低了。解决方案上面也有给出,先卸载这个项目,然后右键选择编辑.vcxproj文件,在里面提高.NET版本或者平台工具集。我将.NET版本从3.5改为4.0之后就可以编译了。

 

至此总算成功编译。

 

另:在.NET中导入COM组件时报过一个错误:“无法嵌入互操作类型 请改用适用的接口”。遇到这种情况的解决方案:

选中项目中引入的dll,鼠标右键,选择属性,把“嵌入互操作类型”设置为False。

 

另2:由于提高了.NET版本,又出了一个错误:混合模式程序集是针对“v2.0.50727”版的运行时生成的,在没有配置其他信息的情况下,无法在 4.0 运行时中加载该程序集。

这个错误已经是我的常客了,解决方案是在app.config中加入:

<startup useLegacyV2RuntimeActivationPolicy="true">
  <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.0"/>
</startup>
确保这段位于<configuration>之内。
 
 

添加评论

Loading