作者:Michael Dunn(codeproject.com)
译者:蒋国纲
本文目的
同
我写的上一篇《COM入门第一部分》一样,本文也是为初学COM的程序员准备的,帮助他们来弄懂一些COM基础。本文覆盖了COM
Server部分,解释如何逐步编写COM接口和COM Server,也提及到当被COM运行库调用的时候COM Server内部的详细过程。
本文假定你精通C++,并懂得《COM入门第一部分》所提及到的术语,章节安排如下:
COM Server快览 - 讲述COM Server的要素;
COM Server生存时间管理 - 讲述COM Server如何来控制它自己被加载多长时间;
接口实现,从IUnknown开始 - 向你展示如何用C++来实现一个接口,并解释IUnknown方法;
CoCreateInstance()内幕 - 关于调用CoCreateInstance()的概览;
COM Server注册 - 讲述完整地注册一个COM Server需要哪些要素;
建立COM对象 - 类工厂 - 讲述为你的Client程序建立COM对象的过程;
例子 - 举例说明上面所讲的内容;
Client - 简单的可以用来测试Server的Client;
其它 - 源码及调试中的注意事项。
COM Server快览
本文中,我们先来学习一种最简单的COM Server,进程内Server(in-process server),“进程内”意味着Server被加载到Client程序的进程空间,进程内Server总是DLL的形式,并且要和Client在同一电脑上。
进程内Server在它能正常使用前必须符合两种规范:
1、它必须已经被注册到注册表的“HKEY_CLASSES_ROOT\CLSID”键下;
2、它必须导出了一个叫“DllGetClassObject()”的函数。
这是你想要一个进程内Server正常工作的最低要求,在注册表的“HKEY_CLASSES_ROOT\CLSID”下面必须有个这个Server的GUID键值,这个键还需要包括这个Server(DLL文件的形式)的具体位置和它的线程模式;DllGetClassObject()函数是被COM运行库的CoCreateInstance()这个API来调用的。
通常也会同时导出以下三个函数:
DllCanUnloadNow(): 让COM运行库检查Server是否已经从内存中移除;
DllRegisterServer(): 被类似RegSvr32这种工具调用,来让Server注册自己;
DllUnregisterServer(): 反注册自己,DllRegisterServer()的逆操作。
当然,光导出正确的函数还是不够的,我们得遵循COM的规格来编写COM,这样COM运行库和Client才能正常使用它们。
Server生存时间管理
DLL Server有点不寻常的特征是它们控制他们驻留多长时间,而普通的DLL则是被动地被它的调用者加载到内存和从内存中卸载,其实,纯技术角度来说,DLL Server也是被动的,本质上它们也是DLL,但COM运行库给它们提供了“自动”的机制,来自动加载和卸载它们,这点通过导出函数DllCanUnloadNow()来实现,这个函数原形如下:
HRESULT DllCanUnloadNow();
当
Client调用COM API CoFreeUnusedLibraries()的时候(通常在系统空闲的时候调用),COM运行库通过调用COM
Server的DllCanUnloadNow()来检查所有这个COM
Server的引用,如果这个Server需要保持加载,函数就返回S_FALSE,否则说明这个Server不需要再加载,它返回S_OK,COM运行
库就从内存中卸载它。
一个Server判定是否可以被卸载,用种简单的引用计数就可以了,DllCanUnloadNow()的实现通常如下:
extern UINT g_uDllRefCount; // server's reference count
HRESULT DllCanUnloadNow()
{
return (g_uDllRefCount > 0) ? S_FALSE : S_OK;
}
下章节我将来讲述如何维护这个引用计数,同时我们也会有些范例。
接口实现,从IUnknown开始
所
有接口继承于IUnknown,因为IUnknown包括两个COM对象引用计数的基本功能,当你写一个CoClass,你就得实现IUnknown,我
们来看例子,可能是你能写得出的最简单的CoClass例子,我们在一个叫CUnknownImpl的类中实现了IUnknown,类声明如下:
class CUnknownImpl : public IUnknown
{
public:
// Construction and destruction
CUnknownImpl();
virtual ~CUnknownImpl();
// IUnknown methods
ULONG AddRef();
ULONG Release();
HRESULT QueryInterface( REFIID riid, void** ppv );
protected:
UINT m_uRefCount; // object's reference count
};
构造与析构函数
构造与析构函数管理着Server的引用计数:
CUnknownImpl::CUnknownImpl()
{
m_uRefCount = 0;
g_uDllRefCount++;
}
CUnknownImpl::~CUnknownImpl()
{
g_uDllRefCount--;
}
当COM对象创建的时候,构造函数被调用,Server的引用计数增加,以此保证Server驻留内存,它同时也将COM对象的引用计数置为1,反之当COM对象销毁的时候,析构函数减少Server的引用计数。
AddRef()与Release()
这两个方法控制COM对象的生存时间,AddRef()很简单:
ULONG CUnknownImpl::AddRef()
{
return ++m_uRefCount;
}
它简单地增加了对象的引用计数,并返回更新了的引用计数值。
Release()就相对有点琐碎:
ULONG CUnknownImpl::Release()
{
ULONG uRet = --m_uRefCount;
if ( 0 == m_uRefCount ) // releasing last reference
delete this;
return uRet;
}
它
除了减少对象的引用计数,还在引用计数为0的时候销毁这个对象,Release()也返回更新了的引用计数,注意Release()的实现假定了COM对
象被建立在堆中(译者:局部变量),如果你是在栈中(译者:用new创建)或全局中创建它,那在delete这个对象的时候就会出错。
现在就很清楚了,为什么在你的程序中恰当地调用AddRef()和Release()是很重要的。如果你不正确地调用它们,COM对象就有可能提前被销毁,或者根本不被销毁。如果COM对象被过早销毁,而你的应用程序却企图访问它,就会出现内存非法访问导致程序崩溃。
如果你做过多线程编程,你可能对使用“++”和“--”运算符的“线程安全性”感到担忧,你用InterlockedIncrement()和InterlockedDecrement()来取代它,其实,“++”和“--”运
算符在单线程Server中非常安全,因为,尽管我们的Client程序是多线程的,调用COM的方法也是不同的线程来调用,但COM运行库最终会把这些
调用串行化,也就是说我们一旦调用了一个方法,在这个调用完成之前,其它线程企图对这个方法的调用就会被堵塞,直到第一个方法调用返回,COM运行库确保
了我们的Server不会同时被一个以上的线程进入。
QueryInterface()
QueryInterface(),或者简称为QI(),是给Client用来从COM对象获取不同接口的,因为我们的CoClass例子只实现了一个接口,我们的QI()也将会很简单,QI()带两个参数:接口的IID和要获取的接口的指针。
HRESULT CUnknownImpl::QueryInterface ( REFIID riid, void** ppv )
{
HRESULT hrRet = S_OK;
// Standard QI() initialization - set *ppv to NULL.
*ppv = NULL;
// If the client is requesting an interface we support, set *ppv.
if ( IsEqualIID ( riid, IID_IUnknown ))
{
*ppv = (IUnknown*) this;
}
else
{
// We don't support the interface the client is asking for.
hrRet = E_NOINTERFACE;
}
// If we're returning an interface pointer, AddRef() it.
if ( S_OK == hrRet )
{
((IUnknown*) *ppv)->AddRef();
}
return hrRet;
}
QI()中做了三件不同的事情:
1、初始化参数传进来的指针为NULL;
2、检查参数riid,看是否我们的CoClass实现了Client所需要的接口;
3、如果我们真的实现了这个所要的接口,就增加COM Object的引用计数。
这里特别注意下AddRef(),很重要:
*ppv = (IUnknown*) this;
这
行代码功能是对COM对象建立起了新的引用,所以我们必须调用AddRef()来告诉这个对象有了新的引用,将AddRef()的调用返回转换为
IUnknown*看起来有些怪异,其实并不是所有的*ppv都是IUnknown*,所以这个转换还是有必要的,是个好习惯。
现在,我们来讨论DLL Server的一些内部细节,让我们再回头看看我们的Server是怎样处理一个Client的CoCreateInstance()调用的。
CoCreateInstance()内幕
回
到我写得前一篇文章《COM介绍第一部分》,我们可以看到CoCreateInstance()这个API,当Client需要的时候,它来建立一个
COM对象,对Client而言,它是个黑盒,只要用正确的参数调一下CoCreateInstance(),你就得到个COM对象,当然这里面其实并没
有什么魔术,它执行了一个预先定义好的过程,将COM对象建立起来,并返回这个对象的接口。
这里有个这个过程的概览,这里可能有些不熟悉的术语,但不用担心,我会在下面的章节中讲述它们。
1、Client程序调用CoCreateInstance(),传递要取得的CoClass的CLSID和接口的IID;
2、COM运行库在注册表“HKEY_CLASSES_ROOT\CLSID”键下查找要取得的Server的CLSID,这个键保存着这个Server的注册信息;
3、COM运行库读取Server的DLL的全路径并将其加载入Client的进程空间;
4、COM运行库调用Server的DllGetClassObject()函数,向类工厂请求要获取CoClass;
5、Server建立所需要的类工厂,并从DllGetClassObject()返回;
6、COM运行库调用类工厂的CreateInstance()方法来创建Client要获取的COM对象。
7、CoCreateInstance()返回接口指针到Client。
COM Server的注册
在正常工作前,一个COM Server必须正确地在Windows注册表中注册,如果你打开注册表的“HKEY_CLASSES_ROOT\CLSID”,你就会发现巨量的子键,HKCR\CLSID保存着本机所有可用的COM Server的列表,当一个COM Server被注册(通常是使用DllRegisterServer()),它就建立一个CLSID下的键,键名就是Server的GUID,例如:
{067DF822-EAB6-11cf-B56E-00A0244D5087}
花括号和连字号都是必须的,字母可以大写也可以小写。
默认键值是一个人工可读的CoClass的名字,以便在一些OLE/COM对象查看工具中显示出来。
在GUID键的子键下面有更多的信息,你要创建的子键很大程度上取决于你有怎样的COM Server,还有怎样使用它,对于简单的进程内Server来说,我们只需要一个子键:InProcServer32。
这个InProcServer32的键包括了两个字符串:“(默认)”--这个Server DLL的全路径;“ThreadingModel”--线程模式,线程模式超出了本文范围,但对于单线程的Server来说,这个模型是“Apartment”。
建立COM对象 - 类工厂
我们回过头来看COM的Client,我讲过关于COM如何具有其语言无关性来创建和销毁COM对象的,Client调用CoCreateInstance()来创建一个COM对象,现在我们来看它如何在Server端工作。
每次你实现一个CoClass,你也得写一个对应的用来创建前面这个CoClass实例的CoClass,这个对应的CoClass就叫“类工厂”,使用“类工厂”的理由就是语言无关性,COM本身并不创建COM对象,因为这样就不是语言无关了。
当Client要创建一个COM对象,COM运行库就从COM Server请求类工厂,类工厂建立起COM对象并返回给Client,这种通信机制通过导出函数DllGetClassObject()实现。
这里岔开讲一下,类工厂和类对象这两个术语通常指的都是同样的东西,但其实它们并不准确描述了它们的作用,因为工厂建立的是对象而不是类,所以将它称为“对象工厂”更为合理,(事实上MFC就这么干了,它的类工厂实现称作COleObjectFactory)但官方术语就叫作类工厂,那我们就依照它吧。
当
COM运行库调用DllGetClassObject(),它传递CLSID进去,Server根据这个CLSID创建类工厂并返回,类工厂自己就是个
CoClass并实现了IClassFactory接口,如果DllGetClassObject()成功了,它就返回IClassFactory指针给
COM运行库,COM运行库使用IClassFactory的方法来创建COM对象实例,最后返回到Client。
IClassFactory接口如下:
struct IClassFactory : public IUnknown
{
HRESULT CreateInstance( IUnknown* pUnkOuter, REFIID riid,
void** ppvObject );
HRESULT LockServer( BOOL fLock );
};
CreateInstance()是创建新COM对象的方法,LockServer()让COM运行库增加或减少Server的引用计数。
例子
这有个类工厂的例子,我们来看这篇文章的范例工程,它是个在CSimpleMsgBoxImpl CoClass中实现了一个ISimpleMsgBox接口的DLL Server。
接口定义
我
们新的接口叫作ISimpleMsgBox,和其它接口一样,它必须继承于IUnknown,这里只有一个方法DoSimpleMsgBox(),注意它
返回的是标准类型HRESULT,所有你写的方法都应该返回这个标准类型HRESULT,其它你需要返回的数据都应该通过指针参数。
struct ISimpleMsgBox : public IUnknown
{
// IUnknown methods
ULONG AddRef();
ULONG Release();
HRESULT QueryInterface( REFIID riid, void** ppv );
// ISimpleMsgBox methods
HRESULT DoSimpleMsgBox( HWND hwndParent, BSTR bsMessageText );
};
struct __declspec(uuid("{7D51904D-1645-4a8c-BDE0-0F4A44FC38C4}"))
ISimpleMsgBox;
(__declspec这行将一个GUID分配给ISimpleMsgBox符号,之后这个GUID就能被__uuidof运算符获取,__declspec和__uuidof都是Microsoft C++扩展)
第二个参数DoSimpleMsgBox()是BSTR类型,BSTR代表“Binary String”--COM中一个定长直接串,BSTR主要是给Visual Basic和Windows Scripting Host使用的。
这个接口是通过一个叫CSimpleMsgBoxImpl的C++类来实现的,它定义是:
class CSimpleMsgBoxImpl : public ISimpleMsgBox
{
public:
CSimpleMsgBoxImpl();
virtual ~CSimpleMsgBoxImpl();
// IUnknown methods
ULONG AddRef();
ULONG Release();
HRESULT QueryInterface( REFIID riid, void** ppv );
// ISimpleMsgBox methods
HRESULT DoSimpleMsgBox( HWND hwndParent, BSTR bsMessageText );
protected:
ULONG m_uRefCount;
};
class __declspec(uuid("{7D51904E-1645-4a8c-BDE0-0F4A44FC38C4}"))
CSimpleMsgBoxImpl;
当一个Client要建立一个SimpleMsgBox COM对象,它应该这样来做:
ISimpleMsgBox* pIMsgBox;
HRESULT hr;
hr = CoCreateInstance( __uuidof(CSimpleMsgBoxImpl), // CLSID of the coclass
NULL, // no aggregation
CLSCTX_INPROC_SERVER, // the server is in-proc
__uuidof(ISimpleMsgBox), // IID of the interface
// we want
(void**) &pIMsgBox ); // address of our
// interface pointer
类工厂
我们的类工厂实现
我们的SimpleMsgBox类工厂是用C++类来实现的:
class CSimpleMsgBoxClassFactory : public IClassFactory
{
public:
CSimpleMsgBoxClassFactory();
virtual ~CSimpleMsgBoxClassFactory();
// IUnknown methods
ULONG AddRef();
ULONG Release();
HRESULT QueryInterface( REFIID riid, void** ppv );
// IClassFactory methods
HRESULT CreateInstance( IUnknown* pUnkOuter, REFIID riid, void** ppv );
HRESULT LockServer( BOOL fLock );
protected:
ULONG m_uRefCount;
};
构造函数,析构函数,IUnknown方法都像前面的例子那样实现,唯一新鲜的玩意儿就是IClassFactory方法,LockServer()大致和你想的那样:
HRESULT CSimpleMsgBoxClassFactory::LockServer ( BOOL fLock )
{
fLock ? g_uDllLockCount++ : g_uDllLockCount--;
return S_OK;
}
现在我们来看有趣的部分,调用CreateInstance()这个方法就能返回新建的CSimpleMsgBoxImpl对象,我们来看它的原形和参数:
HRESULT CSimpleMsgBoxClassFactory::CreateInstance ( IUnknown* pUnkOuter,
REFIID riid,
void** ppv );
pUnkOuter是为聚合新对象准备的,指向一个外部COM对象,聚合超出了本文讨论范围,我们的例子不支持聚合。
riid和ppv跟使用QueryInterface()中的一样,它们是Client请求的接口的IID和要返回的接口指针。
这里有个CreateInstance()的实现,它从一些数据校验和初始化开始。
HRESULT CSimpleMsgBoxClassFactory::CreateInstance ( IUnknown* pUnkOuter,
REFIID riid,
void** ppv )
{
// 我们不支持“聚合”,因此pUnkOuter必须是NULL
if ( NULL != pUnkOuter )
return CLASS_E_NOAGGREGATION;
// 检查ppv是否真的指向一个void*
if ( IsBadWritePtr ( ppv, sizeof(void*) ))
return E_POINTER;
*ppv = NULL;
// 我们检查了这些参数是合法的,所以我们能创建新对象
CSimpleMsgBoxImpl* pMsgbox;
// 创建一个新的COM对象
pMsgbox = new CSimpleMsgBoxImpl;
if ( NULL == pMsgbox )
return E_OUTOFMEMORY;
// 最后,我们QI()新的对象的接口,如果QI()失败,那么对象就没有用途了,我们删除它
HRESULT hrRet;
// 用QI()取得客户端请求的接口
hrRet = pMsgbox->QueryInterface ( riid, ppv );
// 如果QI()失败,将COM对象删除,因为客户端不再需要它
if ( FAILED(hrRet) )
delete pMsgbox;
return hrRet;
}
DllGetClassObject()
让我们仔细看看DllGetClassObject(),它的原形是:
HRESULT DllGetClassObject ( REFCLSID rclsid, REFIID riid, void** ppv );
rclsid是Client要求的CoClass的CLSID,这个函数必须返回这个CoClass的类工厂。
riid和ppv也和QI()函数的参数类似,在这里,riid是COM运行库要获取的接口的IID,通常是IID_IClassFactory。
因为DllGetClassObject()建立一个新的COM对象(类工厂),代码看起来非常像IClassFactory::CreateInstance(),开头是一些校验和初始化:
HRESULT DllGetClassObject ( REFCLSID rclsid, REFIID riid, void** ppv )
{
// Check that the client is asking for the CSimpleMsgBoxImpl factory.
if ( !InlineIsEqualGUID ( rclsid, __uuidof(CSimpleMsgBoxImpl) ))
return CLASS_E_CLASSNOTAVAILABLE;
// Check that ppv really points to a void*.
if ( IsBadWritePtr ( ppv, sizeof(void*) ))
return E_POINTER;
*ppv = NULL;
//第一部分是检查参数,我们的Server只包含了一个CoClass,所以rclsid肯定是CSimpleMsgBoxImpl类的CLSID,
//__uuidof运算符返回已经用__declspec(uuid())分配给CSimpleMsgBoxImpl的GUID,
//InlineIsEqualGUID是个检查两个GUID是否相等的内联函数。
//下一步就是建立一个类工厂对象了
CSimpleMsgBoxClassFactory* pFactory;
// Construct a new class factory object.
pFactory = new CSimpleMsgBoxClassFactory;
if ( NULL == pFactory )
return E_OUTOFMEMORY;
//这里跟CreateInstance()稍有点不同了,回到CreateInstance(),我们只调用了下QI(),如果它失败,
//我们删除COM对象,这里则有点不太一样,我们把自己当作刚创建的COM对象的Client,
//我们调用AddRef()来增加它的引用计数,然后我们调用QI(),如果QI成功,它又调了AddRef()一次,
//使得引用计数变成了2,如果QI()失败,那么引用计数增加1。QI()调用后,类工厂对象的使用就算结束了,
//所以我们调用Release(),如果QI()失败,对象将删除它自己(因为引用计数是0),所以结果还是一样的。
// AddRef() the factory since we're using it.
pFactory->AddRef();
HRESULT hrRet;
// QI() the factory for the interface the client wants.
hrRet = pFactory->QueryInterface ( riid, ppv );
// We're done with the factory, so Release() it.
pFactory->Release();
return hrRet;
}
QueryInterface()故地重游
我展示过一个简单的QI()实现,但研究类工厂的QI()更具价值,因为它是个真实的例子,在这个COM对象实现中除了有IUnknown还有更多的内容。首先我们创建ppv缓存,并初始化它。
HRESULT CSimpleMsgBoxClassFactory::QueryInterface( REFIID riid, void** ppv )
{
HRESULT hrRet = S_OK;
// Check that ppv really points to a void*.
if ( IsBadWritePtr ( ppv, sizeof(void*) ))
return E_POINTER;
// Standard QI initialization - set *ppv to NULL.
*ppv = NULL;
//接下来我们来看riid,看他是否属于类工厂的实现:IUnknown或者IClassFactory
if ( InlineIsEqualGUID ( riid, IID_IUnknown ))
{
*ppv = (IUnknown*) this;
}
else if ( InlineIsEqualGUID ( riid, IID_IClassFactory ))
{
*ppv = (IClassFactory*) this;
}
else
{
hrRet = E_NOINTERFACE;
}
//最后,如果riid是个支持的接口,我们通过接口指针调用AddRef()并返回
// If we're returning an interface pointer, AddRef() it
if ( S_OK == hrRet )
{
((IUnknown*) *ppv)->AddRef();
}
return hrRet;
}
ISimpleMsgBox的实现
最后,但不是最终我们得为ISimpleMsgBox的唯一的方法来写些代码,DoSimpleMsgBox(),我在这里用Microsoft的扩展类_bstr_t来把bsMessageText转换成TCHAR串。
HRESULT CSimpleMsgBoxImpl::DoSimpleMsgBox ( HWND hwndParent,
BSTR bsMessageText )
{
_bstr_t bsMsg = bsMessageText;
LPCTSTR szMsg = (TCHAR*) bsMsg; // Use _bstr_t to convert the
// string to ANSI if necessary.
//转换后,我们显示消息框,并返回
MessageBox ( hwndParent, szMsg, _T("Simple Message Box"), MB_OK );
return S_OK;
}
Client
现在我们算完成了这个“出色的”COM
Server,我们如何来使用它?我们的接口是个定制的接口,这意味着它只能被C或者C++Client所使用,(如果我们的CoClass实现了
IDispatch,我们就能用各种工具写它的Client了,Visual Basic、Windows Scripting Host、a web
page和PerlScript等等,但这些不在本文讨论)我已经写好了一个简单的应用程序来使用ISimpleMsgBox。
此程序是个应用程序向导创建的Win32 Hello World程序,菜单中包含两个命令来测试这个Server:
“Test MsgBox COM Server”命令建立一个CSimpleMsgBoxImpl对象,并调用它的DoSimpleMsgBox()方法,因为这是个简单的方法,代码并不很长,我们一开始用CoCreateInstance()创建了个COM对象。
void DoMsgBoxTest(HWND hMainWnd)
{
ISimpleMsgBox* pIMsgBox;
HRESULT hr;
hr = CoCreateInstance ( __uuidof(CSimpleMsgBoxImpl), // CLSID of coclass
NULL, // no aggregation
CLSCTX_INPROC_SERVER, // use only in-proc
// servers
__uuidof(ISimpleMsgBox), // IID of the interface
// we want
(void**) &pIMsgBox ); // buffer to hold the
// interface pointer
if ( FAILED(hr) )
return;
//接下来我们调用DoSimpleMsgBox()并释放我们的接口
pIMsgBox->DoSimpleMsgBox ( hMainWnd, _bstr_t("Hello COM!") );
pIMsgBox->Release();
}
这就是全部代码。
另一个菜单命令“CoFreeUnusedLibraries”则调用CoFreeUnusedLibraries() API,所以你能看到Server的DllCanUnloadNow()函数会使用到。
其它
COM宏
在COM代码中有好几个宏,它们隐藏了一些实现上的细节,这些宏能在C和C++中使用,我在本文中没有使用这些宏,但范例工程中有使用,所以你得明白它们的含义,这里是ISimpleMsgBox的声明:
struct ISimpleMsgBox : public IUnknown
{
// IUnknown methods
STDMETHOD_(ULONG, AddRef)() PURE;
STDMETHOD_(ULONG, Release)() PURE;
STDMETHOD(QueryInterface)(REFIID riid, void** ppv) PURE;
// ISimpleMsgBox methods
STDMETHOD(DoSimpleMsgBox)(HWND hwndParent, BSTR bsMessageText) PURE;
};
STDMETHOD()
includes the virtual keyword, a return type of HRESULT, and the
__stdcall calling convention. STDMETHOD_() is the same, except you can
specify a different return type. PURE expands to "=0" in C++ to make
the function a pure virtual function.
STDMETHOD()包含了virtual关键字,返回HRESULT类型,使用__stdcall调用协定,STDMETHOD_()也是一样的,除非你指定了一个不同的返回值,PURE则扩展成“=0”来表示C++中的存虚函数。
STDMETHOD()和STDMETHOD_()在实现中也有对应的宏定义--STDMETHODIMP和STDMETHODIMP_(),例如,这里有个DoSimpleMsgBox()的实现:
STDMETHODIMP CSimpleMsgBoxImpl::DoSimpleMsgBox ( HWND hwndParent,
BSTR bsMessageText )
{
//...
}
最后,标准导出函数用STDAPI宏来声明,如:
STDAPI DllRegisterServer();
STDAPI包括了返回类型和调用协定,你使用STDAPI不利的一面就是你不能再使用__declspec(dllexport),你不得不用.DEF文件来替代它。
Server的注册和反注册
Server
实现了我前面提及的DllRegisterServer()和DllUnregisterServer(),它们的功能就是建立和删除注册表相关项目来告
诉COM我们的Server的信息,其中代码尽是烦人的注册表操作,我就不在这里重复了,但这里有个用DllRegisterServer()创建的注册
表项目列表:
HKEY_CLASSES_ROOT\CLSID\{7D51904E-1645-4a8c-BDE0-0F4A44FC38C4}
Default="SimpleMsgBox class"
HKEY_CLASSES_ROOT\CLSID\{7D51904E-1645-4a8c-BDE0-0F4A44FC38C4}\InProcServer32
Default=[path to DLL]; ThreadingModel="Apartment"
(译者注:由于本译文没有附带源代码,下面关于源代码的说明也略作改动)
代码范例的注意事项
如前面所提及,你需要一个.def文件来从Server导出你的标准导出函数,.def通常如下:
EXPORTS
DllRegisterServer PRIVATE
DllUnregisterServer PRIVATE
DllGetClassObject PRIVATE
DllCanUnloadNow PRIVATE
每一行包含函数名和PRIVATE关键字,这个关键字意思是函数将导出,但不包含在导入库,这意味着Client不能直接从代码上调用这些函数,即使Clientlink了导入库,这是个必须的步骤,如果你漏了PRIVATE这个关键字,编译器就会报错。
在Server端设置断点
如果你需要在Server端(DLL)设置断点以便调试,那你有两种办法:
第
一种办法是把Server端所属的工程设置为当前活动工程,然后开始调试,如果VC向你询问可执行文件的位置,你就输入测试Client的全路径,当然之
前你先要生成Client;另一种办法是把Client所属工程设置为当前活动工程,并配置工程的依赖关系,使得Client要依赖于Server,这样
的话,当你改动Server代码并编译时,Client就会自动重新编译,最后,当你开始调试Client的时候,你得告诉VC加载Server的符号:
打开Client的工程设置,在Debug标签下Category中选择“Additional DLLs”,单击下面的列表框增加一个条目,指明你要调试的Server(DLL文件)的位置。
(第二部分完)