COM与ATL大扫盲 (第二回书)
第二回书, Salute接口的故事
上回书说到,我们已经开始了一个ATL项目并简要向你介绍了一下生成的DLL输出函数
及全局变量都是怎么回事儿。这一次,我们将继续上回的介召并试着建立一个简单的方法,
但更的的应该是理论性的东西。
在这一讲中,我们会接触到BSTR,VARIANT,我们将看到如何添加ATL对象,如何向一个
接口中添加方法,以及添加方法的过程中需要注意的问题,IDL及如何用VB使用我们建立好
的组件。
那就打开上次我们未完成的项目“ATLHello”吧!OK!现在面对着你的是一个比用MFC向导
生成的还“干净”的框架。比你想像的还干净!嗯,看我的脸白吧?比我的脸还干净!因为
它没有提供让外部应用程序创建的对象。也许你会问:“你怎么知道的?”哈!带着手电筒
跟我来!...快!双击左边的_Moudle然后看右边(这句话你不会不懂吧?)注意如下的两
行:
BEGIN_OBJECT_MAP(ObjectMap)
END_OBJECT_MAP()
对于ATL服务器,你应该在这两行中间使用OBJECT_ENTRY()宏指定暴露给外部应用程序的
对象。这儿什么也没有,所以我就说它“比脸还干净”啦!
我们就先添加一个COM对象!选 Insert->New ATL Object菜单项,ATL Object Wizard
就启动了。看看左边是类别,右边列出每类中它所支持的对象。注意到左边Data Access了
吗!使用其下的Provider和Consumer可以添加支持OLE DB模板的对象!怎么样?吸引人吧?
一步一步来,我们先建立一个最简单的COM对象,选择左边的Objects再在右边选择Simple
Object,按Next,然后在弹出窗口的Short Name中输出一个对象的名字,建立一个简单的
对话的例子,就输入:“Salute”吧!呵!该填满的它都给填满啦!那我们就不动了。你也
可以上Attributes选项卡看看,但我们先全部取默认值,按OK按钮吧!
硬盘在一阵痛苦的痉挛之后,在ClassView中将多了一个CSalute类和一个叫ISalute的接
口。咦?现在它给外界提供了什么接口呢?那就看看呗!再次点击左边Globals中的_Module
上右边看上边提到的那对宏,你发现了什么变化?对啦!现在我们的程序可真的能干活儿啦!
让它干点儿什么活呢?对啦!当你向它问好时,它能简单地回答你一句话,简单吧?那
就干!用鼠标右键点击ISalute,然后在快捷菜单上选择Add Method,于是乎就弹出了个
一对话框。在“Method Name”中输入“HelloMyNameIs”作为方法的名称,在“Parame-
ters”中输入“[in]BSTR sName,[out]BSTR* sAnswers”作为参数信息(可能你对这句
话摸不着头脑,别着急,我在后面说),最后,我们再回头看看上面的“Return Type”,
它是固定的HRESULT类型。按“OK”键!左边Class View树的CSalute->ISalute下就多了我
们刚刚添加方法的定义,双击它,看看ATL向导为我们生成了什么代码!
哈!看到了吗?调用这个方法时,我们要告诉sName我们是谁,而方法将会把回答通过
sAnswers返回给你。向导只在该函数中添加了一行“return S_OK”,那我们就把自己的代
码添加进去吧!添加后的该方法的代码如下所示。这段代码并不复杂,只是需要你知道BSTR
的用法。妈呀!这一展开来说涉及到的东西太多啦!你首先要知道BSTR是用SysAllocString
及相关的函数分配的,必须用SysFreeString来释放,看过下面的代码后我们再详细讲讲BSTR。
STDMETHODIMP CSalute::HelloMyNameIs(BSTR sName, BSTR* sAnswers)
{
// 用到W2A,OLE2T等一些字符串类型转换宏之前必须加上这句。
USES_CONVERSION;
// 先清除输出参数的内容
if ( *sAnswers )
{
::SysFreeString(*sAnswers); // 释放一个BSTR串
*sAnswers = NULL;
}
// 用户调用本函数然后不告诉我他是谁!
if ( 0 == sName )
{
*sAnswers = ::SysAllocString((const OLECHAR*)A2W("说话呀!有事儿没?没事儿
叫我干嘛?!"));
return S_OK;
}
char lpszAnsiAnswers[256];
if ( strstr(OLE2T(sName),"克林顿") )
lstrcpy(lpszAnsiAnswers,"滚!");
else if ( strstr(OLE2T(sName),"达明一排") )
lstrcpy(lpszAnsiAnswers,"看这位仁兄一定是国家栋梁之才,现在在哪高就?");
else
lstrcpy(lpszAnsiAnswers,"虽然我不认识你,但我愿意和你交朋友!");
// 返回值
*sAnswers = ::SysAllocString(A2W(lpszAnsiAnswers));
return S_OK;
}
怎么样?是不是看得很清楚?唯一可能你没接触过的函数就是SysAllocString和
SysFreeString以及A2W、OLE2T两个宏。待俺慢慢道来。要想在一个自动化服务器中添加
方法、属性、事件等,它们的参数必须符合一些强制的约定,比如要与接口传递一个字符
串数据,它就必须是BSTR类型的。通常为BSTR变量赋值的方法是使用Win32 API:
SysAllocString,用以下方法直接给BSTR串赋值的办法是错误的:“BSTR bstr = L"
There's a wrong.";”。也许有些奇怪,BSTR到底是怎么回事?它怎么使用起来有这么
多限制?实际上,它是一个指向wchar_t串的指针,只不过它在串的最前面保存着整个字
符串的字符数。到这儿又有问题了,它有必要保存字符数吗?一个简单的函数不就能查到
字符串的长度吗?哈!要知道,如果我说在一个BSTR串中允许出现多个'/0'字符,那你还
觉得这个串的长度容易判断吗?这就是BSTR的一个与众不同的特点,它可以有多个字符串
终止符。另外,为了简化BSTR的使用,ATL在atlbase.h中定义了一个叫CComBSTR的类,
你可以去看一看,可以学到很多东西。
另外,A2W,W2A等宏都是在Unicode和Ansi字符之间转换用的,你可以在MSDN上查找
“A2W”看到更详细的信息。
下面,我们再来看看在上面添加本方法时输入的“[in]BSTR sName,[out]BSTR*
sAnswers”是怎么回事。当我们要添加一个方法时,完成了方法的名称和参数信息并按OK
按钮之后,Wizard实际上不但在“Salute.h”和“Salute.cpp”文件中以C++风格定义函
数之外,还在“Salute.idl”中加入类似如下一行的接口方法定义:
[id(1), helpstring("method HelloMyNameIs")] HRESULT HelloMyNameIs([in]BSTR
sName,[out]BSTR* sAnswers);
一共在三个地方修改了源程序。
你一定发现了,这并不是C++中定义函数的方法。是啊,这句话实际上使用的是一种叫做
“接口定义语言”(IDL)的的语法,但实际上它和C++的语法是十分相似的。那IDL是什么呢?
实际上,编写一个COM程序,还有很多很多工作需要做,为了简化掉大理琐碎无味的工作,
把程序的注意力集中到组件的功能上来,ATL引入了IDL来描述接口。使用它的目的是为了提
供足够的信息,便于函数的参数可以在客户与服务器之间调整(如果你使用IDL,那么你完
全可以不关心这些)。接下来,你就双击左边的“ISalute”看看代码窗口中的ATLHello.
idl文件的内容吧!我们应该感谢这个文件,它为我们做了无数我们最不爱做的工作!!!
为了更好地做好你不必关心的幕后工作,你需要要对每一个函数指定参数类型是输入、输出
还是输入输出及其它一些说明,这方面的东西老多了,建议你还是看看有关的书籍。
对于IDL接口定义,有几点最重要的东西,你一定要看一看:
1、在接口函数定义时,对于输出参数,要求必须是一个指针!!!重申一遍:必须是一
个指针!!!请回头看一下我们刚加入的方法,它的sAnswers必须是一个指针!!否则编译
会出错。
2、在COM中对字符串的标准约定是使用Unicode。也就是wchar_t串,在COM中,该数据类
型被define成了OLECHAR和LPOLECHAR。所以在程序中可能经常使用转换函数,在使用转换
函数的函数中,要首先执行USES_CONVERSION宏。
3、对于可选参数(可有可没有的那种参数),必需在参数表的最后,并且数据类型必须
为VARIANT!再次注意:输出参数必须是指针!
哇塞!那VARIANT又是什么呢?你一定见过VARIANT的定义,它实际是一个包括各种数据
类型的大Union,你可以通过这种数据类型灵活地对输出及输入参数进行处理。在使用
VARIANT之间一定要使用::VariantInit()函数对其进行初值化,有很多以Variant开头的
API函数用于操作VARIANT数据,你可以在MSDN中找一找,看一看。当然,M$为了简化该数
据类型的使用,在atlbase.h中也定义了CComVariant类,你可以参照源程序和MSDN上的信
息对其进行深入的了解。
看了这么多理论,你是不是已经快睡着了?快起来,我们要试试我们编写的自动化服务
器了!
选择“build”菜单中的“Build ATLHello.dll”项,硬盘在一阵极其痛苦的呻吟之后,
我们的自动化服务器将被自动注册,立刻就可以使用了!
就我本人来看,测试组件,最好的工具不是“ActiveX control test container”而是
VB。快快快!用最快的速度打开VB,我们要用最快的速度建立一个工程测试我们的杰作!
我敢打赌,你也可以绑上右手后在两分钟内完成!
启动VB。选择“工程->引用”并选中“ATLHello 1.0 Type Library”。再在窗体上放
一个Text和一个Button控件,然后在Button控件的Click事件中添加如下代码:
Private Sub Command1_Click()
Dim a As New Salute
Dim sAnswer As String
a.HelloMyNameIs Text1.Text, sAnswer
MsgBox sAnswer
End Sub
OK!现在运行程序,你就可以在Text框中输入“克林顿”“达明一排”之类的文字看看
发生了什么!
如果还有下一讲的话,我们可能要建立一个有点儿实用价值的东西,
我要喝口水去,各位老兄,回头见。
另外,明天是本人2?岁诞辰纪念日,我愿意和大家共同分享我的快乐!!
COM与ATL大扫盲系列之(三)
又一个动人故事的开始: MacroVirusMaker出世记。
谢谢大家!今年的生日是我过得最快乐的一次!感谢大家!
哈!看到本文章的题目,你别误会-----啊?!怎么要做个病毒机器不成?!其实只是起
个骇人听闻的名字,一来让大家对这篇文章感兴趣,二来可以让这个例子的“可扩充性”十
分强,如果我能坚持下去,说不定这真的就是一个完整的??生产机?!我不知道。不过从
这儿开始,我们假设真的要编写一个Word宏病毒生产机,该组件可以由“外部程序”指定要
攻击的Word文件名,然后...然后我还没想好呢!咱们一步一步走着瞧吧!没事儿!反正现在
的宏病毒的传播机制大家都已经知道了(在网络上拌个跟头就是被“美丽杀”源代码拌倒
的)。
前两次大家已经知道如何建立ATL项目以及如何添加一个ATL组件还有怎么添加方法等
。这次我们将从头建立一个新的、比上两次要复杂些、但比上两次更有趣的一个自动化服务
器。这次,OLE异常的建立与生成将是我们讨论的重点。
开始吧!按下面的步骤生成ATL应用程序,大家都会的呀!自已来吧!
1、建立一个ATL项目,名称是:“MacroVirusMaker”,“Server Type”选“DLL”,
其他什么也不选。
2、“Insert ATL Object”->“Objects”->“Simple Object”。Names->ShortName
=“Worker”。Attributes->ISupportErrorInfo选中(选中该项以后,我们就可以在程序里
生成OLE异常了)。其它的取默认值。
3、用右键单击左边的IWorker,添加没有参数的两个Method:“StartWork”、
“EndWork”。其实它们没有太大的用途,只是StartWork做一些初始化的工作,而EndWork
做一些“收尾”的工作罢了。我们做一下规定,使用该组件的程序必须在建立组件后首先调
用StartWork并且必须在使用完的最后调用EndWork。
4、用右键单击左边的IWorker,选“Add Property...”添加WordFileName属性。
Property Type=“BSTR”property name=“WordFileName”选中Get Function和Put Function。
用右键单击左边的CWorker,选“Add Member Variable...”。Variable
Type选“BSTR”,Variable Name输入“m_bstrWordFileName”。
WordFileName属性就是我们要“操作”的Word文档的名字,我们在CWorker中加一个成员
变量保存这个值。
这样,我们就建立了一个简单的ATL应用程序框架了,经过前两节的讨论,大家是不
是对上述操作已经十分熟练了?哈!我真高兴!接着来,我们看看今天有些什么新内容呢?
从最简单的看,我们先给StartWork和EndWork两个接口函数加上内容。左边的CWorker
->IWorker下不是有个StartWork吗?啊!看我干嘛呀?双击它呀!对喽!用此方法类推。修
改后的两个函数内容如下:
STDMETHODIMP CWorker::StartWork()
{
m_bstrWordFileName = NULL;
return S_OK;
}
STDMETHODIMP CWorker::EndWork()
{
if ( m_bstrWordFileName )
{ // 如果需要,释放字符串
::SysFreeString( m_bstrWordFileName );
}
return S_OK;
}
是不是觉得它们的内容特没意思?不要紧!下面的函数就有意思了:
为什么有意思呢?因为你一定又有问题了:“我明明定义WordFileName是一个属性,怎
么在程序中找不到这个属性的源代码,倒变成两个带前缀的函数了?”我说老兄,别再以
“外行”的眼光看任何东西啦!在不知不觉中,你已经走进了自动化服务器的内部了。对外
部说,给属性赋值与取属性值没有区别,但对我们来说,它们的区别是很大的!get_*是外部
程序要获得属性值;put_*是外部程序要给属性赋值。
如果还记不住的话,告诉你个损招。你干脆记住“输出参数必须是指针”就得了!这
不?get_WordFileName的参数是个指针,那它一定是外部程序要获得属性值!反之则...对
吧!不过,最好你别这么记,如果以后你定义了一个复杂的属性,参数中有指针类型的形参
的话,那就不好判断了。这只是个“偏方”,听不听由你。
get_WordFileName和put_WordFileName的代码如下。注意:以下的代码并不完美,它只
是为了让你看懂,在后面,我们将修改它以使它更完善。
STDMETHODIMP CWorker::get_WordFileName(BSTR *pVal)
{
if ( *pVal )
{ // 如果需要,先释放字符串
::SysFreeString( *pVal );
}
if ( m_bstrWordFileName )
{ // 给返回值分配字符串
*pVal = ::SysAllocStringLen( m_bstrWordFileName, ::SysStringLen(m_bstrWordFileName) );
}
return S_OK;
}
STDMETHODIMP CWorker::put_WordFileName(BSTR newVal)
{
USES_CONVERSION;
if ( newVal )
{
if ( m_bstrWordFileName )
{ // 如果需要,先释放字符串
::SysFreeString( m_bstrWordFileName );
}
// 分配新字符串
m_bstrWordFileName = ::SysAllocStringLen( newVal,::SysStringLen(newVal) );
// ToDo: 检查文件是否存在,如果不存在,则生成一个OLE异常
FILE * fp = ::fopen(W2A(newVal),"r");
if ( NULL == fp )
{
// 文件不能打开
return S_FALSE;
}
else
{
::fclose(fp);
}
}
return S_OK;
}
实际上,以上代码是典型的复制BSTR字符串的的方法。上述代码中,新API有两个:
SysStringLen和SysAllocStringLen。它们的作用顾名思义,我就不解释了。
请注意put_WordFileName函数中的“return S_FALSE;”这句话,在文件不能打开时,
它返回一个假值,如果该返回值的方法用在Method上本无可厚非,但用在Property上怕不
妥,因为对于外部程序来说似乎无法判断该属性的返回值是否为S_FALSE。那怎么办呢?哈!
这儿我们将使用一个让人十分感兴趣的技术----OLE异常。下面我们就详细唠一唠它,这可
是我们这次谈论的重点内容!
在用C++编写组件时,大家知道我们可以使用两种异常,一种是C++异常,它只能在定义
它的应用程序中使用;另一种就是OLE异常,它可以将同样类型的错误传递给使用该组件的外
部程序。比如VB的on Error goto ...语句实际上就是在“外部程序”中使用的在“内部程
序”(组件内部)中定义的OLE异常。在添加Work接口时,我们选择了“Support
ISupportErrorInfo”,使本服务器可以在发生错误时通过IErrorInfo寻找更多的信息。
要想使用OLE异常,我们只需要增加一项工作:
我们唯一需要做的准备工作就是生成错误类型的枚举表并把它加到IDL文件中去。注意:
这个枚举表也必须有自己的CLSID(问题又来了!CLSID是什么?我们在后面说。),CLSID
必须要用GUIDGEN.EXE生成。有的同志问:“哎!我怎么没有这个程序?”别着急,它在VS6
光盘第一张的“/COMMON/TOOLS”目录下,快拷贝过来吧!现在我们就运行guidgen.exe,先
选择“registry format”然后按“new GUID”再按拷贝按钮,新生成的GUID就拷贝到剪切
版里了。我们再把类似下面的代码加到MacroVirusMaker.idl中......等等!那位举手的同
学说什么?怎么打开IDL文件?嗨!你双击左边的IWorker不就行了!快坐下!
得!别等后面说了,我还是在这儿说说GUID、UUID和IID吧!
GUID是Globally Unique Identifier(全局唯一标识符)的缩写。GUID是一个128位
的结构。它用来唯一的标识一个接口,它提供了特殊的生成GUID的算法(CoCreateGuid生成
GUD)以保证世界上每一个人每一次生成的GUID都不是重复的,以保证可以方便地设计出国际
化的组件。而每一个接口至少都要有两个标识符,一个是类ID(CLSID)一个是接口ID
(IID),它们实际上都是GUID结构。这三者的关系就是这样。当然,GUID也被广泛地用来唯
一标识不同的组件,甚至被用于在数据库中做PrimaryKey的值(如:Access)。
在ATL中,你经常会看到REFIID类型,实际上它是“const IID&”的define。同理,
REFCLSID和REFGUID的含义我就不用多说了吧?
请大家把类似下述代码加到MacroVirusMaker.idl文件的未尾(其实它可以加到该文件
的任意地方)。
下面代码的上半部分定义了一个新的错误枚举的ID,下半部分不用说,大家都很熟悉。
但有一点要注意:自定义OLE异常的类型值一定不要和预定义的类型相冲突,最好的办法就是
象下面这样,远远地避开,M$总不至于把错误定义50000个吧(也不一定)。
typedef[
uuid(C1F8EDA1-0EF1-11d3-94F4-99B4C6100D86),
helpstring("Worker Error Enumeration")
]
enum tagWorkerError
{
WORKER_ERROR_FILECANNOTOPEN = 5000
} WORKERERROR;
在上面,我们只定义了一个OLE错误类型,当然你可以自己往里面加更多的错误类型!
OK!然后就万事大吉,我们可以生成自己的OLE异常了!为了简化我们的工作,ATL提
供了一AtlReportError函数用来让我们用一行代码生成丰富多采的OLE异常。怎么样?为
ATL喝采吧!看一看MSDN就可以知道,它有四个参数:服务器的CLSID,错误信息串,接口
IID和错误代码。哈!这么看来生成一个错误信息简直是易如反掌!在内部(TMD,又是在
内部),AtlReportError使用IErrorInfo检索并返回错误。
这样,我们就可以对上面put_WordFileName函数中的“return S_FALSE;”这句话进
行一番改造了!示范代码如下
// 注意:这段代码只是替换“return S_FALSE;”那句话
//return S_FALSE;
return AtlReportError(CLSID_Worker,"文件打不开",IID_IWorker,MAKE_SCODE
(SEVERITY_ERROR,FACILITY_ITF,WORKER_ERROR_FILECANNOTOPEN));
怎么样?一句话生成一个OLE异常,你是不是感觉特爽?
OK!现在我们简单地完成了WordFileName属性,现在可以使用它了。
编译!编译!赶快编译!
然后打开VB。新建一个项目并保存之。因为以后我们要经常使用它。
选择“工程”->“引用”->“MacroVirusMaker 1.0 Type Library”
再在窗体上新建一个文本框,再新建一个按钮,然后在按钮的Click事件中这样说:
Private Sub Command1_Click()
Dim vir As New Worker
On Error GoTo ErrProc
vir.StartWork
vir.WordFileName = text1.text
MsgBox vir.WordFileName
vir.EndWork
Exit Sub
ErrProc:
MsgBox "出现错误:" & Err.Description
vir.EndWork
End Sub
然后运行之,再在文本框里输入一个存在的文件名,如:“c:/windows/win.com”,
它会将该字符串返馈给你。
然后你再试着乱敲一个没有的文件名,它就会说“出现错误:文件打不开”,而那句文件打
不开就是我们在接口中定义的错误信息呀!怎么样?是不是一种成就感从你的心头由然而生?
困死了!我要睡觉了!今天我们就先谈到这儿吧!
See you next time!
第二回书, Salute接口的故事
上回书说到,我们已经开始了一个ATL项目并简要向你介绍了一下生成的DLL输出函数
及全局变量都是怎么回事儿。这一次,我们将继续上回的介召并试着建立一个简单的方法,
但更的的应该是理论性的东西。
在这一讲中,我们会接触到BSTR,VARIANT,我们将看到如何添加ATL对象,如何向一个
接口中添加方法,以及添加方法的过程中需要注意的问题,IDL及如何用VB使用我们建立好
的组件。
那就打开上次我们未完成的项目“ATLHello”吧!OK!现在面对着你的是一个比用MFC向导
生成的还“干净”的框架。比你想像的还干净!嗯,看我的脸白吧?比我的脸还干净!因为
它没有提供让外部应用程序创建的对象。也许你会问:“你怎么知道的?”哈!带着手电筒
跟我来!...快!双击左边的_Moudle然后看右边(这句话你不会不懂吧?)注意如下的两
行:
BEGIN_OBJECT_MAP(ObjectMap)
END_OBJECT_MAP()
对于ATL服务器,你应该在这两行中间使用OBJECT_ENTRY()宏指定暴露给外部应用程序的
对象。这儿什么也没有,所以我就说它“比脸还干净”啦!
我们就先添加一个COM对象!选 Insert->New ATL Object菜单项,ATL Object Wizard
就启动了。看看左边是类别,右边列出每类中它所支持的对象。注意到左边Data Access了
吗!使用其下的Provider和Consumer可以添加支持OLE DB模板的对象!怎么样?吸引人吧?
一步一步来,我们先建立一个最简单的COM对象,选择左边的Objects再在右边选择Simple
Object,按Next,然后在弹出窗口的Short Name中输出一个对象的名字,建立一个简单的
对话的例子,就输入:“Salute”吧!呵!该填满的它都给填满啦!那我们就不动了。你也
可以上Attributes选项卡看看,但我们先全部取默认值,按OK按钮吧!
硬盘在一阵痛苦的痉挛之后,在ClassView中将多了一个CSalute类和一个叫ISalute的接
口。咦?现在它给外界提供了什么接口呢?那就看看呗!再次点击左边Globals中的_Module
上右边看上边提到的那对宏,你发现了什么变化?对啦!现在我们的程序可真的能干活儿啦!
让它干点儿什么活呢?对啦!当你向它问好时,它能简单地回答你一句话,简单吧?那
就干!用鼠标右键点击ISalute,然后在快捷菜单上选择Add Method,于是乎就弹出了个
一对话框。在“Method Name”中输入“HelloMyNameIs”作为方法的名称,在“Parame-
ters”中输入“[in]BSTR sName,[out]BSTR* sAnswers”作为参数信息(可能你对这句
话摸不着头脑,别着急,我在后面说),最后,我们再回头看看上面的“Return Type”,
它是固定的HRESULT类型。按“OK”键!左边Class View树的CSalute->ISalute下就多了我
们刚刚添加方法的定义,双击它,看看ATL向导为我们生成了什么代码!
哈!看到了吗?调用这个方法时,我们要告诉sName我们是谁,而方法将会把回答通过
sAnswers返回给你。向导只在该函数中添加了一行“return S_OK”,那我们就把自己的代
码添加进去吧!添加后的该方法的代码如下所示。这段代码并不复杂,只是需要你知道BSTR
的用法。妈呀!这一展开来说涉及到的东西太多啦!你首先要知道BSTR是用SysAllocString
及相关的函数分配的,必须用SysFreeString来释放,看过下面的代码后我们再详细讲讲BSTR。
STDMETHODIMP CSalute::HelloMyNameIs(BSTR sName, BSTR* sAnswers)
{
// 用到W2A,OLE2T等一些字符串类型转换宏之前必须加上这句。
USES_CONVERSION;
// 先清除输出参数的内容
if ( *sAnswers )
{
::SysFreeString(*sAnswers); // 释放一个BSTR串
*sAnswers = NULL;
}
// 用户调用本函数然后不告诉我他是谁!
if ( 0 == sName )
{
*sAnswers = ::SysAllocString((const OLECHAR*)A2W("说话呀!有事儿没?没事儿
叫我干嘛?!"));
return S_OK;
}
char lpszAnsiAnswers[256];
if ( strstr(OLE2T(sName),"克林顿") )
lstrcpy(lpszAnsiAnswers,"滚!");
else if ( strstr(OLE2T(sName),"达明一排") )
lstrcpy(lpszAnsiAnswers,"看这位仁兄一定是国家栋梁之才,现在在哪高就?");
else
lstrcpy(lpszAnsiAnswers,"虽然我不认识你,但我愿意和你交朋友!");
// 返回值
*sAnswers = ::SysAllocString(A2W(lpszAnsiAnswers));
return S_OK;
}
怎么样?是不是看得很清楚?唯一可能你没接触过的函数就是SysAllocString和
SysFreeString以及A2W、OLE2T两个宏。待俺慢慢道来。要想在一个自动化服务器中添加
方法、属性、事件等,它们的参数必须符合一些强制的约定,比如要与接口传递一个字符
串数据,它就必须是BSTR类型的。通常为BSTR变量赋值的方法是使用Win32 API:
SysAllocString,用以下方法直接给BSTR串赋值的办法是错误的:“BSTR bstr = L"
There's a wrong.";”。也许有些奇怪,BSTR到底是怎么回事?它怎么使用起来有这么
多限制?实际上,它是一个指向wchar_t串的指针,只不过它在串的最前面保存着整个字
符串的字符数。到这儿又有问题了,它有必要保存字符数吗?一个简单的函数不就能查到
字符串的长度吗?哈!要知道,如果我说在一个BSTR串中允许出现多个'/0'字符,那你还
觉得这个串的长度容易判断吗?这就是BSTR的一个与众不同的特点,它可以有多个字符串
终止符。另外,为了简化BSTR的使用,ATL在atlbase.h中定义了一个叫CComBSTR的类,
你可以去看一看,可以学到很多东西。
另外,A2W,W2A等宏都是在Unicode和Ansi字符之间转换用的,你可以在MSDN上查找
“A2W”看到更详细的信息。
下面,我们再来看看在上面添加本方法时输入的“[in]BSTR sName,[out]BSTR*
sAnswers”是怎么回事。当我们要添加一个方法时,完成了方法的名称和参数信息并按OK
按钮之后,Wizard实际上不但在“Salute.h”和“Salute.cpp”文件中以C++风格定义函
数之外,还在“Salute.idl”中加入类似如下一行的接口方法定义:
[id(1), helpstring("method HelloMyNameIs")] HRESULT HelloMyNameIs([in]BSTR
sName,[out]BSTR* sAnswers);
一共在三个地方修改了源程序。
你一定发现了,这并不是C++中定义函数的方法。是啊,这句话实际上使用的是一种叫做
“接口定义语言”(IDL)的的语法,但实际上它和C++的语法是十分相似的。那IDL是什么呢?
实际上,编写一个COM程序,还有很多很多工作需要做,为了简化掉大理琐碎无味的工作,
把程序的注意力集中到组件的功能上来,ATL引入了IDL来描述接口。使用它的目的是为了提
供足够的信息,便于函数的参数可以在客户与服务器之间调整(如果你使用IDL,那么你完
全可以不关心这些)。接下来,你就双击左边的“ISalute”看看代码窗口中的ATLHello.
idl文件的内容吧!我们应该感谢这个文件,它为我们做了无数我们最不爱做的工作!!!
为了更好地做好你不必关心的幕后工作,你需要要对每一个函数指定参数类型是输入、输出
还是输入输出及其它一些说明,这方面的东西老多了,建议你还是看看有关的书籍。
对于IDL接口定义,有几点最重要的东西,你一定要看一看:
1、在接口函数定义时,对于输出参数,要求必须是一个指针!!!重申一遍:必须是一
个指针!!!请回头看一下我们刚加入的方法,它的sAnswers必须是一个指针!!否则编译
会出错。
2、在COM中对字符串的标准约定是使用Unicode。也就是wchar_t串,在COM中,该数据类
型被define成了OLECHAR和LPOLECHAR。所以在程序中可能经常使用转换函数,在使用转换
函数的函数中,要首先执行USES_CONVERSION宏。
3、对于可选参数(可有可没有的那种参数),必需在参数表的最后,并且数据类型必须
为VARIANT!再次注意:输出参数必须是指针!
哇塞!那VARIANT又是什么呢?你一定见过VARIANT的定义,它实际是一个包括各种数据
类型的大Union,你可以通过这种数据类型灵活地对输出及输入参数进行处理。在使用
VARIANT之间一定要使用::VariantInit()函数对其进行初值化,有很多以Variant开头的
API函数用于操作VARIANT数据,你可以在MSDN中找一找,看一看。当然,M$为了简化该数
据类型的使用,在atlbase.h中也定义了CComVariant类,你可以参照源程序和MSDN上的信
息对其进行深入的了解。
看了这么多理论,你是不是已经快睡着了?快起来,我们要试试我们编写的自动化服务
器了!
选择“build”菜单中的“Build ATLHello.dll”项,硬盘在一阵极其痛苦的呻吟之后,
我们的自动化服务器将被自动注册,立刻就可以使用了!
就我本人来看,测试组件,最好的工具不是“ActiveX control test container”而是
VB。快快快!用最快的速度打开VB,我们要用最快的速度建立一个工程测试我们的杰作!
我敢打赌,你也可以绑上右手后在两分钟内完成!
启动VB。选择“工程->引用”并选中“ATLHello 1.0 Type Library”。再在窗体上放
一个Text和一个Button控件,然后在Button控件的Click事件中添加如下代码:
Private Sub Command1_Click()
Dim a As New Salute
Dim sAnswer As String
a.HelloMyNameIs Text1.Text, sAnswer
MsgBox sAnswer
End Sub
OK!现在运行程序,你就可以在Text框中输入“克林顿”“达明一排”之类的文字看看
发生了什么!
如果还有下一讲的话,我们可能要建立一个有点儿实用价值的东西,
我要喝口水去,各位老兄,回头见。
另外,明天是本人2?岁诞辰纪念日,我愿意和大家共同分享我的快乐!!
COM与ATL大扫盲系列之(三)
又一个动人故事的开始: MacroVirusMaker出世记。
谢谢大家!今年的生日是我过得最快乐的一次!感谢大家!
哈!看到本文章的题目,你别误会-----啊?!怎么要做个病毒机器不成?!其实只是起
个骇人听闻的名字,一来让大家对这篇文章感兴趣,二来可以让这个例子的“可扩充性”十
分强,如果我能坚持下去,说不定这真的就是一个完整的??生产机?!我不知道。不过从
这儿开始,我们假设真的要编写一个Word宏病毒生产机,该组件可以由“外部程序”指定要
攻击的Word文件名,然后...然后我还没想好呢!咱们一步一步走着瞧吧!没事儿!反正现在
的宏病毒的传播机制大家都已经知道了(在网络上拌个跟头就是被“美丽杀”源代码拌倒
的)。
前两次大家已经知道如何建立ATL项目以及如何添加一个ATL组件还有怎么添加方法等
。这次我们将从头建立一个新的、比上两次要复杂些、但比上两次更有趣的一个自动化服务
器。这次,OLE异常的建立与生成将是我们讨论的重点。
开始吧!按下面的步骤生成ATL应用程序,大家都会的呀!自已来吧!
1、建立一个ATL项目,名称是:“MacroVirusMaker”,“Server Type”选“DLL”,
其他什么也不选。
2、“Insert ATL Object”->“Objects”->“Simple Object”。Names->ShortName
=“Worker”。Attributes->ISupportErrorInfo选中(选中该项以后,我们就可以在程序里
生成OLE异常了)。其它的取默认值。
3、用右键单击左边的IWorker,添加没有参数的两个Method:“StartWork”、
“EndWork”。其实它们没有太大的用途,只是StartWork做一些初始化的工作,而EndWork
做一些“收尾”的工作罢了。我们做一下规定,使用该组件的程序必须在建立组件后首先调
用StartWork并且必须在使用完的最后调用EndWork。
4、用右键单击左边的IWorker,选“Add Property...”添加WordFileName属性。
Property Type=“BSTR”property name=“WordFileName”选中Get Function和Put Function。
用右键单击左边的CWorker,选“Add Member Variable...”。Variable
Type选“BSTR”,Variable Name输入“m_bstrWordFileName”。
WordFileName属性就是我们要“操作”的Word文档的名字,我们在CWorker中加一个成员
变量保存这个值。
这样,我们就建立了一个简单的ATL应用程序框架了,经过前两节的讨论,大家是不
是对上述操作已经十分熟练了?哈!我真高兴!接着来,我们看看今天有些什么新内容呢?
从最简单的看,我们先给StartWork和EndWork两个接口函数加上内容。左边的CWorker
->IWorker下不是有个StartWork吗?啊!看我干嘛呀?双击它呀!对喽!用此方法类推。修
改后的两个函数内容如下:
STDMETHODIMP CWorker::StartWork()
{
m_bstrWordFileName = NULL;
return S_OK;
}
STDMETHODIMP CWorker::EndWork()
{
if ( m_bstrWordFileName )
{ // 如果需要,释放字符串
::SysFreeString( m_bstrWordFileName );
}
return S_OK;
}
是不是觉得它们的内容特没意思?不要紧!下面的函数就有意思了:
为什么有意思呢?因为你一定又有问题了:“我明明定义WordFileName是一个属性,怎
么在程序中找不到这个属性的源代码,倒变成两个带前缀的函数了?”我说老兄,别再以
“外行”的眼光看任何东西啦!在不知不觉中,你已经走进了自动化服务器的内部了。对外
部说,给属性赋值与取属性值没有区别,但对我们来说,它们的区别是很大的!get_*是外部
程序要获得属性值;put_*是外部程序要给属性赋值。
如果还记不住的话,告诉你个损招。你干脆记住“输出参数必须是指针”就得了!这
不?get_WordFileName的参数是个指针,那它一定是外部程序要获得属性值!反之则...对
吧!不过,最好你别这么记,如果以后你定义了一个复杂的属性,参数中有指针类型的形参
的话,那就不好判断了。这只是个“偏方”,听不听由你。
get_WordFileName和put_WordFileName的代码如下。注意:以下的代码并不完美,它只
是为了让你看懂,在后面,我们将修改它以使它更完善。
STDMETHODIMP CWorker::get_WordFileName(BSTR *pVal)
{
if ( *pVal )
{ // 如果需要,先释放字符串
::SysFreeString( *pVal );
}
if ( m_bstrWordFileName )
{ // 给返回值分配字符串
*pVal = ::SysAllocStringLen( m_bstrWordFileName, ::SysStringLen(m_bstrWordFileName) );
}
return S_OK;
}
STDMETHODIMP CWorker::put_WordFileName(BSTR newVal)
{
USES_CONVERSION;
if ( newVal )
{
if ( m_bstrWordFileName )
{ // 如果需要,先释放字符串
::SysFreeString( m_bstrWordFileName );
}
// 分配新字符串
m_bstrWordFileName = ::SysAllocStringLen( newVal,::SysStringLen(newVal) );
// ToDo: 检查文件是否存在,如果不存在,则生成一个OLE异常
FILE * fp = ::fopen(W2A(newVal),"r");
if ( NULL == fp )
{
// 文件不能打开
return S_FALSE;
}
else
{
::fclose(fp);
}
}
return S_OK;
}
实际上,以上代码是典型的复制BSTR字符串的的方法。上述代码中,新API有两个:
SysStringLen和SysAllocStringLen。它们的作用顾名思义,我就不解释了。
请注意put_WordFileName函数中的“return S_FALSE;”这句话,在文件不能打开时,
它返回一个假值,如果该返回值的方法用在Method上本无可厚非,但用在Property上怕不
妥,因为对于外部程序来说似乎无法判断该属性的返回值是否为S_FALSE。那怎么办呢?哈!
这儿我们将使用一个让人十分感兴趣的技术----OLE异常。下面我们就详细唠一唠它,这可
是我们这次谈论的重点内容!
在用C++编写组件时,大家知道我们可以使用两种异常,一种是C++异常,它只能在定义
它的应用程序中使用;另一种就是OLE异常,它可以将同样类型的错误传递给使用该组件的外
部程序。比如VB的on Error goto ...语句实际上就是在“外部程序”中使用的在“内部程
序”(组件内部)中定义的OLE异常。在添加Work接口时,我们选择了“Support
ISupportErrorInfo”,使本服务器可以在发生错误时通过IErrorInfo寻找更多的信息。
要想使用OLE异常,我们只需要增加一项工作:
我们唯一需要做的准备工作就是生成错误类型的枚举表并把它加到IDL文件中去。注意:
这个枚举表也必须有自己的CLSID(问题又来了!CLSID是什么?我们在后面说。),CLSID
必须要用GUIDGEN.EXE生成。有的同志问:“哎!我怎么没有这个程序?”别着急,它在VS6
光盘第一张的“/COMMON/TOOLS”目录下,快拷贝过来吧!现在我们就运行guidgen.exe,先
选择“registry format”然后按“new GUID”再按拷贝按钮,新生成的GUID就拷贝到剪切
版里了。我们再把类似下面的代码加到MacroVirusMaker.idl中......等等!那位举手的同
学说什么?怎么打开IDL文件?嗨!你双击左边的IWorker不就行了!快坐下!
得!别等后面说了,我还是在这儿说说GUID、UUID和IID吧!
GUID是Globally Unique Identifier(全局唯一标识符)的缩写。GUID是一个128位
的结构。它用来唯一的标识一个接口,它提供了特殊的生成GUID的算法(CoCreateGuid生成
GUD)以保证世界上每一个人每一次生成的GUID都不是重复的,以保证可以方便地设计出国际
化的组件。而每一个接口至少都要有两个标识符,一个是类ID(CLSID)一个是接口ID
(IID),它们实际上都是GUID结构。这三者的关系就是这样。当然,GUID也被广泛地用来唯
一标识不同的组件,甚至被用于在数据库中做PrimaryKey的值(如:Access)。
在ATL中,你经常会看到REFIID类型,实际上它是“const IID&”的define。同理,
REFCLSID和REFGUID的含义我就不用多说了吧?
请大家把类似下述代码加到MacroVirusMaker.idl文件的未尾(其实它可以加到该文件
的任意地方)。
下面代码的上半部分定义了一个新的错误枚举的ID,下半部分不用说,大家都很熟悉。
但有一点要注意:自定义OLE异常的类型值一定不要和预定义的类型相冲突,最好的办法就是
象下面这样,远远地避开,M$总不至于把错误定义50000个吧(也不一定)。
typedef[
uuid(C1F8EDA1-0EF1-11d3-94F4-99B4C6100D86),
helpstring("Worker Error Enumeration")
]
enum tagWorkerError
{
WORKER_ERROR_FILECANNOTOPEN = 5000
} WORKERERROR;
在上面,我们只定义了一个OLE错误类型,当然你可以自己往里面加更多的错误类型!
OK!然后就万事大吉,我们可以生成自己的OLE异常了!为了简化我们的工作,ATL提
供了一AtlReportError函数用来让我们用一行代码生成丰富多采的OLE异常。怎么样?为
ATL喝采吧!看一看MSDN就可以知道,它有四个参数:服务器的CLSID,错误信息串,接口
IID和错误代码。哈!这么看来生成一个错误信息简直是易如反掌!在内部(TMD,又是在
内部),AtlReportError使用IErrorInfo检索并返回错误。
这样,我们就可以对上面put_WordFileName函数中的“return S_FALSE;”这句话进
行一番改造了!示范代码如下
// 注意:这段代码只是替换“return S_FALSE;”那句话
//return S_FALSE;
return AtlReportError(CLSID_Worker,"文件打不开",IID_IWorker,MAKE_SCODE
(SEVERITY_ERROR,FACILITY_ITF,WORKER_ERROR_FILECANNOTOPEN));
怎么样?一句话生成一个OLE异常,你是不是感觉特爽?
OK!现在我们简单地完成了WordFileName属性,现在可以使用它了。
编译!编译!赶快编译!
然后打开VB。新建一个项目并保存之。因为以后我们要经常使用它。
选择“工程”->“引用”->“MacroVirusMaker 1.0 Type Library”
再在窗体上新建一个文本框,再新建一个按钮,然后在按钮的Click事件中这样说:
Private Sub Command1_Click()
Dim vir As New Worker
On Error GoTo ErrProc
vir.StartWork
vir.WordFileName = text1.text
MsgBox vir.WordFileName
vir.EndWork
Exit Sub
ErrProc:
MsgBox "出现错误:" & Err.Description
vir.EndWork
End Sub
然后运行之,再在文本框里输入一个存在的文件名,如:“c:/windows/win.com”,
它会将该字符串返馈给你。
然后你再试着乱敲一个没有的文件名,它就会说“出现错误:文件打不开”,而那句文件打
不开就是我们在接口中定义的错误信息呀!怎么样?是不是一种成就感从你的心头由然而生?
困死了!我要睡觉了!今天我们就先谈到这儿吧!
See you next time!