总结下C++中模块(Dll)对外暴露接口的方式:
(1)导出API函数的方式
这种方式是Windows中调用DLL接口的最基本方式,GDI32.dll, User32.dll都是用这种方式对外暴露系统API的。
优点:
导出函数没有语言限制,什么语言都能调用;
缺点:
这种方式是面向过程的,外部如果要支持多实例等不是很方便,另外它要求的回调函数(callback)只能是普通C函数,C++中我们通常用类静态成员函数,很不方便。
当然,我们通过封装其实也可以让这种方式支持多实例。
通过一个抽象句柄HComponent, 比如:
1.支持导出函数HComponent CreateInstance(); VOID DeleteInstance(HComponent h);
2.然后内部的其他导出函数的第一个参数都是实例句柄,类似INT SendMessage(HComponent h, …), 用这种方式可以模拟出面向对象的效果。
另外如果用动态加载(LoadLibrary, GetProcAddress)的方式调用它的导出函数,即使导出函数内部实现修改了,外部程序也不用重新编译,仍然可用。
导出函数方式一个比较优秀的例子是GDI+的实现,整个GdiPlus.dll对外提供的都是普通导出函数,但是它却可以方便的给面向对象的语言使用,因为一方面它用Handle的方式在DLL内部封装了对象,另一方面它在DLL外围又用C++类的方式封装了头文件直接提供给用户, 所以C++程序可以直接以面向对象的方式调用。
(2)导出类方式
导出类的方式就是把整个C++类对外导出, MFC42.dll就是这种方式。
优点
直接面向对象。
缺点
只能给C++用,而且最好编译器都要一致,另外DLL一变动, 外部程序需要重新编译, 而且外部程序可以通过头文件看到你类的内部实现,所以这种方式是最不建议使用的方式。
(3)COM方式
COM方式实际上导出了几个固定函数(DllGetClassObject, DllCanUnloadNow, DllRegisterServer, DllUnregisterServer), 然后以这几个函数为入口,调用组件内部实现的接口。
优点:
COM方式综合了上面2种方法的所有优点,没有语言限制,面向对象,多实例,只能看到接口,动态升级等。
缺点:
较复杂和对注册表的依赖
当然COM因为其复杂性和对注册表的依赖,很多时候我们在封装模块时不愿意严格按照COM标准来实现,但是我们可以按照COM思想来提供接口。
比如我们可以让我们模块只提供一个导出函数CreateFactory, 然后外部可以调用该接口来创建工厂,最后通过工厂创建出各种类型的对象,这些对象实现了某些接口,外部只需要这些接口的头文件即可调用对象的方法。
现在越来越多的组件以这种方式对外提供接口,比如D2D对外的导出接口就是D2D1CreateFactory, 然后就可以通过该工厂来创建其他的对象,比如pD2DFactory->CreateHwndRenderTarget(…),最后可以直接调用对象实现的接口:pRenderTarget->DrawRectangle(D2D1::RectF(100.f, 100.f, 500.f, 500.f), pBlackBrush);
当然,上面几种DLL对外暴露接口的方式本质上没有区别,都是利用PE文件的导出节来导出数据和函数,但是根据它们使用方式的不同,对外部模块来说还是有很大的区别,推荐次序依次是:COM方式->导出API函数方式->导出类方式。
深入CoCreateInstance():
- 客户端程序调用CoCreateInstance(),传递组件对象类的CLSID以及所要接口的IID。
- COM库在HKEY_CLASSES_ROOT\CLSID.键值下查找服务器的CLSID键值,这个键值包含服务器的注册信息。
- COM库读取服务器DLL的全路径并将DLL加载到客户端的进程空间。
- COM库调用在服务器中DllGetClassObject()函数为所请求的组件对象类请求类工厂。
- 服务器创建一个类工厂并将它从DllGetClassObject()返回。
- COM库在类工厂中调用CreateInstance()方法创建客户端程序请求的COM对象。
- CreateInstance()返回一个接口指针到客户端程序。
步骤
1.创建一个COM接口
.h文件:
1 | typedef struct Interface |
COM定义的每一个接口都必须从IUnknown继承过来,其原因在于IUnknown接口提供了两个非常重要的特性:生存期控制和接口查询。客户程序只能通过接口与COM对象进行通信,虽然客户程序可以不管对象内部的实现细节,但它要控制对象的存在与否。计算机与计算机或计算机与终端之间的数据传送可以采用串行通讯和并行通讯二种方式。
若不采用上述方式创建一个接口,而是使用idl文件定义接口的话,虽然会生成相关头文件,但可读性很差
2.声明一个C++类来实现该接口
.h文件:
1 | extern UINT g_uDllRefCount; // 服务器的引用计数 |
重点关注QueryInterface和DoSimpleMsgBox部分
.c文件:
1 | //构造器和析构器管理服务器的引用计数: |
在QueryInterface()中做了三件不同的事情:
- 初始化传入的指针为NULL[*ppv = NULL;]。
- 检查riid,确定组件对象类(coclass)实现了客户端所请求接口.[if ( InlineIsEqualGUID( riid, IID_IUnknown ))]
- 如果确实实现勒索请求的接口,则增加COM对象的引用计数。[((IUnknown) *ppv)->AddRef();]AddRef()调用很关键。*ppv = (IUnknown) this;
要创建新的COM对象引用,就必须调用这个函数通知COM对象这个新引用成立。在AddRef()调用中的强制转换IUnknown看起来好像多余,但是在QI()中初始化的\ppv有可能不是IUnknown*类型,所以最好是养成习惯对之进行强行转换。
COM服务器必须在Windows注册表中正确注册以后才能正常工作。如果你看一下注册表中的HKEY_CLASSES_ROOT\CLSID键,就会发现大把大把子键,它们就是在这个计算机上注册的COM服务器。当某个COM服务器注册后(通常是用DllRegisterServer()进行注册),就会以标准的注册表格式在CLSID键下创建一个键,它名字为服务器的GUID。
下面是一个这样的例子:
{067DF822-EAB6-11cf-B56E-00A0244D5087}
注:大括弧和连字符是必不可少的,字母大小写均可。
3.为接口和类指定各自指定一个GUID
使用VC++扩展方法
下面两个在上面的.h文件中定义
1 | struct __declspec(uuid("{7D51904D-1645-4a8c-BDE0-0F4A44FC38C4}")) ISimpleMsgBox; |
有__declspec的一行将一个GUID赋值给ISimpleMsgBox,并且以后可以用__uuidof操作符来获取GUID。这两个东西(__declspec和__uuidof)都是微软的C++的扩展。
UUID 是通用唯一识别码(Universally Unique Identifier)的缩写,是一种软件建构的标准,亦为开放软件基金会组织在分布式计算环境领域的一部分。其目的是让分布式系统中的所有元素都能有唯一的辨识信息,而不需要通过中央控制端来做辨识信息的指定。如此一来,每个人都可以创建不与其它人冲突的UUID。在这样的情况下,就不需考虑数据库创建时的名称重复问题。目前最广泛应用的UUID,是微软公司的全局唯一标识符(GUID)。
这样便可以使用__uuidof关键字了
__uuidof使用方法介绍
属于VC++扩展语法
1 | class Demo |
4.创建类厂(必须创建)
每个类必须配有一个类厂,关注CreateInstance 和LockServer 方法. CoCreateInstance方法内部会生命周期依赖这个接口
回过头看一下客户端的COM,它是如何以自己独立于语言的方式创建和销毁COM对象。客户端调用CoCreateInstance()创建新的COM对象。现在我们来看看它在服务器端是如何工作的。
你每次实现组件对象类的时候,都要写一个旁类负责创建第一个组件对象类的实例。这个旁类就叫这个组件对象类的类工厂(class factory),其唯一目的是创建COM对象。之所以要一个类工厂,是因为语言无关的缘故。COM本身并不创建对象,因为它不是独立于语言的也不是独立于实现的。
当某个客户端想要创建一个COM对象时,COM库就从COM服务器请求类工厂。然后类工厂创建COM对象并将它返回客户端。它们的通讯机制由函数DllGetClassObject()来提供。
术语 “类工厂”和“类对象”实际上是一回事。没有那个单词能精确描述类工厂的作用和义,但正是这个工厂创建了COM对象,而不是COM类所为。将“类工厂”理解成“对象工厂”可能会更有助于理解(实际上MFC就是这样理解的——它的类工厂实现就叫做COleObjectFactory)。但“类工厂”是正式术语,所以本文也这样用。
当COM库调用DllGetClassObject()时,它传递客户端请求的CLSID。服务器负责为所请求的CLSID创建者各类工厂并将它返回。类工厂本身就是一个组件对象类,并且实现CSimpleMsgBoxClassFactory接口。如果DllGetClassObject()调用成功,它返回一个CSimpleMsgBoxClassFactory指针给COM库,然后COM库用CSimpleMsgBoxClassFactory接口方法创建客户端所请求的COM对象实例。
.h文件:
1 | struct IClassFactory : public IUnknown |
其中,CreateInstance()是创建COM对象的方法。LockServer()在必要时让COM库增加或减少服务器的引用计数。
我们的类工厂是在一个叫做CSimpleMsgBoxClassFactory的C++类中实现的
.c文件:
1 | // IClassFactory方法 |
5.注册COM组件(手动注册)
万事俱备,现在要将其信息写入注册表,
5.1 实现DllRegisterServer和DllUnregisterServer方法,并将方法声明为STDAPI,表明属于导出函数
1 | //DllRegisterServer()创建告诉COM的注册项。 |
5.2 定义def文件
1 | EXPORTS |
5.3 编译通过后使用regsvr32命令,注册此dll组件
在调用此com组件之前,必须通过cmd命令注册,很重要,DllRegisterServer将会成为入口点!!!
6.编写DllGetClassObject
只有通过全局函数DllGetClassObject,才可以创建类厂,这个方法com类库会去调用,其会根据CLSID返回一个类工厂(一个dll可能会有多个com类)
现在让我们深入DllGetClassObject()内部。它的原型是:
1 | HRESULT DllGetClassObject ( REFCLSID rclsid, REFIID riid, void** ppv ); |
rclsid是客户端所请求的组件对象类的CLSID。这个函数必须返回指定组件对象类的类工厂。
这里的两个参数: riid 和 ppv类似QI()的参数。不过在这个函数中,riid指的是COM库所请求的类工厂接口的IID。通常就是IID_IClassFactory。
因为DllGetClassObject()也创建一个新的COM对象(类工厂),所以代码与IClassFactory::CreateInstance()十分相似。开始也是进行一些有效性检查以及初始化。
1 | STDAPI DllGetClassObject ( REFCLSID rclsid, REFIID riid, void** ppv ) |
第一个if语句检查rclsid参数。我们的服务器只有一个组件对象类,所以rclsid必须是CSimpleMsgBoxImpl类的CLSID。__uuidof操作符获取先前在__declspec(uuid())声明中指定的CsimpleMsgBoxImpl类的GUID
生命周期(非常重要):
CoCreateInstance->CoGetClassObject->DllGetClassObject->IClassFactory->CreateInstance
再谈QueryInterface():
前面讨论过QI()的实现,但还是有必要再看一看类工厂的QI(),因为它是一个很现实的例子,其中COM对象实现的不光是IUnknown。首先进行的是对ppv缓冲的有效性检查以及初始化。
1 | HRESULT CSimpleMsgBoxClassFactory::QueryInterface( REFIID riid, void** ppv ) |
7.调用com组件
当某一客户端想要创建一个ISimpleMsgBox COM对象时,它应该用下面这样的代码:
1 | void DoMsgBoxTest(HWND hMainWnd) |
以上步骤不可以省略,可以看到创建一个com组件是比较麻烦的一个流程,很容易出错或者先创建一个类工厂
1 |
|