今日来在学习COM编程,找到一些自认为有价值文件分享给大家:
在C++中创建COM DLL
在本篇文章中我们将讨论如何在MSVC++中开发一个非常简单的ActiveX dll,并在Visual Basic中对它进行调用,其中的例子将用到Active Template Library(ATL)和相关的向导工具。本篇文章不会涉及COM和ATL的工作原理(尽管这是必需的),也不要求读者具有任何ATL方面的经验。
IDL:主要的差别
在C++与VB中开发ActiveX的最大的不同之处(除了语言和其他一些小的方面)是IDL的引入。IDL是界面定义语言(Interface Definition Language)的简写,就象其名字那样,它定义了dll的界面。客户端可以用界面将自己与对象和dll的方法绑定,但它的作用并不仅仅限于此。
通过使用IDL,我们可以将对象的外观和代码完全独立开来,隐藏在界面之后的任何东西可以随时被改变,任何使用dll的客户端程序都无需有任何改变,因为界面并没有改变。IDL还定义了过程之间传递的数据类型和参数。
由于IDL是完全独立于具体语言的,不仅仅局限于COM或C++,CORBA同样也可以使用IDL来定义界面。
所有的VB AX(ActiveX)对象在编译时都使用了IDL类型库,根据对象、方法和函数在VB程序中是如何定义的,VB会在幕后完成所有的工作。在MSVC++中,创建AX dll时不会自动生成IDL,类型库并非是COM对象所必需的(DirectX COM API就是一个很好的例子。),这个工作需要C++编程人员来完成。
MSVC++ COM ATL工程
创建一个ATL Com AppWizard工程,并将它命名为MyAXDll。具体的方法我在这里就不啰嗦了。
AppWizard将在工程中添加一些文件,最重要的是MyAXDll.cpp,这个文件包括了每个COM对象必须提供的dll导出函数━━DllCanUnloadNow、DllGetClassObject、DllRegisterServer和DllUnregisterServer,这些函数是dll在注册和创建时就带有的。向导代码forwards调用基本ATL CComModule类的变量_Module,该类中包含COM对象创建、注册和注销它自己所必需的所有基本函数。
AppWizard 还会添加一个MyAXDll.idl文件,这个文件包含该工程中所有的对象、属性和方法的IDL。
这个工程本身并没有什么有用的东西,它只是包括一些COM对象和函数的外部结构和模块,如果要使该工程具有一定的功能,就需要在其中添加一个对象。添加对象的方法相信对于大家都是一件再简单不过的事儿了,我在这里就不再详细讨论了。
向导在该工程中添加了三个文件━━MyObject.h、MyObject.cpp和MyObject.rgs,rgs文件包含用一种注册语言编写的COM类的注册设置,其内容在编译过的dll文件中是作为一种注册源存在的,可以被Registration使用,以正确地注册dll。MyObject.h文件中包含MyObject类的定义,打开该文件并详细观察其代码就可以发现,该文件包含了一个类━━CMyObject,该类是由其他四个类继承生成的,其中的三个是模式板类,另一个是一个独立的类(ISupportErrorInfo)。此外,其中还包含一些宏定义:
DECLARE_REGISTRY_RESOURCEID(IDR_MYOBJECT)
DECLARE_PROTECT_FINAL_CONSTRUCT()
BEGIN_COM_MAP(CMyObject)
COM_INTERFACE_ENTRY(IMyObject)
COM_INTERFACE_ENTRY(IDispatch)
COM_INTERFACE_ENTRY(ISupportErrorInfo)
END_COM_MAP()
DECLARE_REGISTRY_RESOURCEID宏扩展为一个静态函数,该函数在系统注册表中注册该服务器(COM对象),IDR_MYOBJECT是要注册的类的源ID。
DECLARE_PROTECT_FINAL_CONSTRUCT()可以确保任何内部对象不会产生调用FinalConstruct()的对象,被组合的对象必须使用该宏。
BEGIN_COM_MAP()、COM_INTERFACE_ENTRY()和END_COM_MAP()的定义是有超前性的,对它们的讨论已经超出本篇文章的范围。我们只要理解类执行的每个界面就可以了,COM_MAP中的COM_INTERFACE_ENTRY(ITheInterface)是必需的。
MyObject.h中接下来的代码是一个函数的定义:
STDMETHOD(InterfaceSupportsErrorInfo)(REFIID riid);
这个函数的代码是在MyObject.cpp中完成的。宏STDMETHOD(FunctionName)可以扩展成如下的形式:
virtual HRESULT STDMETHODCALLTYPE FunctionName
STDMETHODCALLTYPE是一个标准的__stdcall类型,因此,宏STDMETHOD可以确保该函数使用标准的调用规则,并返回一个HRESULT类型的值。
InterfaceSupportsErrorInfo函数是ISupportErrorInfo界面中除必需的函数之外的唯一函数。通过执行这个界面,服务器可以设置出错信息对象,客户端可以使用这一出错信息对象。尽管VB中的错误与COM错误都属同一类型,但这种处理错误信息的方式与VB有很大不同。由于恰当的错误信息处理方式对于软件是极为重要的,因此很有必要对这一问题进行深入的探讨。
COM和COM错误
MyAXdll工程中的COM对象执行ISupportErrorInfo界面,这一界面使服务器的客户端可以查询扩展的错误信息。这种方式会使已经习惯了标准VB错误处理方式的用户感到迷惑。二者之间的差别在于对象的方法的返回值上。在VB中,我们在函数的首部定义返回值的类型,如下所示:
Public Function DoIt() As Long...DoIt = SomeValueEnd Function
上面的代码将函数返回值的类型定义成Long类型数据,在VB和脚本语言中这没有什么问题。在C++中则完全不同,该函数的头部将变成如下所示的格式:
HRESULT __stdcall DoIt( long *return_value ){...*return_value = SomeValue;return S_OK;}
上面的函数头部将返回值定义成S_OK,它向客户端表明,函数执行成功,真正可用的返回值被传送到一个与ByRef类似的参数中。那么为什么会是这样呢?
其原因就在于COM的构建方式。COM子系统有时需要通知客户端在函数调用中发生了错误,例如,如果一个客户端正在访问一个远程服务器,如果该服务器没有在网络上,此时,COM子系统就有许多返回值可供选择,而对异常的自理又对语言有较大的依赖性。因此 ,几乎所有的COM方法都返回一个HRESULT值,这个值可以表明函数的执行结果。在SDK中的 文件定义了一些基本的返回类型,其中最常用的二种为:S_OK,它表明函数执行正常;E_FAIL表示函数执行异常。
表1列出了一些常用的返回类型:
E_NOTIMPL :没有执行
E_OUTOFMEMORY:内存溢出
E_INVALIDARG :一个或多个参数无效
E_NOINTERFACE:不支持界面
E_POINTER :无效的指针
E_HANDLE :无效的句柄
E_ABORT :操作中止
上面的表列出了可以在过程之间传递的消息,一旦过程在执行中有什么不正常,COM就可以把这些消息通知客户端,在VB中这一切是如何实现的呢?
VB也有同样的机制。如果一个VB函数得到成功的执行,VB就加返回一个S_OK值;如果函数执行有什么异常,就会出现错误,VB会返回一个类型为HRESULT的错误对象的错误代码。
当VB客户端程序访问COM服务器时,如果返回一个调用失败的HRESULT值,VB就会立即查询服务器看它是否支持ISupportErrorInfo界面。因为如果服务器支持这一界面,VB将向InterfaceSupportErrorInfo函数查询失败的界面是否支持扩展的错误信息。
由于二者相同,InterfaceSupportErrorInfo会返回一个true值,这表明该界面不支持扩展错误信息,VB会询问其他界面看它们是否支持这些信息━━这一切都是由ATL在幕后完成的,因此只要COM服务器设置了相应的信息,VB是可以得到扩展错误信息的。VB使用这些信息建立相似的错误对象,然后给出一个错误类型值━━类型为HRESULT的值。
因此,如果在VB中发现一个错误信息,真正的错误仍然只局限于COM对象的范围内,VB只是简单地返回Err.Number的值,它只用于客户端向COM对象查询扩展错误信息。
稍后,我们将会仔细讨论如何设置错误信息。
在对象中添加方法
现在我们来向对象中添加方法。本例中的对象有3种方法:
1、取二个long类型值,将二者相加,并返回结果。
2、取一个long型数据并报告出错信息
3、取一个long型数据,将它转换为BSTR数据,并返回这个数据
在一个COM对象中添加方法需要二个步骤:1、需要在IDL文件中定义方法;2、必须由MyObject类来实现。为了说明在COM对象中添加方法的具体步骤,我们将用手工方式添加第一个方法,第二、三个方法则使用AppWizard向导来完成。
打开MyAXDll.idl文件。文件中的IDL可以分为二部分,第一部分是MyObject对象的定义,下面是该对象所有的方法、属性。代码如下所示:
MyObject definitions
[
object,
uuid(28D7C31F-1FB2-4BF8-BC98-C8A256348354), dual,
helpstring("IMyObject Interface"),
pointer_default(unique)
] interface IMyObject : IDispatch
{
};
The MyAXDll definitions:
[ uuid(E37200F2-E3DD-4243-9248-AA6A7FE71369),
version(1.0),
helpstring("MyAXDll 1.0 Type Library")
]
library MYAXDLLLib
{
importlib("stdole32.tlb"); importlib("stdole2.tlb");
[
uuid(09374147-1B90-4D78-B5B1-6E5B97C60DF2), helpstring("MyObject Class")
]
coclass MyObject
{
[default] interface IMyObject;
};
};
MyAXDll把MyObject界面作为一个联合类。
要为MyObject定义一个方法,该方法必须添加在上面代码中IMyObject : IDispatch { }中的二个括弧中间。把下列内容添加到二个括弧之间:
[id(1), helpstring("AddLongs method - adds two longs and return the result")]
HRESULT AddLongs([in] long FirstParam, [in] long SecondParam, [out, retval] long* rt);
这是在对象中添加方法的最基本IDL。 [id(1), helpstring("AddLongs method - adds two longs and return the result")]设置要添加的方法的一些属性。id(#)设置供IDispatch界面函数使用的对象的DISPID,Helpstring是一个解释这个方法作用的字符串,我们可以在VB中的对象浏览器中查看这个字符串。
真正的函数: HRESULT AddLongs([in] long FirstParam, [in] long SecondParam, [out, retval] long* rt);除了[direction]标志外是非常简单的,这些标志用于表明如何以及谁对参数进行控制,他们同VB如何向函数传递参数有直接的关系。下面列出的一些常用的标记组合:
[in] 与VB中的ByVal相同,表明该参数是由调用者分配存储和控制的。
[out] 表明该参数由被调用函数分配存储空间。
[in, out] 与VB中的ByRef相同,表明该参数应该由调用者分配存储空间,被调用函 数可以回收该参数的存储空间,也可以重新为该参数分配空间。
[out, retval] 与VB中的As Type返回值相同,如果指针指向一个指针,则被调用函数只能为该参数分配存储空间。
要完成这个函数,需要打开MyObject.h文件。在类的public小节中添加下面基本的函数定义:
STDMETHOD(AddLongs)( long FirstParam, long SecondParam, long* rt);
这只是该对象的定义,还需要在MyObject.cpp文件中添加下面的内容:
STDMETHODIMP CMyObject::AddLongs(long FirstParam, long SecondParam, long *rt)
{
*rt = FirstParam + SecondParam;
return S_OK;
}
至此,我们就为该对象添加了一个方法。
第二个函数通过使用向导工具可以很方便地进行定义。在工程资源管理器中选择ClassView标签,扩展CMyObject并选择IMyObject界面,右击该界面并选择“添加方法”,把该方法命名为RaiseAnError,并把下面的内容添加到参数框中:
[in] long FirstParam, [out,retval] long * rt
至此,向导已经在MyAXDll.idl中添加了该方法的IDL,在MyObject.h、MyObject.cpp文件中添加了该函数的框架。打开MyObject.cpp文件,在其中的RaiseAnError函数中添加下面的内容:
STDMETHODIMP CMyObject::RaiseAnError(long FirstParam, long *rt)
{
//该函数必须设置错误对象
return AtlReportError( CLSID_MyObject, "Upps an error occurred", IID_IMyObject, E_FAIL );
}
该函数使用了AtlReportError帮助函数,该函数设置错误信息,并能够让VB得到并显示这些错误信息。
由于使用了BSTR,最后一个函数有点特殊,BSTR是一个指向OLECHAR的简单指针,分配和收回BSTR的存储空间必须通过SysAllocaXX字符串函数实现,否则就可能扰乱COM系统,从而可能使你遇到一些奇怪的问题。
The function takes one long value, convert it to a BSTR and returns it. Define it like this, using the AppWizard mentioned above:
该函数取得一个long型数据,转换为BSTR数据并将转换的结果返回给调用者。用AppWizard定义如下:
名字: ConvertToString
参数: [in] long FirstParam, [out, retval] BSTR* ConvertedValue
生成的代码非常简单,如下所示:
STDMETHODIMP CMyObject::ConvertToString(long FirstParam, BSTR* ConvertedValue )
{
TCHAR tzConverted[20];
//清零
memset( tzConverted, 0, 20 );
//用C运行库函数进行转换
_itoa( FirstParam, tzConverted, 10 ); //转换为BSTR
USES_CONVERSION;
if ( ::SysReAllocString( ConvertedValue, A2OLE( tzConverted ) ) )
return S_OK;
return E_OUTOFMEMORY;
}
这里比较重要的一点是,返回值的存储空间的分配是由SysReAllocString()完成的,由于BSTR是由客户端程序创建的,使用SysAllocString()函数可以保证它不被覆盖。
That is it. Compile the project and fix any errors that have sneaked into the typing of the code.
所有的3个方法都添加后,编译该工程并修正其中出现的错误。
客户端软件
客户端软件是一个VB程序,我们可以使用本篇文章附加的程序(需要首先编译dll)也可以自己创建一个VB程序。在工程符号库中为MyAXdll.dll类型库建立一个符号。
结论
本篇文章没有详细讨论COM和ATL的有关知识,但它从一个VB编程人员的角度出发,简要地介绍了如何使用ATL和COM对VB进行扩展的基本技巧。