DCOM揭秘之五

  理解ATL产生的代码

  我们服务器端DLL的源代码是由ATL产生的。对于许多人来说,可以完全不用了解ATL创建的代码。不过,对于一些喜欢寻根究底的人来说,这是不可以接受的。这里就介绍一下由ATL产生的代码。

  服务端的DLL代码由三种不同类型的文件组成

   首先,是传统的C++源文件和头文件。在开始时,所有这些代码是由ATL向导产生的

   Beep方法是通过使用“AddMethod”对话框加入的,它修改了MIDL接口的定义。MIDL的源代码是一个IDL文件--在这个例子中它是BeepServer.IDL。MIDL编译器将使用该文件来创建几个输出文件。这些文件负责了大部分实现服务器的工作。当我们为COM对象加入方法时,我们也将在IDL加入一些东西。

   第三组的源文件是自动产生的MIDL输出文件,是由MIDL编译器产生的。这些文件是源代码文件,不过由于它们是由MIDL编译器通过IDL源代码自动产生的,因此不能被向导或者开发者直接修改。你可以将它们称为“第二产生文件”--向导创建了一个IDL文件,而MIDL编译器由该IDL文件创建了源代码文件。由MIDL创建的文件包括有:

    BeepServer.RGS - 服务器的注册脚本 


    BeepServer.h - 该文件包括了COM组件的定义 

    BeepServer_i.c - COM组件的GUID结构 

    Proxy/Stub files - 包括了"C"源代码、DLL定义,以及Proxy和Stub的makefile (.mk)

  ATL向导还创建了一个应用的“资源”。如果你查看项目的资源,你将会发现它处在“REGISTRY”下。该资源包含了BeepServer.RGS中定义的注册脚本。资源的名字是IDR_BEEPOBJ。

  我们将在以下的部分看一下这些不同的组件

  主C++模块

  我们在运行ATL COM Appwizard时,我们选择创建一个基于DLL的服务器,并且选择不使用MFC。向导的第一个选择屏幕决定了服务器的整体配置。

  AppWizard创建了一个标准的DLL模块。该类的标准DLL并没有一个WinMain应用循环,不过它有一个DllMain函数用作在载入时初始化该DLL:


CComModule _Module;

BEGIN_OBJECT_MAP(ObjectMap)
OBJECT_ENTRY(CLSID_BeepObj, CBeepObj)
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);
DisableThreadLibraryCalls(hInstance);
}
else if (dwReason == DLL_PROCESS_DETACH)
_Module.Term();
return TRUE; // ok
}


  DllMain函数的真正工作是检查有没有一个客户连上该DLL,然后会做一些初始化的工作。一眼看去,并没有一个明显的指示这是一个COM应用。

  我们新服务器的COM部分被封装到ATL类CComModule中。CComModule是ATL服务器的基类。它包含了所有用作登记和运行服务器、开始和维护COM对象的COM逻辑。CComModule被定义在头文件“altbase.h”中。该代码用以下的行声明一个全局的CComMoudule 对象:

   CComModule _Module;

  这个单一的对象包含了许多用作我们应用的COM服务器功能。它在程序执行开始时的创建和初始化设置了一连串的事件动作。

  ATL需要你的服务器命名它的全局CComModule对象“_Module”。使用你自己的类来覆盖CComModule是可以的,不过你不能改变它的名字。

  如果我们选择创建一个可执行的服务器,或者一个带MFC的DLL,代码将会是完全不同。这时还会有一个基于CComModule的全局对象,不过程序的入口将会是WinMain()。选择一个基于MFC的DLL将会创建一个基于CWinApp的主对象。

  对象映射

  CComModule通过在前面部分看到的对象映射连接到我们的COM对象(CBeepObj)。一个对象映射定义了该服务器控制的所有COM对象的一个数组。对象映射在代码中使用OBJECT_MAP宏定义。以下就是我们DLL的对象映射:


BEGIN_OBJECT_MAP(ObjectMap)
 OBJECT_ENTRY(CLSID_BeepObj, CBeepObj)
END_OBJECT_MAP()


  OBJECT_ENTRY宏通过一个C++类与对象的CLSID关联。一个服务器中包含有多于一个的COM对象是很常见的。这时,对于每个对象,都将会有一个OBJECT_ENTRY。

  输出文件

  我们这个进程内的DLL和大部分的DLL一样,都拥有一个输出文件。输出文件将被客户端用来连接到我们DLL中的外部函数。这些定义都放在BeepServer.def文件中:


; BeepServer.def : Declares the module parameters.

LIBRARY "BeepServer.DLL"

EXPORTS
DllCanUnloadNow @1 PRIVATE
DllGetClassObject @2 PRIVATE
DllRegisterServer @3 PRIVATE
DllUnregisterServer @4 PRIVATE


  重要的是要注意到那些没有被输出的。没有用户的方法,也没有“Beep”方法的输出。以上你只看到一个COM DLL应有的输出。

  查看BeepServer.CPP文件,我们可看到这4个函数的定义是由COM应用的类来处理的。以下就是DllRegisterServer的代码:


