使用dll启动的组件,将和客户程序共享一个进程地址空间。所以组件的接口指针我们可以直接使用。但是如果服务器是以exe形式提供的,则客户端和服务器运行在不同的地址空间中,客户端调用服务器端的方法时就要跨越进程边界。对于调用跨进程边界的接口,我们需要考虑以下问题:
6.1 问题一的解决
要想在一个进程中调用另一个进程中的函数,其实就是一种进程间通信。操作系统提供了多种执行进程间通信的方法,其中COM所使用的是本地过程调用(LPC)。那么LPC是如何实现的,这一点看似比较神奇,实际上并非如此。虽然不同的进程在不同的虚拟地址空间中,通过进程自身的能力是无法跨越进程边界的。但是进程是由操作系统创建的,进程的虚拟内存和物理内存的映射也是由操作系统完成的。所以操作系统当然知道每一个进程的逻辑地址空间相对应的物理地址,因此操作系统可以调用任意进程中的任意函数。自然LPC的实现也就非常简单了。
在COM中使用LPC实现一个进程调用另一个进程中的函数。
6.2 问题二的解决
我们使用LPC实现进程间函数的调用,但是这只是调用exe中函数的第一步。函数的调用常常需要传递参数,如何将一个进程的地址空间中的参数传递到另一个进程的地址空间中。在COM中解决这个问题的方法被称作是"调整"。
如果参与参数传递的两个进程在同一台机器上,则调整过程将是相当直接的:只需将参数数据从一个进程的地址空间复制到另一个进程的地址空间中即可。
如果参与参数传递的两个进程在不同的机器上,那么要考虑不同的机器在数据表示和存储方面的不同,例如整数的字节顺序可能会不一样,必须将参数数据转换成标准格式。
在COM中为了解决进程间参数传递的问题,我们可以让组件实现 IMarshal 接口。
6.3 问题三的解决
这第三个问题是客户无需知道它所访问的组件是进程内组件,进程外组件,还是远程组件。但是如果让客户自己处理 LPC 的问题,这样就暴露了组件实现的细节---组件是进程内,进程外,远程组件。这违背了COM规则:接口与实现分离。
为了在这一点上实现接口与实现分离。COM使用了一种狠简单的方法:
从上面这段话,我们也可知,其实user32.dll, gdi32.dll, kernel32.dll 这些函数只不过在内部使用了 LPC进行调用内核级代码,从而保证了系统的稳定性。其实系统提供的api,在ntoskrnl.exe这个程序中,所以系统必须使用LPC和参数调整来实现Win32API.
客户端,组件,代理,残根四者直接的关系图:
通过上面的分析,我们要想编写一个进程外或者说是远程服务器组件,就必须要完成大量的代码,如代理和残根dll的实现等,还有要进行LPC,以及将参数从一个进程传递到另一个进程中。这使得我们编写COM组件的难度大大增加。所幸的是,所有这些大量的工作都不需要我们自己来亲自完成,我们使用IDL/MIDL来完成这些工作
6.4 IDL/MIDL
IDL的全称是接口定义语言,它的语法同C和C++是一样的,可以细致的描述接口和组件。
在使用IDL对接口和组件进行了描述之后,可以通过MIDL(Microsoft的IDL编译器)来编译它。MIDL将接收接口的IDL描述,并生成相应的代理和残根dll的C代码。对这些代码进行编译和链接之后,就可以得到相应的代理和残根dll。这样就不需要我们手工实现这两个dll了。
在编写好idl文件之后,即可使用midl编译器编译此idl文件,例如如果我们有一个描述我们接口的文件foo.idl,则可以使用如下的命令编译此文件:
midl foo.idl
执行此命令后生成以下文件: