EXE 中的服务器
不同的进程
DLL也被称作是进程中服务器, 而EXE则被称作是进程外服务器,远程服务器指的是运行于另外一个不同的机器上的进程外服务器。
对于跨越进程边界的接口,我们需要考虑如下一些条件:
- 一个进程需要能够调用另外一个进程中的函数
- 一个进程需要能够将数据传递给另一个进程
- 客户无需关心她所访问的服务器是进程内服务器还是进程外服务器
本地过程调用
调整
对指针的处理就将不同于对整数的处理。 对指针的调整指的是要将指针引用的结构复制到另外一个进程中。 但若此指针是一个接口指针 , 则将不能复制此指针所引用的内存。这里我们看到 , 调整并不是用一条简单的 memcpy 命令就能完成的。
为对组件进行调整 , 可以实现一个名为IMarshal 的接口。 在 COM 创建组件的过程中 , 它将查询组件IMarshal 接口。 然后它将调IMarshal的成员函数以在调用函数的前后调整或反调整有关的参数。
代理/残根 DLL
一个代理 就是 同另 外 一个 组件 行为 相同 的 组件。 代理 必须 是DLL 形式的 , 因为它们需要访问客户进程的地址空 间以便对 传给接 口函数的 数据进行 调整。 对数据的调整只完成了任 务的 一 半 , 组件 还需 要 一个 被称 作是 残根 的 DLL, 以对 从客户传来的数据进行反调整。 残根也将对传回给客户的数据进行调整。
IDL/MIDL
IDL 接口定义语言。MIDL 编译器即可生成代理和残根DLL。
IDL 接口描述举例
import "unknwn.idl" ;
// Interface IX
[
object,
uuid(32bb8323-b41b-11cf-a6bb-0080c7b2d682),
helpstring("IX Interface"),
pointer_default(unique)
]
interface IX : IUnknown
{
HRESULT FxStringIn([in, string] wchar_t* szIn) ;
HRESULT FxStringOut([out, string] wchar_t** szOut) ;
} ;
. pointer_default
pointer_default 的作用就是告诉MIDL 编译器在没有为指针指定其他属性时如何处理此指针。 pointer_default 关键字具有三个不同的选项:
Ref |
将指针当成引用,此时表示此指针将总是指向一个合法的地址 |
Unique |
此类指针可以为空,并且在函数内可以修改它们的值,但不能为之指定别名 |
Ptr |
相应的指针就是C指针,此类指针可以是有一个别名,也可以为NULL,并且其值可以被修改 |
2. IDL 中的输入与输出参数
MIDL 可以使用in 和 out 参数属性对属性及残根代码进行进一步的优化,in 代表仅仅输入,残根代码不需要送回任何值。Out 关键字高速MIDL 相应的参数仅被用来从组件向用户传回有关的数据。,可以同时用in和out
- IDL 中的字符串
通过参数在函数中加上一个string 修饰符,MIDL 将知道相应的参数为一个字符串,并且它可以通过查找末尾的空字符而决定其产长度。COM 中对于字符串的标准约定是Unicode,即wchar_t。当然,可以使用在COM 头文件中定义的OLECHAR 或 LPOLESTR。
- IDL 中的HRESULT 值
MIDL 要求用object 修饰符标记的接口返回HRESULT 值。当连接到一个远程服务 器时, 任何函数调用都可能由于网络传输的问题而失败。因此应该有一种让所有函数都能指示网络错误的方法。最简单的方法就是让所有的函数返回一个HRESULT 值。
若某个函数需 要返回一个不是HRESULT类型的值,那么可以在相应的地方加上一个输出参 数。 在FxStringOut 中, 我们定义了一个输出参数以从组件得到一个返回串。函数FxStringOut将使用 CoTaskMemAlloc来分配返回串所需的内存。客户必须使用CoTaskMemFree来释放这块内存。下面的代码显示了如何使用前面定义的 IX 接口。
wchar_t* szOut = NULL ;
hr = pIX->FxStringIn(L"This is the test.") ;
assert(SUCCEEDED(hr)) ;
hr = pIX->FxStringOut(&szOut) ;
assert(SUCCEEDED(hr)) ;
// Display returned string.
ostrstream sout ;
sout << "FxStringOut returned a string: "
<< szOut
<< ends ;
trace(sout.str()) ;
// Free the returned string.
::CoTaskMemFree(szOut) ;
IDL 中的import 关键字
用于将其他IDL 文件中的定义包含到当前文件中。
六:IDL 中的site_is 修饰符
客户和组件之间传递数组的接口的IDL 描述
// Interface IY
[
object,
uuid(32bb8324-b41b-11cf-a6bb-0080c7b2d682),
helpstring("IY Interface"),
pointer_default(unique)
]
interface IY : IUnknown
{
HRESULT FyCount([out] long* sizeArray) ;
HRESULT FyArrayIn([in] long sizeIn,
[in, size_is(sizeIn)] long arrayIn[]) ;
HRESULT FyArrayOut([out, in] long* psizeInOut,
[out, size_is(*psizeInOut)] long arrayOut[]) ;
} ;
提供给 size - is 修饰符的参 数只 能是 输入 参 数或 输入 - 输出 参 数。实际代码实现中,尽量少的使用in,out 属相的参数。
IDL 中的接口
IDL 文件中也可以定义C 和C++ 风格的结构,并可用它们作为函数的参数。
但是在传递包含指针的复杂接口时,MIDL 需要精确制导一个指针所指的到底是什么内容,这样它才能对指针所指的数据进行调整。因此,绝对不可以使用void* 作为参数。如果需要传递一个一般性的接口,可以使用一个IUnknown* 接口。最灵活的方法是让客户传递一个IID,这同QueryInterface 相似。
HRESULT GetIface([in] cosnt IID& iid,[out,iid_is(iid)]IUnknown** ppI);
MIDL 编译器
代理DLL 的建立
MIDL 编译器将为组件生成相应的代理和残根的代码。但是将这些代码编译成一个DLL 仍然是程序员的工作。为此第一步需要为代理DLL 编写一个DEF 文件:
#################################################
#
# Proxy source files
#
iface.h server.tlb proxy.c guids.c dlldata.c : server.idl
midl /h iface.h /iid guids.c /proxy proxy.c server.idl
!IF "$(OUTPROC)" != ""
dlldata.obj : dlldata.c
cl /c /DWIN32 /DREGISTER_PROXY_DLL dlldata.c
proxy.obj : proxy.c
cl /c /DWIN32 /DREGISTER_PROXY_DLL proxy.c
PROXYSTUBOBJS = dlldata.obj \
proxy.obj \
guids.obj
PROXYSTUBLIBS = kernel32.lib \
rpcndr.lib \
rpcns4.lib \
rpcrt4.lib \
uuid.lib
proxy.dll : $(PROXYSTUBOBJS) proxy.def
link /dll /out:proxy.dll /def:proxy.def \
$(PROXYSTUBOBJS) $(PROXYSTUBLIBS)
regsvr32 /s proxy.dll
LIBRARY Proxy.dll
DESCRIPTION 'Proxy/Stub DLL'
EXPORTS
DllGetClassObject @1 PRIVATE
DllCanUnloadNow @2 PRIVATE
GetProxyDllInfo @3 PRIVATE
DllRegisterServer @4 PRIVATE
DllUnregisterServer @5 PRIVATE
代理/残根的登记
编译文件DLLDATA.C 和 PROXY.C 时定义了一个宏REGISTER_PROXY_DLL ,这个宏将生成完成代理/存根的登记所需的代码
然后上面,makefile 文件中,proxy.dll 生成后,紧接着一句,regsvr32 /s proxy.dll 确保了该代理一定会被登记。
登记的位置:
HKEY_CLASSES_ROOT\
Interfaces\
{一个ID}
本地服务器的实现
当运行客户程序时,询问用户想使用组件的进程中服务器版本还是进程外服务器版本。客户将使用CLSCTX_INPROC_SERVER连接进程中服务器,用CLSCTX_LOCAL_SERVER 连接到进程外服务器。
去掉入口点函数
问题:EXE 无法输出函数。现在我们的进程中服务器依赖于如下的一些函数:
DllCanUnloadNow
DllRegisterServer
DllUnregisterServer
DllGetClassObject
注册卸载可以通过命令行参数来实现。
EXE 不是被动的,它可以自己实现对于生命周期的控制。
DllGetClassObject的实现相对比较困难。
类厂的启动
CoCreateInstance->CoGetClassObject->DllGetClassObject->返回一个IClassFactory 指针->创建相应的组件。
EXE 不输出DllGetClassObject 。COM 维护了一个关于被登记的类厂的内部表格。当用户调用CoGetClassObject->根据CLSID->类厂->如果类厂不在当前的表格->在注册表中查找并启动相应的EXE。此EXE 将完成相应类厂的登记(CoRegisterClassObject),COM->此时可以找到。对于CFactory ,我们可以给它加上一个新的静态成员函数StartFactories,它将为CFactoryData 结构中所列的每一个组件调用CoRegisterCalssObject。
HRESULT CoRegisterClassObject(
REFCLSID rclsid,
LPUNKNOWN pUnk,
DWORD dwClsContext,
DWORD flags,
LPDWORD lpdwRegister
);
上面我们看到, 类厂的登记实际上是一个比较简单的过程: 只需建立相应的类厂并将
其接口指针传给CoRegisterClassObject 即可。CoRegisterClassObject 的大多数参数的含义
都可以从前面的代码明显地看出来。第一个参数是被登记的类的CLSID, 其后是一个指
向其类厂的指针。最后一个指针可以传回一个魔饼。此魔饼将在用CoRevokeClassObject
来取消相应类厂的登记时用到。第三个和第四个参数都是一些DWORD 标志, 它们可以
对CoRegisterClassObject 的行为进行控制
二:类厂的释放
HRESULT CoRevokeClassObject(
DWORD dwRegister
);
下面的星号中间,是一个小细节的地方
**********************************************************************************************************
- CoRegisterClassObject 标志
Flags 参数表示EXE 的单个实例能否支持一个组件的多个实例。
假定有一个登记了若干个组件的EXE,并且此EXE 需要使用它登记的某一个组件,如果按照:
Hr = ::CoRegisterClassObject(clsid,pIUnknown,CLSCTX_LOCAL_SERVER,REGCLS_MULTI_SEPARATE,&dwRegister);
系统将装载EXE 的另外一个实例以提供它自己的组件。这是没有必要和低效的。
此时我们可以将第三个参数设置为:CLSCTX_LOCAL_SERVER|CLSCTX_INPROC_SERVER
因为这种情况十分常见,系统提供了REGCLS_MULTIPLEUSE,此标志可以在出现了CLSCTX_LOCAL_SERVER时自动设置CLSCTX_INPROC_SERVER ,因此下面的代码前面的调用是相等同的。:
Hr = ::CoRegisterClassObject(clsid,pIUnknown,CLSCTX_LOCAL_SERVER,REGCLS_MULTIPLEUSE,&dwRegister);
为此可首先用如下的命令取消进程中服务器的登记。这将保证可用的服务器只是本地服务器。然后可运行客户程序并选择第二个选项以运行本地服务器。本地服务器此时将能够正常工作。
然后我们可以将CFactory::StartFactories中的REGCLS_MULTIPLEUSER 修改为REGCLS_MULTI_SEPARATE,在重新编译、链接客户和服务器之后,可运行客户并选择第二个选项。此时Create调用将会失败,因为没有进程中服务器来满足内部组件的创建过程,而REGCLS_MULTI_SEPARATE则告诉COM 库不要自己来满足对进程中组件的请求。
**********************************************************************************************************
对LockServer 的修改
进程中服务器将输出函数DllCanUnloadNow。COM 库将调用此函数以决定能否从
内存中将服务器卸载掉。此函数是通过静态函数CFactory ∷CoUnloadNow 实现的。
CFactory∷CoUnloadNow 将检查静态变量CUnknown∷ s_ActiveComponents 的值, 决定
是否可以将服务器从内存中卸载掉。在建立一个新的组件时, 静态变量CUnknown∷s_ActiveComponents 的值将被相应地增大。但是在创建类厂时我们将不增大s_ActiveComponents的值(因为,组件的激活数目和类厂的创建是无关的)。因此, 一个服务器可以拥有打开的类厂, 但它将仍然能够被关闭掉。
本地服务器所完成的第一件事情就是创建它所用的所有类厂, 而最后做的就是释放这些类厂。若
服务器在它被卸载之前需要等待所有这些类厂被释放, 那么此等待的时间将是相当长的,
这是因为服务器在卸载时需要亲自将它们卸载掉。因此, 当客户在创建组件时, 若它们需
要服务器确实是在内存中, 那么可以使用IClassFactory∷LockServer 来保证这一点。
DLL 不能控制它们的生命期, 它们的装载和卸载都是由另外的EXE 文件完成的。但EXE 则不同,
它们本身可以起到控制作用, 并且可以装卸或卸载它们自己。正是由于EXE 的卸载是由
它自己完成的, 因此需要对LockServer 进行修改以便当加锁计数值变成零时退出相应的
EXE。为此, 我们可以在CFactory 中加上一个新的成员函数CloseExe, 它将给应用程序
的消息循环发送一条WM_QUIT 消息
//
// Determine if the component can be unloaded.
//
HRESULT CFactory::CanUnloadNow()
{
if (CUnknown::ActiveComponents() || IsLocked())
{
return S_FALSE ;
}
else
{
return S_OK ;
}
}
// LockServer
HRESULT __stdcall CFactory::LockServer(BOOL bLock)
{
if (bLock)
{
::InterlockedIncrement(&s_cServerLocks) ;
}
else
{
::InterlockedDecrement(&s_cServerLocks) ;
}
// If this is an out-of-proc server, check to see
// whether we should shut down.
CloseExe() ; //@local
return S_OK ;
}
#ifdef _OUTPROC_SERVER_
//
// Out-of-process server support
//
static BOOL StartFactories() ;
static void StopFactories() ;
static DWORD s_dwThreadID ;
// Shut down the application.
static void CloseExe()
{
if (CanUnloadNow() == S_OK)
{
::PostThreadMessage(s_dwThreadID, WM_QUIT, 0, 0) ;
}
}
#else
// CloseExe doesn't do anything if we are in process.
static void CloseExe() { /*Empty*/ }
#endif
组件的析构函数中,也需要调用CloseExe,因为此时组件也可以决定是否可以将其从内存中卸载掉:
//
// Destructor
//
CUnknown::~CUnknown()
{
::InterlockedDecrement(&s_cActiveComponents) ;
// If this is an EXE server, shut it down.
CFactory::CloseExe() ;
}
消息循环
为了不致使EXE 退出,可以加上一个windows 的消息循环。这个消息循环同所有的windows 程序所用的消息循环是相同的。
那么我们如何决定用户何时启动了服务器而不是COM 库呢? 当CoGetClassObject
装载本地服务器的EXE 时, 它将在命令行上加上一个Embedding 参数。EXE 将检查命
令行中的Embedding 参数。若没有此参数, 它将增大s_cServerLocks 值并创建一个窗口
以便同用户进行交互。
当用户将服务器关闭时, 客户可能仍然在使用服务器。因此, 服务器应在用户退出时
将其用户界面元素清除掉, 但它不应该退出, 这样才能继续为其客户提供相应的服务。因
此当服务器收到一个WM - DESTROY 消息时, 它只应在CanUnloadNow 返回S - OK 时
才发送WM - QUIT 消息。
case WM_DESTROY: // message: window being destroyed
if (CFactory::CanUnloadNow() == S_OK)
{
// Only post the quit message, if there is
// no one using the program.
::PostQuitMessage(0) ;
}
break ;
- 用户的计数
服务器的用户也是一种类型的客户,也需要有它们自己的加锁计数值。当所有用户创建组件时,也需要将CFactory::s_cServerLocks增大。这样,只要有用户在使用服务器,它将一直保存在内存中。