“不同EXE中的组件和客户将在不同的进程中运行,这是因为每一个EXE都有其自己的进程空间。这样一来,客户和组件之间的交互就会跨越进程边界了。”
每个EXE都有自己的不同进程,DLL将被映射到链接它们的EXE文件的进程空间中。
因此DLL被称作进程中服务器,EXE则被称作进程外服务器。
某些情况下,EXE也被称作是本地服务器以同另一种进程外服务器“远程服务器”相区别。
远程服务器也就是运行在另外一台机器上的进程外服务器。
组件调用,其实是将接口指针返回给客户,如果客户和组件位于不同的进程、使用的不是同一地址空间,那么接口指针(地址空间内的逻辑地址)的物理地址将是不同的。这样一来组件调用即成空谈。
SO,解决进程间的通信是多么重要啊!
本地过程调用LPC
进程间通信有几种方法:动态数据交换(DDE)、命名管道以及共享内存等。COM所用的方法则为本地过程调用(LPC),它是基于远程过程调用RPC的用于单机上的进程通信的专利技术。RPC是在分布式计算环境(DCE)RPC规范中定义的,使得不同机器上的进程可以使用各种网络传输技术进程通信。
LPC实际上是由操作系统实现的。操作系统当然知道每个进程的逻辑地址空间和对应的物理地址空间,他当然可以调用任何想调用的东东。(操作系统为什么是男的?)(笔者你忘记了吗,这是你自己写的嘛)
得到了EXE中的函数也就是某个地址以后,第二步就是要在不同进程间传递数据,将函数调用的参数传入。俺们使用一种被称为“调整”的方法。如果倆进程在同一单机上,淡然只需要将参数复制到另一进程的地址空间就OK了;如果倆进程在不同的机器上,就要考虑到二者在数据表示方面的不同,例如字节顺序啊之类的,必须要将参数转换成标准的格式。(虾米标准?谁定的标准?不是类似W3C的标准吧?啊)
为对组件进行调整,可以实现一个IMarshal的接口。在COM创建组件的过程中,它将查询组件IMarshal接口,然后调用IMarshal的成员函数,以在客户调用函数的前后调整或反调整有关参数。COM库中实现了一个可以供大多数接口使用的IMarshal的标准版本。(又是一个标准)
关于IMarshal的问题,Krarig。。。所著《Inside OLE》有详细讨论
代理/残根DLL
一直都在说的一件事是通过一个IUnknown或其他什么接口就可以调用组件,但到底怎么做的也不知道,就知道是通过LPC做的。LPC,当然,实际上是操作系统在做。因此,此时得知几乎所有Win32函数都会用到LPC也就不奇怪了。调用Win32函数,也就是操作系统调用一个DLL中的函数,也就是会通过LPC调用Windows中的实际代码。
COM使用的结构与此类似(嗷~什么?类似?难道前面说的不就是COM所使用的结构吗?细细回味ing),客户将同一个模仿组件的DLL进行通信,这个DLL可以为客户完成参数的调整和LPC调用,在COM中,这个DLL,当然这个DLL也是一个组件,这个DLL这个组件被称作是一个代理。
以COM的专业说法,一个代理就是同另外一个组件行为相同的组件。代理必须是DLL的!(为什么捏?)自然是因为代理就是要完成参数的调整,如果不是DLL不能正确获取地址空间内的数据,那就是白搭。通过代理DLL,组件能对输出的参数进行调整,对客户返回的数据调整是由另一个被称作残根的DLL来做的。
IDL/MIDL简介
不要被代理和残根吓到,借助一种名为IDL(接口定义语言),我们只需要描述一个接口,然后MIDL编译器就会自己生成代理和残根。当然,如果感兴趣、愿意和有能力去做,你也可自己去写这个代理和残根。(真想知道残根一词的英文原词是虾米)
IDL语言,同UUID设计和RPC规范一样,都是从开放软件基金会(OSF)的分布式计算机环境(DCE)借用来的,“语法同C和C++是一样的”(那还何必叫劳什子IDL呢?),可以细致地描述接口和组件所共享的接口和数据。COM只用到了IDL的一个子集,Microsoft为了更好的支持COM对它做了一些扩展。
MIDL,微软的IDL编译器。
一个idl例子:
import "unknown.idl"
// Interface IX
[
object,
uuid(xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx),
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 ** szOut);
IDL用方括号[]来作为信息分隔符,在每个接口定义前,都有一个属性列表或称接口头。
object表示定义的接口为一个COM接口,关键字object即是Microsft对IDL的扩展之一。
uuid,就是接口IID了。
helpstring,将一个帮助字符串放到类型库中。类型库的事情,咱下回再说。
pointer _default,此处的重点。
pointer _default
用来告诉MIDL编译器在没有为指针指定其他属性时,如何处理该指针。
ref 将指针当成引用。此时表示指针总是指向一个合法地址,并可被反引用??。
这种指针不能为空。在调用前后它们指向同一内存地址。
在函数内部,不能为它们指定别名。
unique 此类指针可以为空,并且函数内部可以修改它们的值,但不能为之指定别名。
ptr 此指针就是一个个C指针。可以有一个别名(强调一个?)可以NULL,值可被修改。
in、out和string
MIDL会根据in和out属性对代理及残根代码进行优化。in,表示残根DLL不需返回值,out表示代理DLL不需调整值,不需将值传送给组件。
string告诉MIDL该参数是一个字符串(末尾以一空字符结束)。COM中对字符串的标准约定是Unicode字符即wchar _t。
HRESULT
MIDL要求,用object修饰的接口必须返回HRESULT值。因为object修饰的接口是COM接口么,COM接口都是返回HRESULT?断然不是。“因为任何函数调用都会由于网络传输问题失败,因此应该有一种让所有函数都只是网络错误的方法,也就是让所有函数都返回HRESULT。”由此,COM接口都是返回HRESULT——这是果,而非因。
import
用于将其他idl文件中的定义包含到当前文件。类似……你知道的。但又和……你知道的……有所不同,import可以任意多次,不会引起重复定义问题。所有COM及OLE现在叫ActiveX的标准接口都定义在相应的IDL文件中,这些文件位于C++编译器的\include中。闲得疼时可浏览之研究之以图level up。
size _is
先看一个在客户和组件之间船体数组的接口的IDL描述。
// Interface IY
[
object,
uuid(XXXXXXXX-xxxx-xxxx-xxxx-xxxxxxxxxxxx),
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 就是用来告诉MIDL,数组中的元素的个数。可以大胆的预料到,只是out参数,是不会用size_is修饰的。嗯。太傻了。
IDL的结构体
IDL说,struct可以有!于是IDL中struct就有了。
作者不愧是做OpenGL的,我刚想到可以在IDL定义Point、Vector啥的,就看到
// Structure for interface IZ
typedef struct
{
double x;
double y;
double z;
} Point3d;
具体的实现。。。。。。略!-__-b