第2章 COM基础: 创建一个简单的组件和客户端程序
2.1 创建步骤
1) 创建一个传统的C++ Win32DLL 和客户端程序(不是COM);
2) 增加IUnknown接口;
3) 实现QueryInterface();
4) 管理引用计数;
5) 创建类工厂;
6) 使用COM API.
2.2 传统的StopWatch实现
2.2.1 最初的StopWatch的设计
2.2.2 建立Timers.dll库
1. 创建一个Win32DLL项目
2. 编写类的代码
1) 从Insert(插入)菜单中,选择 New Class
2) 类名键入CStopWatch, 然后点击OK
[要点] COM数据类型HRESULT, 它是LONG型的typedef, 如这里所示
typedef LONG HRESULT
3. 添加计时代码
4. 定义输出函数(添加Timers.cpp)
1) DllMain(): 在DLL被装载和再次被装载时调用,无论什么时候使用DLL的线程被终止, 或者调用LoadLibrary() 或 FreeLibarary()时, DLLMain()就被调用
extern "C"
BOOL WINAPI DllMain(HINSTANCE hInstance, DWORD dwReason, LPVOID /*lpReserved*/)
{
Return TRUE
}
DllMain()函数的实现是可选的。
2) DllGetClassObject(): 这个函数由COM定义, 但由自己实现. 目前使用这个函数创建一个指定类的对象, 并返回这个对象上的接口指针.
extern "C"
HRESULT __stdcall DllGetClassObject(
REFCLSID rclsid,
REFIID riid,
LPVOID* ppv)
{
*ppv = static_cast<IStopWatch*>(new CStopWatch);
return S_OK;
}
[问题]这里为什么不返回类的对象的指针.
5. 创建Timers.def
2.3 创建StopWatchClient.exe
创建一个Win32控制台应用程序项目
#include <iostream>
#include "../StopWatch.h"
#include "../Timers_i.c"
#define TIMERSDLL "..//Debug//Timers.dll"
typedef HRESULT (__stdcall *DLLGETCLASSOBJECT) (REFCLSID rc_sid,
REFIID riid, LPVOID* ppv);
// 声明typedef便于通过指针调用这个函数,它也提供一个可以改变函数声明的位置
// Instantiates a stopwatch object and returns it by reference
HRESULT CreateInstance(REFCLSID rclsid,REFIID riid,void** ppv)
{
HRESULT hr = E_FAIL;
HINSTANCE hinstDll;
DLLGETCLASSOBJECT DllGetClassObject;
GUID guid;
hinstDll = LoadLibrary(TIMERSDLL);
if ( hinstDll == NULL )
std::cout<<"Unable to load /""TIMERSDLL"/""<<std::endl;
else
{
DllGetClassObject =
(DLLGETCLASSOBJECT) GetProcAddress(hinstDll,
"DllGetClassObject");
if ( DllGetClassObject != NULL )
hr = DllGetClassObject(guid,guid,ppv);
}
return hr;
}
int main(int argc, char* argv[])
{
float nElapsedTime;
HRESULT hr;
IStopWatch* pStopWatch = NULL;
hr = CreateInstance( (void**) & pStopWatch);
if ( !SUCCEEDED(hr) )
std::cout << "ERROR: Unable to create StopWatch!!";
else
{
pStopWatch->Start();
pStopWatch->ElapsedTime(&nElapsedTime);
std::cout << "The overhead time is"<< nElapsedTime << std::endl;
pStopWatch->Release();
pStopWatch = NULL;
}
return 0;
}
2.5 添加IUnknown
[要点] 在接口的虚函数表中头三个函数指针必须是IUnknown接口的方法
接口: 定义和暴露了组件对象的函数. 它是对象提供服务器程序和客户端程序所通过的了解服务器程序性能的机制的描述. 在C++中, 接口使用一个抽象类创建.
组件类: 负责实现COM对象函数, 支持一个或多个接口的类
Struct IUnknown
{
HRESULT __stdcall QueryInterface(
REFIID riid,
void** ppvobject);
unsigned long __stdcall AddRef();
unsigned long __stdcall Release();
}
2.5.1 实现IUnknown的两种方法
1. IUnknown函数重新声明
2. 继承IUnknown
[要点] 接口不支持多继承, 原因是没有一个标准定义了被继承的类在虚函数表中排列的顺序, 而接口的虚函数表要求IUnknown排在首位.
2.5.2 IUnknown必须被完全实现
2.5.3 添加IUnknown
#include <windows.h>
class IStopWatch : public IUnknown
{
public:
// Utility functions
//virtual unsigned long __stdcall Release() = 0;
//IStopWatch specific functions
virtual HRESULT __stdcall Start() = 0;
virtual HRESULT __stdcall ElapsedTime(float *Time) = 0;
};
class CStopWatch : public IStopWatch
{
public:
CStopWatch();
virtual ~CStopWatch();
private:
// The frequency of the counter
// returned by QueryPerformanceCounter()
LARGE_INTEGER m_nFrequency;
// The counter value when the start method was last called
LARGE_INTEGER m_nStartTime;
long m_nReferenceCount;
public:
// Utility functions
HRESULT __stdcall QueryInterface(
REFIID riid,
void** ppvobject);
unsigned long __stdcall AddRef();
unsigned long __stdcall Release();
//IStopWatch specific functions
HRESULT __stdcall Start();
HRESULT __stdcall ElapsedTime(float *Time);
};
2.6 实现QueryInterface()
QueryInterface()实现支持在一个组件对象上的多个接口.
2.6.1 QueryInterface()规则
1) 由一个组件对象支持的一组接口{X}不能在运行时改变;
2) 已知任何一个由组件对象支持的一组接口{X}中的任何接口, 客户端程序可以成功地请求到在{X}中的任何接口;
3) 如果请求IX返回成功,那么再一次在同样的组件对象上请求同样的接口, IX必须要返回相同的指针.
[要点] COM不提供使客户端程序可以请求组件所支持接口列表的方法
QueryInterface函数原型
HRESULT __stdcall QueryInterface(
REFIID riid,
void** ppvobject);
第一个参数riid, 是被请求接口的GUID, 第二个参数ppvObject, 是用于接收返回给客户端程序的接口指针的void** 型引用传递的指针.
[要点] 每个接口需要被指定一个唯一的GUID
2.6.2 生成GUID
Uuidgen.exe Guidgen.exe生成GUID
[要点] 所有的组件对象必须有唯一的CLSID与组件相关联,
2.6.3 添加QueryInterface()
HRESULT __stdcall CStopWatch::QueryInterface(
REFIID riid,
void **ppvObject)
{
HRESULT hr = S_OK;
if (riid == IID_IUnknown)
*ppvObject = static_cast<IUnknown*>(
static_cast<IStopWatch*>(this));
else if (riid == IID_IStopWatch)
*ppvObject = static_cast<IStopWatch*>(this);
else
{
ppvObject = NULL;
hr = E_NOINTERFACE;
}
if (SUCCEEDED(hr))
(static_cast<IUnknown*>(*ppvObject))->AddRef();
return hr;
}
2.6.4 调用QueryInterface()
extern "C"
HRESULT __stdcall DllGetClassObject(
REFCLSID rclsid,
REFIID riid,
LPVOID* ppv)
{
// *ppv = static_cast<IStopWatch*>(new CStopWatch);
// return S_OK;
HRESULT hr;
if (rclsid == CLSID_StopWatch)
{
CStopWatch* stopwatch = new CStopWatch;
hr = stopwatch->QueryInterface(riid,ppv);
}
else
hr = CLASS_E_CLASSNOTAVAILABLE;
return hr;
}
2.7 引用计数
[要点] 引用计数用于控制组件对象的生存期.
unsigned long __stdcall CStopWatch::Release()
{
// delete this;
// return 0;
if( InterlockedDecrement( &m_nReferenceCount ) == 0 )
{
delete this;
return 0;
}
return m_nReferenceCount;
}
unsigned long __stdcall CStopWatch::AddRef()
{
return InterlockedIncrement( &m_nReferenceCount );
}
2.7.2 使用COM引用计数
[要点] 每一次在接口上调用AddRef()必须在同一个接口上调用相匹配的Release().
[要点] 客户端程序负责正确地维护引用计数.
什么时候调用AddRef()和什么时候调用Release()由两个原则:
1. 无论什么时候指派了接口的一个接口指针,应该在那个接口上调用AddRef().
2. 在非空的接口指针出了它的作用域或被重新指派了一个新值时,应该调用Release().
例一 新的接口指针返回到客户端程序的情况。
当检查在客户端程序上引用计数的调用时, 确保将所有的QueryInterface()和CoCreateInstance()的调用与Release()的调用相匹配。
例二 接口指针被复制到一个新位置的情况。
If (pI2 != NULL)
pI2->Release();
pI2 = pI1;
pI2->AddRef();
例三 假定一个非空接口指针出了作用域.
{
IUnknown pIUnk = NULL;
…
If( pIUnk != NULL)
pIUnk->Release();
}
例四 调用函数时,传递了一个接口, 然后这个接口被转换成一个新接口.
HRESULT Refresh( IUnknown* pIUnk)
{
If (pIUnk != NULL)
pIUnk->Release();
…
pIUnk = m_pIUnk;
pIUnk->AddRef();
}
[要点] 专门关于接口生存期的知识可以用来省去对引用计数的调用.
第3章 ATL介绍
3.1 使用ATL应用程序向导创建COM服务器程序
3.1.1 运行ATL应用程序向导
1. 服务器程序类型
(1) 动态链接库:
优点: 它可以和调用它的客户端程序在同一个进程中运行, 调用之间通常不需要上下文的转换
缺点: 不能独立运行
(2) 可执行文件
优点: 可以以独立的模式运行或被另一个应用程序调用.
缺点: 每个函数的调用有一点延迟
(3) 服务(Service)
可执行服务除了与一般的可执行文件有相同的优点和缺点之外,还有另外一点,服务可以在没有任何客户端程序交互的情况下运行。
3. 允许合并代理和存根代码
[要点] 调度是在进程间或机器间传递参数的过程
[要点] 代理是为远程过程调用将参数打包的一段代码
[要点] 存根是在远程过程调用中将参数解包的一段代码
调度一个参数涉及到通过字节流把它输送到本机或另一台计算机上运行的过程。
客户端程序与在客户端程序进程的上下文中运行的代理对话。
代理可以被发布成两种方式。一种是,让它定义到一个独立的DLL中. 第二种方式是合并代理存根到服务器程序的DLL中,以使服务器程序和代理代码可以合并到同一个模块中,如3-4
合并代理存根的优点是管理和发布较少的文件,另外,一个分布式服务器程序的安装和本地服务器程序的安装时一样的,因为文件是一样的。缺点是文件比较大
4. 支持MFC
另外一种添加MFC支持的方法,在stdafx.h文件开头部分包含下列代码
#include <afxwin.h>
#include <afxdisp.h>
5. 支持MTS(微软事务处理服务器程序)
3.1.2 完成向导
[要点] 对象映射指定了由组件服务器程序实现的对象
// TimersATL.cpp : Implementation of DLL Exports.
// Note: Proxy/Stub Information
// To build a separate proxy/stub DLL,
// run nmake -f TimersATLps.mk in the project directory.
#include "stdafx.h"
#include "resource.h"
#include <initguid.h>
#include "TimersATL.h"
#include "TimersATL_i.c"
CComModule _Module;//支持服务器程序的ATL类,类世于CMyApp
BEGIN_OBJECT_MAP(ObjectMap) //对象映射
END_OBJECT_MAP()
/
// DLL Entry Point
extern "C"
BOOL WINAPI DllMain(HINSTANCE hInstance, DWORD dwReason, LPVOID /*lpReserved*/)
{
if (dwReason == DLL_PROCESS_ATTACH)
{
_Module.Init(ObjectMap, hInstance, &LIBID_TIMERSATLLib);//初始化所有数据对象
DisableThreadLibraryCalls(hInstance);//关闭了来自于同一进程不同线程连接到同一个dll实例时的更多通知
}
else if (dwReason == DLL_PROCESS_DETACH)
_Module.Term();
return TRUE; // ok
}
/
// Used to determine whether the DLL can be unloaded by OLE
STDAPI DllCanUnloadNow(void)
{
return (_Module.GetLockCount()==0) ? S_OK : S_FALSE;//判定什么时候卸载DLL是安全的,它返回调用对象的锁定计数
}
/
// Returns a class factory to create an object of the requested type
STDAPI DllGetClassObject(REFCLSID rclsid, REFIID riid, LPVOID* ppv)
{
return _Module.GetClassObject(rclsid, riid, ppv);
}
/
// DllRegisterServer - Adds entries to the system registry
STDAPI DllRegisterServer(void)
{
// registers object, typelib and all interfaces in typelib
return _Module.RegisterServer(TRUE);
}
/
// DllUnregisterServer - Removes entries from the system registry
STDAPI DllUnregisterServer(void)
{
return _Module.UnregisterServer(TRUE);
}
3.2.1 线程模式
[要点]公寓定义了一个逻辑执行的上下文
1. 单线程
在单线程模式中,COM强制所有的调用都来自于在一个主单线程公寓的所有组件对象实例。
对组件类代码的调用将被串行化,在组件中同时仅有一个线程执行,组件的开发者不需要担心同步访问组件的相关资源的问题,因为COM将为程序员控制。
2. 公寓