// DllRegisterServer - Adds entries to the system registry
STDAPI DllRegisterServer(void)
{
// registers object, typelib and all interfaces in typelib
return _Module.RegisterServer(TRUE);
}



  在这里,DLL仅调用了ATL的CComModule::RegisterServer()方法。CComModule实现服务器注册的方法是兼容进程内、本地和远程COM服务器的。其余4个被输出的DLL函数是完全相同的。真正的实现被隐藏在ATL的模板中。

  以上介绍的大部分代码是DLL的特定代码。只有在你选择创建一个基于DLL的服务器时,才会得到这个配置。主模块的代码都不是COM特定的。该主模块的体系完全是为了在一个DLL中传送COM对象,这部分的代码根据服务器类型的不同,有着明显的区别。服务器内的真正代码则是更为类似的。coclass和接口的实现是一样的,与你创建的服务器类型无关(DLL、EXE、服务器)。如果要将一个DLL服务器的coclass转换为使用一个基于EXE的服务器实现,你不需做多少的改变。


  COM对象--“CBeepObj”

  COM服务器必须实现至少一个COM对象。我们使用一个单一的称为“CBeepObj”的对象。该对象一个最有趣的地方是它的代码完全是由ATL向导产生的。它的对象定义也非常地紧凑,该类的定义放在BeepObj.h中:


// BeepObj.h : Declaration of the CBeepObj
#include "resource.h" // main symbols
//
// CBeepObj
class ATL_NO_VTABLE CBeepObj : 
public CComObjectRootEx
,
public CComCoClass
,
public IBeepObj
{
public:
CBeepObj()
{
}

DECLARE_REGISTRY_RESOURCEID(IDR_BEEPOBJ)

BEGIN_COM_MAP(CBeepObj)
COM_INTERFACE_ENTRY(IBeepObj)
END_COM_MAP()

// IBeepObj
public:
STDMETHOD(Beep)(/*[in]*/ long lDuration);
};


  这个简单的头文件定义了大量的功能,以下部分将予以讲解。

  对象继承

  在这些代码中,你首先留意到的可能就是多重继承了。我们的COM对象拥有三个基类。这些基类是模板类,用来实现我们对象的基本COM功能。每一个的类都定义了一个特定的COM动作。

  CComObjectRootEx< > 和 CComObjectRoot< > 是ATL对象类的根。这些类处理全部的引用计数和COM类管理,包括有三个需要的IUnknown接口的实现,即QueryInterface(), AddRef()和Release()。当我们的CBeepObj对象被服务器创建时,基类将会在整个对象的生存期间跟踪它。

  CComObjectRootEx模板指定了CComSingleThreadModel参数。单线程意味着COM对象不必处理多线程的访问。在该对象的设置中,我们指定为“Apartment threading(独立线程)”。独立线程使用一个windows信息循环来同步COM对象的访问。这个方式是最简单的,因为它消除了多线程的问题。

  CComCoClass< >定义了创建ATL COM对象的类制造器(Class factories)。类制造器是特别的COM类,它是用来创建COM对象的。CComCoClass使用一个默认类型的类制造器,并且允许aggregation(集合)。

  IBeepObj是该服务器实现的接口。一个接口被定义一个C++结构体(你可能会记起C++结构体的使用和类相似,但是只能使用公共的成员)。如果你查看自动产生的BeepServer.h文件,你将会发现MIDL已经创建了一个我们接口的定义。


interface DECLSPEC_UUID(
"36ECA947-5DC5-11D1-BD6F-204C4F4F5020")
IBeepObj : public IUnknown
{
public:
virtual /* [helpstring] */ HRESULT 
STDMETHODCALLTYPE Beep( 
/* [in] */ long lDuration) = 0;
};


  DECLSPEC_UUID宏让编译器将一个GUID和接口名字关联起来。要注意到我们的单一方法“Beep”被定义为一个纯的抽象函数。当CBeepObj被定义时,将必须要提供一个该函数的实现。

  这个类定义的一个特别地方是它拥有ATL_NO_VTABLE属性。该宏是一个优化,它可让对象的初始化更快。

  类定义

  我们的对象使用一个默认的构造器。如果需要,你可以加入特别的初始化动作,不过有一些限制。使用ATL_NO_VTABLE的一个限制是不允许在构造器中调用任何抽象方法。要进行复杂的初始化,可使用FinalConstruct方法(由CComObjectRootEx继承而来)。如果你想使用FinalConstruct,可在类定义中声明它,覆盖ATL的默认。它将会自动地被ATL构架调用。(FinalConstruct通常被用来创建aggregated对象)

  DECLARE_REGISTRY_RESOURCEID()宏用来在系统的注册表中注册COM对象。该宏的参数IDR_BEEPOBJ指向工程的一个资源。这是一个特别类型的资源,它下载MIDL产生的“.rgs”文件。

  BEGIN_COM_MAP是一个宏,它定义了CComObjectRoot< >类将会管理的COM接口的一个数组。这个类有一个接口--IBeepObj。IBeepObj是我们的自定义接口。COM对象实现超过一个接口是很常见的。所有支持的接口都会在这里展示,还包括有顶部类定义中的类继承。

  方法

  最后,我们将谈到方法。作为一个应用的编程者,我们的兴趣主要在这部分的代码。我们的单一Beep()方法由以下行定义:


