接触VC之四:COM组件模型基础
作者:释雪
一年又一年,已经又过了一年了。我VC的生涯已经两岁了。可以相当地庆贺一下哟。回顾这一年的学习(唉,还没有工作实践呢。这年头,工作不好找哇。),还学了不少的好东西。其中,最重要的就是COM组件模型,我个人觉得这个几乎是Windows的核心。许多先进的技术(比如微软著名的DirectX,ADO,没有人会不知道吧)都以COM组件的形式发布的。现在,我瞄上了另一个好东东,就是泛型编程技术。它能够编写出清晰、灵活、高度可重用的代码,在ATL中就可以依稀看出它的影子(现在网上ATL文章有很多,我以后也会谈到它)。好了,关于泛型编程的事今后再谈。
按照我以前的计划,我应该谈谈我对COM组件模型的认识了。一来可以对自己的学习状况进行总结。二来,请教高手,可以帮忙指出错漏之处。三来,说不定会对初学者们有所帮助。请各位高手多多指正啊.在这里先谢了。
一、动态链接库:
动态链态库是大部分COM组件的承载对象(不要在意ocx,它同样也是dll,只不过改了一下后缀而已)。当然Exe同样也是可以的(TTS中的TextToSpeech对象就是一个例证),只不过在事实上要少得多。
在Windows初期,动态链态库的出现是一场革命。它改变了Windows的一生,也为当今Windows操作系统的霸主地位打下一块坚实的基石。(关于Windows的历史问题,我一直没有弄得太清楚。请VCKBASE的有关史学家们尽快写出一篇文章来吧^_^)。
微软对动态链接库就是这样解释的:
动态链接库 (DLL) 是作为共享函数库的可执行文件。动态链接提供了一种方法,使进程可以调用不属于其可执行代码的函数。函数的可执行代码位于一个 DLL 中,该 DLL 包含一个或多个已被编译、链接并与使用它们的进程分开存储的函数。DLL 还有助于共享数据和资源。多个应用程序可同时访问内存中单个 DLL 副本的内容。
嗯,讲得很清楚。动态链接库首先是一个可执行文件(微软解释说,exe叫做起直接可执行文件),它里面包含着一组需要共享的函数。当使用时,动态链接库(和Windows系统)会提供一个方法来使我们的应用程序可以调用其中的函数。此外,动态链接库还会包含一些资源(如:图标、对话框模板等等)。在MFC中,微软在现有动态链接库的基础上施用了一些技巧来提供一些另外功能,如MFC类的导出。
动态链接库的链接方式大致分为两类: 静态链接和动态链接.
静态链接又叫隐式链接,这种链接方式使我们在代码中不用语句来指示系统中,我们的应用程序要加载哪些动态链接库。其静态链接声明是放在工程属性中的(或者使用#pragma comment(lib,"XXX.lib"),这个可以和#include放在一起)。在指定时,只需要输入其动态链接库相应的导入库文件(.lib)。然后,你就可以在程序的任何地方像调用普通函数一样调用该动态链接库中存在的函数了(当然,你需要包含其相应的头文件。一般情况下,头文件会和LIB文件一块给出)。通过这种方法生成的程序在运行初始化的时候(具体到什么时候不太清楚。但我可以肯定是在WinMain函数之前了^_^),会自动将动态链接库加载在系统环境中,并将其映射到我们应用程序的进程当中去。当我们调用一个我们进程没有定义的函数时,VC运行库会通过查找LIB文件的相关信息找到相应动态链接库的函数并调用它。进程结束时,系统会缷载动态链接库。
动态链接又叫显式链接,顾名思义这种方式让我们必需在代码通过调用API来显式地加载动态链接库。COM组件模型全部都是采用这种方式来加载进程内组件模块(就是Dll)的。(我觉得微软的专业术语有些混乱耶)。这个方式有许多好处,它可以在运行时决定具体要加载哪个链接库,要调用哪个函数…这才叫动态链接呢。
要使用动态链接库并不难,首先要调用LoadLibrary,其原型如下:
HMODULE LoadLibrary( LPCTSTR lpFileName // file name of module );参数lpFileName是要加载的动态链接库的文件名。如果加载成功的话,就返回其句柄。否则的,返回NULL。
BOOL FreeLibrary( HMODULE hModule // handle to DLL module );这个就不用我多说了吧。
BOOL WINAPI DllMain( HINSTANCE hinstDLL, // handle to the DLL module DWORD fdwReason, // reason for calling function LPVOID lpvReserved // reserved );你可以通过DllMain函数来完成你的动态链接库中的环境初始化和析构操作。啊,事情是这样的:
FARPROC GetProcAddress( HMODULE hModule, // handle to DLL module LPCSTR lpProcName // function name );啊,hModule我就不说了。lpProcName参数是一个字符串,这个字符串写着我们要找的函数的函数名。如果找到了的话,就返回这个函数的指针,否则返回NULL。
HMODULE hMathLib = LoadLibrary("Math.dll"); int (*MyProc)(int, int) = NULL; int x = 1, y = 1; MyProc = (int(*)(int, int)) GetProcAddress(hMathLib, "Plus"); If (MyProc != NULL) { printf ("%d", (*MyProc)(x, y)); } FreeLibrary(hMathLib);如果我以及这个链接库没有问题的话,我想输出结果应该是2。
#define DefMathProc(name) int (*name)(int, int) #define FUNCTION(name) (*name) DefMathProc(MyProc) = NULL; MyProc = (DefMathProc()) GetProcAddress(hMathLib, "Plus"); nResult = FUNCTION(MyProc)(x, y);虽然会出一个警告,但我觉得这样会舒服一些。
class physiology { public: virtual void eat(Food in) = 0; virtual void drink(Liquid in) = 0; virtual Somethings toilet() = 0; }; class psychics { public: virtual Sound laugh() = 0; virtual Sound cry() = 0; virtual Sound angry() = 0; }; class dynamics { public: virtual Speed run() = 0; virtual Speed walk() = 0; virtual Interval jump() = 0; };我将人的行为分成了生理学、心理学和动力学三类,让它们分别表示人不同的行为。那么,这么三组相关函数就是三个接口。C++组件对象的实现就是从这些接口中多重派生,并实现它们。这样,我们就得到一个组件对象(声明啊,本示例只是一个表示概念,真正的COM组件对象还需要加一些东东)。
class human : public physiology, public psychics, public dynamics { public: void eat(Food in) { cout << "Good! Very delicious!"; } void drink(Liquid in) { cout << "No! I am not drunk!"; } Something toilet() { cout << "hum……."; return dejecta(); } Sound laugh() { return Sound("Ha…Ha…Ha"); } Sound cry() { return Sound("dad!Don’t beat my buns."); } Sound angry() { return Sound("where did you go last night? Darling."); } Speed run() { cout << "Run, Police come!"; return 20km/h; } Speed walk() { cout << "out. yegg, I am no…not afraid o….of y…you."; return 1m/s; } Interval jump() { cout << "Yeah…."; return 4m; } };
typedef struct _GUID { DOWRD Data1; WORD Data2; WORD Data3; WORD Data4[8]; }GUID;结构用来储存一些数字信息,来表识一个COM对象,接口以及其它COM元素。这个结构体就叫做标识符。
extern "C" const GUID CLISID_MYSPELLCHECKER = {0x54bf6567, 0x1007, 0x11d1, {0xb0, 0xaa, 0x44, 0x45, 0x53, 0x54, 0x00, 0x00}}同样的标识符在其它非C环境中是这么表示的:
{54bf6567-1007-11d1-b0aa-444553540000}这个标识符代表着一个COM对象,这是因为一个COM对象的标识符名都以CLISID_为前缀。接口名则是以IID_为前缀。不要问我,标识符定义与对象具体有什么关系式。我不知道。它们根本就没有什么关系的。一个COM对象在编写时,我们会使用随机的方法来确定它的标识符(这个工作可以由VC来帮我们搞定)。一旦COM对象得到一个标识符并发布出去的话,那么就不能更改了。另外,不要担心GUID会有所冲突。如果你的高中数学已经及格了的话,那么请算一算128位二进制中,重复的概率会有多少。假如你真的发现了GUID有冲突的话(你要保证这不是人为),建议你赶去买彩票吧。你离500万不远了。
typedef GUID IID; class IUnknown { public: virtual HRESULT _stdcall QueryInterface(const IID& iid, void **ppv) = 0; virtual ULONG _stdcall AddRef() = 0; virtual ULONG _stdcall Release() = 0; };
// {6AAF876E-FCED-4ee0-B5D3-63CD6E2242F5} static const GUID IID_IPhysiology = { 0x6aaf876e, 0xfced, 0x4ee0, { 0xb5, 0xd3, 0x63, 0xcd, 0x6e, 0x22, 0x42, 0xf5 } }; class IPhysiology: public IUnknown { public: …… }; // {183FC7A1-4C27-4c38-B72D-D1326E2E8A7C} static const GUID IID_IPsychics = { 0x183fc7a1, 0x4c27, 0x4c38, { 0xb7, 0x2d, 0xd1, 0x32, 0x6e, 0x2e, 0x8a, 0x7c } }; class IPsychics: public IUnknown { public: …… }; // {5F144D5C-A20C-42e7-8F91-4D5CAE430B29} static const GUID IID_IDynamics = { 0x5f144d5c, 0xa20c, 0x42e7, { 0x8f, 0x91, 0x4d, 0x5c, 0xae, 0x43, 0xb, 0x29 } }; class IDynamics: public IUnknown { public: …… }; // {ABFA7022-7E2F-4d0e-8A4F-F58BBCEBB2DA} static const GUID CLISID_Human = { 0xabfa7022, 0x7e2f, 0x4d0e, { 0x8a, 0x4f, 0xf5, 0x8b, 0xbc, 0xeb, 0xb2, 0xda } }; class human : public IPhysiology, public IPsychics, public IDynamics { public: …… human() { m_ulRef = 0; } HRESULT QueryInterface(const IID& iid, void **ppv) { if (iid == IID_IUnknown || iid == IID_IPhysiology) { *ppv = static_cast<IPhysiology*>(this); (IPhysiology*)(*this))->AddRef(); } else if (iid == IID_IPsychics) { *ppv = static_cast<IPsychics*>(this); (IPsychics*)(*this))->AddRef(); } else if (iid == IID_IDynamics) { *ppv = static_cast<IDynamics*>(this); (IDynamics*)(*this))->AddRef(); } else { *ppv = NULL; return E_NOTINTERFACE; } return S_OK; } ULONG AddRef() { return ++m_ulRef; } ULONG Release() { m_ulRef--; if (m_ulRef <= 0) { m_ulRef = 0; delete this; } return m_ulRef; } ULONG m_ulRef; };这样我们的组件对象就定义完全了。
#include "olectl.h" import "oaidl.idl"; import "ocidl.idl"; [ object, uuid(6AAF876E-FCED-4ee0-B5D3-63CD6E2242F5), nonextensible, helpstring("IPhysiology 接口"), pointer_default(unique) ] interface IPhysiology : IUnknown { void eat(Food in); void drink(Liquid in); Somethings toilet(); }; [ object, uuid(5F144D5C-A20C-42e7-8F91-4D5CAE430B29), nonextensible, helpstring("IPsychics 接口"), pointer_default(unique) ] interface IPsychics : IUnknown { Sound laugh(); Sound cry(); Sound angry(); }; [ object, uuid(5F144D5C-A20C-42e7-8F91-4D5CAE430B29), nonextensible, helpstring("IDynamics 接口"), pointer_default(unique) ] interface IDynamics : IUnknown { Speed run() = 0; Speed walk() = 0; Interval jump() = 0; }; [ uuid(6CC7B329-B92F-4A8F-9CDD-1AB6D7E4CF4D), version(1.0), helpstring("OLEOBJECT 1.0 类型库") ] library OLEOBJECTLib { importlib("stdole2.tlb"); [ uuid(62FD0E39-DA84-4B19-BAB0-960A27AC2B71), helpstring("OlePaint Class") ] coclass OlePaint { [default] interface IPhysiology, interface IPsychics, interface IDynamics }; };
4) COM对象的接口原则
为了规范COM的接口机制,微软向COM开发者发布了COM对象的接口原则。
(1)IUnknown接口的等价性
当我们要等到两个接口指针,我如何判断它们从属于一个对象呢。COM接口原则规定,同一个对象的Queryinterface的IID_IUnknown查询出来的IUnknown指针值应当相等。也就是说,每个对象的IUnknown指是唯一的。我们可以通过判断IUnknown指针是否相等来判断它们是否指向同一个对象。
IUnknown *pUnknown1 = NULL, *pUnknown2 = NULL; pObjectA->QueryInterface(IID_IUnknown,(void **) &pUnknown1); pObjectB->QueryInterface(IID_IUnknown,(void **) &pUnknown2); if (pUnknown1 == pUnknown2) { cout << “I am sure ObjectA is ObjectB.”; } else { cout << “I am sure ObjectA is not ObjectB.”; }当然,如果查询的不是IUnknown接口,则无此限制。同一对象对非IUnknown接口的查询值可以不同。
IPsychics *pSrcPsychics = …something, *pTarget = NULL; IDynamics *pDynamics = NULL;如果pSrcPsychics->QueryInterface(IID_IDynamics,(void **) &pDynamics);成功的话。
嗯,COM的基本知识好像这么多了。好像片篇太长呵。那么COM实现方法留到下一篇吧。