第10章 EXE中的服务器
1、 对于跨越进程边界的接口,我们需要考虑如下一些条件:
(1) 一个进程需要能够调用另外一个进程中的函数。
(2) 一个进程需要能够将数据传递给另外一个进程。
(3) 客户无需关心它所访问的服务器是进程内服务器还是进程外服务器。
2、 COM对于进程间通信采用的方法为本地过程调用(LPC),它是基于远程过程调用(RPC)的用于单机上进程间通信的专利技术。
3、 LPC由操作系统实现。
4、 几乎在每次调用Win32函数时,我们都用到了LPC。当调用Win32函数时,系统实现上将调用一个DLL中的函数,而次函数将通过LPC调用Wndows中的实际代码。
5、 COM使用的结构与此类似,客户将同一个模仿组件的DLL进行通信。这个DLL可以为客户完成参数的调整及LPC调用。在COM中,此DLL(也是一个组件)被称作是一个代理。
6、 用COM的术语来说,一个代理就是同另外一个组件行为相同的组件。
7、 代理:必须是DLL形式的,因为它们需要访问客户进程的地址空间以便对传给接口函数的数据进行调整。
8、 残根:组件还需要一个被称作是残根的DLL,以对从客户传来的数据进行反调整。
9、 客户同一个代理DLL进行通信。代理将对函数参数进行调整并通过LPC来调用残根DLL。残根DLL将对这些参数进行反调整,并将其传给组件中合适的函数以调用之。
10、IDL(接口定义语言):借助接口定义语言,我们可以编写接口的一个描述,然后用MIDL(Microsoft的IDL编译器)编译即可生成代理和残根DLL。
11、IDL接口描述举例:
import "unknwn.idl" ;
// Interface IX
[
object, // 表示所定义的接口为一个COM接口
uuid(32bb8323-b41b-11cf-a6bb-0080c7b2d682), // 相应接口的IID
helpstring("IX Interface"), // 将帮助串放放到一个类型库中
pointer_default(unique) //
]
interface IX : IUnknown
{
HRESULT FxStringIn([in, string] wchar_t* szIn) ;
HRESULT FxStringOut([out, string] wchar_t** szOut) ;
} ;
在C++中与上述代码相应的函数将如下所示:
virtual HRESULT __stdcall FxStringIn(wchar_t* szIn);
virtual HRESULT __stdcall FxStringOut(wchar_t** pszOut);
pointer_default关键字的作用就是告诉MIDL编译器在没有为指针指定其他属性石如何处理此指针。pointer_default有三个不同的选项:
(1) ref:将指针当成引用对待。此时表示此指针将总是指向一个合法的地址,并可能被反引用。这种指针不能为空。在调用前后他们将指向同一内存地址。在函数内部,不能为他们指定别名。
(2) unique:此类指针可以为空,并且在函数内可以修改他们的值,但不能为之指定别名。
(3) ptr:此选项指定相应的指针就是一个C指针。此类指针可以是有一个别名、可以为NULL,并且其值可以被修改。
12、IDL中的参数:
HRESULT foo([in] int x, [in, out] int* y, [out] int* z);
in的参数,MIDL将知道仅仅需要将此参数值从客户传递给组件,残根代码不需要送回任何值。
out的参数,告诉MIDL相应的参数仅被用来从组件向客户传回有关的数据。代理不需要对输出参数值进行调整,也不需要将此值传送给组件。
所以上述例子中,y既是入参,也是出参。对于出参,MIDL要求它必须是一个指针。
13、IDL中的字符串:COM中对于字符串的标准约定是UNICODE,即wchar_t。也可使用OLECHAR或LPOLESTR.
14、size_is:
// 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[]) ;
} ;
在HRESULT FyArrayIn([in] long sizeIn,
[in, size_is(sizeIn)] long arrayIn[]) ;
函数中,size_is修饰符将告诉MIDL数组中元素的个数将被保存在sizeIn中。提供给size_is修饰符的参数只能是输入参数或输入-输出参数。
15、MIDL生成的文件:
以foo.idl文件为例,其内容如下:
import "unknwn.idl" ;
// Interface IX
[
object, // 表示所定义的接口为一个COM接口
uuid(32bb8323-b41b-11cf-a6bb-0080c7b2d682), // 相应接口的IID
helpstring("IX Interface"), // 将帮助串放放到一个类型库中
pointer_default(unique) //
]
interface IX : IUnknown
{
HRESULT FxStringIn([in, string] wchar_t* szIn) ;
HRESULT FxStringOut([out, string] wchar_t** szOut) ;
} ;
运行midl foo.idl之后,将生成如下文件:
foo.h:一个同C和C++兼容的、包含IDL中描述的所有接口声明的头文件。可以用两个等价的开关/header或/h改变此头文件的名称。
foo_i.c:一个定义有IDL文件中所用的所有GUID的C文件。可以使用/iid开关改变此文件的名称。
foo_p.c:一个实现IDL文件中接口的代理及残根的C文件。此文件的名称可以用/proxy加以修改。
dlldata.c:一个实现包含代理及残根的DLL的C文件。此文件的名称可以用/dlldata开关加以修改。
16、将MIDL编译IDL文件后的文件生成代理和残根DLL的方法:
以本章的server.idl为例,其内容为:
//
// Server.idl - IDL source for Server.dll
//
// The MIDL compiler generates proxy/stub code and a type library
// from this file.
//
//
// Interface descriptions
//
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) ;
} ;
// 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[]) ;
} ;
// Structure for interface IZ
typedef struct
{
double x ;
double y ;
double z ;
} Point3d ;
// Interface IZ
[
object,
uuid(32bb8325-b41b-11cf-a6bb-0080c7b2d682),
helpstring("IZ Interface"),
pointer_default(unique)
]
interface IZ : IUnknown
{
HRESULT FzStructIn([in] Point3d pt) ;
HRESULT FzStructOut([out] Point3d* pt) ;
} ;
//
// Component descriptions
//
[
uuid(d3011ee0-b997-11cf-a6bb-0080c7b2d682),
version(1.0),
helpstring("Component1.0 Type Library")
]
library ServerLib
{
importlib("stdole32.tlb") ;
// Component 1
[
uuid(0c092c29-882c-11cf-a6bb-0080c7b2d682),
helpstring("Component 1 Class")
]
coclass Component1
{
[default] interface IX ;
interface IY ;
interface IZ ;
};
// Component 2
[
uuid(0c092c2a-882c-11cf-a6bb-0080c7b2d682),
helpstring("Component 2 Class")
]
coclass Component2
{
[default] interface IY ;
interface IZ ;
};
// Component 3
[
uuid(0c092c2b-882c-11cf-a6bb-0080c7b2d682),
helpstring("Component 3 Class")
]
coclass Component3
{
[default] interface IZ ;
} ;
} ;
利用以下命令编译IDL文件server.idl:
midl /h iface.h /iid guids.c /proxy proxy.c server.idl
生成的文件包括:
Header File: iface.h
IID File: guids.c
DLL Data File: dlldata.c
Proxy File: proxy.c
利用VC创建一个空的dll工程,将以上文件都添加到工程中。将proxy.def文件也添加到工程中。
proxy.def内容如下:
LIBRARY Proxy.dll
DESCRIPTION 'Proxy/Stub DLL'
EXPORTS
DllGetClassObject @1 PRIVATE
DllCanUnloadNow @2 PRIVATE
GetProxyDllInfo @3 PRIVATE
DllRegisterServer @4 PRIVATE
DllUnregisterServer @5 PRIVATE
然后参考MAKE-ONE中需要连接的如下几个库,添加到工程中:
PROXYSTUBLIBS = kernel32.lib \
rpcndr.lib \
rpcns4.lib \
rpcrt4.lib \
uuid.lib
然后添加如下预定义宏:
REGISTER_PROXY_DLL
OK,可以编译生成代理/残根DLL了。
17、费了老力了才实现本章的4个例程:SERVER.EXE,SERVER.DLL,PROXY.DLL,CLIENT.EXE。
其中需要用到的iface.h文件和guids.c文件都是用midl编译利用以下命令编译IDL文件server.idl:
midl /h iface.h /iid guids.c /proxy proxy.c server.idl
后得来的。或者直接在本章源码下运行nmake –f makefile,也可以得到上述文件。
PROXY.DLL的VC工程建立如上16所示。
SERVER.EXE的VC工程建立注意要在预处理宏或源代码文件中添加中添加_OUTPROC_SERVER_。
SERVER.DLL和CLIENT.EXE没有什么特别的地方。添加上需要的库就可以了。注意DLL需要添加对应的DEF文件。
18、 本章的源码在VC下有几个小错误,需要改正下(前面的章节源码也存在这个问题,不过都很容易,类型转换而已)。
(1)CFACTORY.CPP:
BOOL APIENTRY DllMain(HANDLE hModule,
DWORD dwReason,
void* lpReserved )
{
if (dwReason == DLL_PROCESS_ATTACH)
{
CFactory::s_hModule = (HMODULE)hModule ;
}
return TRUE ;
}
(2)OUTPROC.CPP
BOOL InitWindow(int nCmdShow)
{
// Fill in window class structure with parameters
// that describe the main window.
WNDCLASS wcListview ;
wcListview.style = 0 ;
wcListview.lpfnWndProc = (WNDPROC)MainWndProc ;
wcListview.cbClsExtra = 0 ;
wcListview.cbWndExtra = 0 ;
wcListview.hInstance = CFactory::s_hModule ;
wcListview.hIcon = ::LoadIcon(CFactory::s_hModule,
MAKEINTRESOURCE(IDC_ICON)) ;
wcListview.hCursor = ::LoadCursor(NULL, IDC_ARROW) ;
wcListview.hbrBackground = (HBRUSH)(::GetStockObject(WHITE_BRUSH)) ;
wcListview.lpszMenuName = NULL ;
wcListview.lpszClassName = "MyServerWinClass" ;
19、对于本章的学习主要是把源码编译成了VC的工程,并且大致浏览了一下,但并没有完全掌握。像本章的server.exe这样实现的,可以直接当做进程外服务器来使用。是否遵循COM的EXE都是类似如此的形式?
20、代理/残根的接口登记在HKEY_CLASSES_ROOT\Interface\{$uuid}下,如本章:
HKEY_CLASSES_ROOT\Interface\{32BB8323-B41B-11CF-A6BB-0080C7B2D682}即为IX的接口,然后对应:
HKEY_CLASSES_ROOT\CLSID\{$uuid}即为相应组件在注册表中的信息。
21、本地服务器去掉入口点函数:
DllGetClassObject
DllCanUnloadNow
DllRegisterServer
DllUnregisterServer
(1) DllCanUnloadNow:EXE可以对自己的生命期进行控制,因此EXE不需要实现DllCanUnloadNow,可以去掉该函数。
(2) DllRegisterServer和DllUnregisterServer的替换可以通过给EXE提供相应的命令行参数来实现。
(3) DllGetClassObject:这个稍微麻烦些。CoCreateInstance将调用CoGetClassObject,而CoGetClassObject将调用DllGetClassObject,DllGetClassObject将返回一个IClassFactory指针,此指针将被用于创建相应的组件。由于EXE不输出DllGetClassObject,因此需要给CoGetClassObject提供另外一种方法以获取一个IClassFactory指针。COM解决这个问题的方法试维护一个关于被登记的类厂的内部表格。当客户用合适的参数调用CoGetClassObject时,COM将首先检查此关于类厂的私有表格,以得到与客户请求的CLSID相应的类厂。若相应的类厂不在此表格中,COM将在注册表中进行查找并启动相应的EXE。此EXE奖完成相应类厂的登记。并且此时EXE必须登记它所支持的所有类厂。
22、远程访问能力:这部分的实现由于公司的电脑权限限制,也没法找到两台机器,没有实现。待以后补充。书中讲述是通过DCOMCNFG.EXE来实现将本地服务器变成一个远程服务器的。
23、如何决定DCOM是否可用:
(1)静态链接OLE32.DLL的情况下
#include <Windows.h>
#include <iostream>
using namespace std;
void main()
{
if ( GetProcAddress(GetModuleHandle("OLE32"), "CoInitializeEx") != NULL )
{
cout << "OK" << endl;
}
}
(2)动态链接OLE32.DLL的情况下
#include <Windows.h>
#include <iostream>
using namespace std;
void main()
{
if ( GetProcAddress(LoadLibrary("OLE32"), "CoInitializeEx") != NULL )
{
cout << "OK" << endl;
}
}
奇怪我的电脑上静态链接的没有输出”OK”,动态链接的有输出“OK”。用dumpbin查看
C:\WINDOWS\system32\ole32.dll明明有导出CoInitializeEx的。这个就留着以后实现远程访问的时候再关注。
24、本章例程文件在优快云我的资源中InsideCOM\CHAP010XXX。