STDMETHOD(Beep)(/*[in]*/ LONG duration);

STDMETHOD是一个OLE宏,它被转换为:

typedef LONG HRESULT;
#define STDMETHODCALLTYPE __stdcall
#define STDMETHOD(method) virtual HRESULT STDMETHODCALLTYPE method


  我们可以一个更熟悉的C++样式编写该定义,如下:

  virtual long _stdcall Beep(long lDuration);

  我们将可在BeepObj.cpp模块中找到这个方法的代码。由于该COM对象仅有一个方法,因此COM对象的源代码比较少。所有对象的COM逻辑都被定义在ATL的模板类中。我们剩下的工作就是编写真正的应用代码。在你编写真正的应用时,你的所有注意力将应该集中在这个模块上。


STDMETHODIMP Beep(long lDuration)
{
::Beep( 660, lDuration );
return S_OK;
}


  再次,该函数定义转换为一个标准的函数调用

  long _stdcall CBeepObj::Beep( long lDuration )

  API beep程序接受两个参数:发声的频率和以毫秒算的持续时间。如果你使用的是Windows 95。这两个参数都可以忽略,从而得到默认的beep声。范围操作符“::”是重要的,不过很容易被忘掉。如果你漏了,该方法将会调用自己。

  _stdcall标签告诉编译器该对象使用标准的windows调用协定。默认的情况下,C和C++使用__cdecl调用协定。这些可指示编译器参数的放置顺序和堆栈的移除。Win32 COM使用_stdcall协定,其它的操作系统可能使用不同的调用协定。

  注意到我们的Beep()方法返回一个状态S_OK。这并不意味着调用者一直会得到一个成功的返回状态。要记得调用COM方法和标准的C++函数是不一样的。在调用程序(客户)和COM服务器间存在在一个完整的COM层。

  CBeepObj::Beep()方法返回S_OK是完全可能的,不过连接可在一个COM调用的中间失去。虽然该函数将返回S_OK,不过调用的客户将得到某些RPC错误指示出失败。函数的结果必须由COM返回到客户端。

  在这个例子中,COM服务器作为一个进程内的服务器运行。通过DLL的连接是非常紧密的,因此发生传送错误的机会很少。在以后的例子中,当我们的COM服务器运行在一个远程的计算机时,将有明显的不同。网络错误将会是很常见的,因此你需要在设计应用时处理它们。

  服务器注册

  COM子系统使用Windows的注册表来查找和启动所有的COM对象。每个COM服务器负责自己注册,即在注册表中写入相关的项目。便利的是,该任务大部分可由ATL、MIDL和ATL向导自动完成。

  MIDL产生的其中一个文件是一个注册脚本。该脚本包含了成功操作我们服务器的全部定义。以下就是产生的脚本:


HKCR
{
BeepObj.BeepObj.1 = s 'BeepObj Class'
{
CLSID = s '{861BFE30-56B9-11D1-BD65-204C4F4F5020}'
}
BeepObj.BeepObj = s 'BeepObj Class'
{
CurVer = s 'BeepObj.BeepObj.1'
}
NoRemove CLSID
{
ForceRemove 
{861BFE30-56B9-11D1-BD65-204C4F4F5020} = 
s 'BeepObj Class'
{
ProgID = s 'BeepObj.BeepObj.1'
VersionIndependentProgID = s 'BeepObj.BeepObj'
ForceRemove 'Programmable'
InprocServer32 = s '%MODULE%'
{
val ThreadingModel = s 'Apartment'
}
}
}
}



  注册表脚本

  你可能对注册表的.REG脚本非常熟悉。.RGS脚本也是类似的,不过它使用的是一个完全不同的句法,而且只能被ATL作对象注册使用。这些句法允许作简单的变量取代,如在%MODULE%中的变量。这些脚本被ATL的注册组件调用(Registrar)。它由对象头文件的一个宏定义:

  DECLARE_REGISTRY_RESOURCEID(IDR_BEEPOBJ)

  基本上,当服务器调用CComModule::RegisterServer()时,便会使用这些脚本来载入注册表的设置,并在调用CComModule::UnregisterServer()时移除它们。所有COM的注册键都放在HKEY_CLASSES_ROOT。以下就是被设置的注册键:

  BeepObj.BeepObj.1 - 该类的目前版本 

  BeepObj.BeepObj - 通过名字标识COM对象 

  CLSID - 该对象的唯一类标识

  在CLISD下还有一些子键:

  ProgID -programmatic标识 

  VersionIndependentProgID -将一个ProgID和一个CLSID关联

  InprocServer32 - 定义一个服务器的类型(作为一个DLL)。根据服务器的类型而有不同(进程内,本地,远程服务器)

  ThreadingModel -对象的COM线程模式 

  TypeLib - 服务器类库的GUID

 

你可能感兴趣的:(COM,服务器,dll,object,编译器,脚本,module)