K.O.(KeyFC Open Translation Toolset) 是目前正在开发的“开放版本”汉化工具的代号。
其中的汉化部分工具试验性的采用了代号为Chobits Application Server(CAS)的架构。
这个结构的特点为,程序全部由基于消息传递的模块动态组合而成。
前面几回介绍了CAS架构的理念,内部结构,消息的接口、定义及传递。
至此,各位所了解的CAS还处于"空想模块化"阶段——理论上写出模块化的程序是可行的。
那么具体的实现上呢?这就是今天要谈的内容: CAS模块的结构、封装和程序的组装
--- CAS模块 ---
从理论模块化到实际模块化,CAS选择了一个最直接的方法:一对一"物理"映射。
也就是说,每一个CAS的"功能模块"都映射到一个实际的文件上。
文件的格式自然也就是"动态加载库",在Windows下叫DLL(Dynamic Loading Library),在Linux下叫SO(Shared Object)。
目前CAS仅有Windows的版本;但因为整个CAS除了少数地方使用同步对象外,并没有其他平台相关的代码,因此能够被很容易的移植到Linux下。
其中核心组件Lock-free队列我为了在8CPU环境下验证正确性已经实现在Linux下了(但是由于Linux pthreads的Condition同步对象缺少Timed Wait(带时限的等待)方法,暂时没有写出带锁同步队列作性能比较)。
有时间的话,我会尝试包裹Posix Thread中的同步对象(支持Timed Wait),这样移植CAS应该就没有什么大碍了。
(虽然Interface在CAS的使用是兼容COM规范的,但是其用途并不COMish,因此不影响Linux移植。)
书归正传,那么,也就是说模块的加载需要靠"动态加载库"文件(以下简称DLL)的接口来进行了。
(下面的部分将涉及一些抽象程序代码和具体API)
--- CAS模块封装 ---
因为世界上没有未卜先知,因此CAS模块的接口必须提供一个固定的方法集合才能够被正确的加载。
不过,因为每个人都有自己写加载方法的自由,因此这里讲的只能算"官方推荐",供使用者参考(当然,作为"官方使用者",K.O.系列的程序将遵循这里介绍的接口)
I: 首先要解决的问题是,怎样知道加载的DLL是合法的CAS模块?
因此,我规定每个CAS模块DLL都需要导出(Export)一个函数: ChobitsModuleSignature
这个函数返回一个64字节的记录(没有硬性规定意义,可以是代码的MD5,或者是数码签名等),目的是让加载方能够作选择性的加载。
II: 接下来,需要确定的是加载的模块使用的是兼容的接口和消息定义。
使用动态加载库的一个著名的问题就是"DLL地狱",由于同一个DLL的不同版本可能使用不同的接口,如果管理不善可能导致各种奇怪的问题。
为了解决好这个问题,我规定每个CAS模块DLL都需要导出(Export)另一个函数: ChobitsModuleInfo
这个函数返回多种信息,包括模块名称、GUID、消息队列长度、需要多少工作线程等等;
其中有一条信息(32位整数),称作"兼容总线版本",详细的定义如下:
* 高8位: 总线运行级别
在前面的介绍中,我有意模糊了总线的具体功能,现在给出更清晰的定义。目前定义的总线有三个运行级:
0. 静态总线: 仅提供了模块注册、查询和消息传送支持等基本功能;总线内部没有线程调度,因此不执行消息投递。好处是轻载(light weight),适用于消息较少的应用环境。(K.O.系列程序使用的就是这个总线)
1. 投递总线: 顾名思义,就是增加了消息投递支持。适用于消息较密集的应用环境。(由于涉及到事件顺序,加上前段时间的总线代码重构(code re-factor),目前仍然在编写/调试中...)
2. 管理总线: 在前一个级别的基础上,增加了一个管理线程,用于执行管理工作(总线统计信息收集、消息队列阻塞检测、失效队列回收;当然最重要的是接受队列注册、注销请求——有了这个功能,就可以使用消息来注册消息队列,这也就意味着,可以由一个模块来加载另一个模块)。这个总线适用于较大型的服务和应用程序,因为它可以自动支持一个动态的执行环境(比如动态的替换一个模块的库文件到新的版本,而不需要重新启动整个应用程序)。
因为每一个运行级提供的支持都是前一个运行级的超集(Super-set),因此,自然的,兼容低级别总线的模块可以加载到高级别总线上,反过来则不行。
* 中间12位: 总线的主版本号
前几回说到了,总线和模块之间唯一需要交互的两处就是: 模块消息接口——用于发送和接收消息;基本消息接口——用于处理消息的传递过程。
同一个主版本号的总线将使用相同的模块消息接口和基本消息接口。换句话说,就是一旦模块消息接口和基本消息接口发生任何改变,主版本号将会提升。
这也就意味着,兼容不同主版本号总线的模块一定不能被加载,不论谁高谁低。
* 后面12位: 总线的兼容版本号
如果总线发生了一些细小的修订,但是没有更改模块消息接口或者基本消息接口,那么兼容版本号将会提升。
因为接口并未改变,所以兼容旧版本总线的模块将可以使用在新的总线上;但是,因为兼容新总版本总线的模块将会期待总线一定程度上的变化,因此不能用于旧版本的总线。
最后举一下例子,以免有人看晕。以下版本号的格式为: [运行级别] [主版本号] [兼容版本号]
如果总线版本为:
2 2 2
模块兼容版本为:
2 2 2 可以加载
1 2 2 可以加载 (期待运行级别低可以)
3 2 2 不可以加载 (期待运行级别高不行)
2 2 1 可以加载 (兼容前一个修订版本可以)
2 2 3 不可以加载 (兼容后一个修订版本不行)
x y z 只要y不是2就不可以加载 (主版本不一致)
III: 接下来就是模块特定导出函数了。
在目前使用的规范中,只有两种不同的模块: 总线模块(是的总线也可以是一个单独的DLL :D) 和 应用模块
总线模块导出两个函数: CMB_Request (请求总线) 和 CMB_Drop (丢弃总线)
服务模块导出三个函数: CAM_Attach (接驳总线),CAM_Detach (脱离总线) 和 CAM_Finalize (结束应用)
(注意,因为目前CAS的应用仅仅在总线运行级别0上的,因此更高级别的总线以及对应服务模块*可能*会有更多的导出函数。但是这里已经列出的函数及接口将保持不变)
* 先谈总线模块 (Chibots Bus Module, CMB):
CMB_Request 需要一个总线ID参数,返回一个总线管理接口(Interface)。(是的,如果你愿意,同一个应用程序里面可以存在多个总线。总线之间是相互独立的,没有内置的交互能力,因此怎么使用嘛就是你的事了=v=)
如果指定ID的总线不存在则创建一个新的;如果不能创建(达到个数上限),则返回空指针。
CMB_Drop 需要一个总线ID参数。其功能是将符合ID的总线的内置记录清除,这样下一次请求同样ID时,将创建一个新的总线。
注意,因为总线是通过管理接口引用的,因此也是引用计数的。调用CMB_Drop和总线是否被释放没有任何关系。(当然,推荐合理的使用方法为即将释放总线前调用CMB_Drop,然后将管理接口置空,总线将被自动释放)
* 然后是服务模块 (Chobits Application Module, CAM):
模块加载程序在调用了前面提到的ChobitsModuleInfo,检查版本符合之后,应该根据同时返回的其他信息为正在加载的模块收集资源。(比如,使用返回的名称和GUID在总线上注册一个消息接口;同时生成要求数量的线程)
准备完毕后,调用服务模块的CAM_Attach,将资源递交给模块。
这时,模块应该做的事情有:
* 初始化内部结构
* 记录传递的消息接口
* 将传入的线程(一般来说,只有一个)分配到指定的任务(一般来说,是消息处理循环)
接下来,将会有很长一段时间DLL的接口不再有动静。因为所有的交互都通过消息接口在总线上进行...
当由于某种原因(一般来说,是程序结束),消息队列将被从总线取下,模块加载程序应当首先调用CAM_Detach。(如果顺序颠倒,服务模块在发送消息时将会受到一个异常: 队列已经从总线脱离)
收到这个调用的时候,服务模块应该做的事情是:
* 将分配的线程从任务脱离 (不需要终结,它们将会被线程池自动回收)
* 如果必要的话,做出适当清理,为下一个接驳总线的请求做好准备
当CAM_Detach返回后,模块加载程序就可以放心的从总线注销(一般来说是)消息队列了。
最后,当模块加载器需要卸载模块前,应该调用CAM_Finalize,这个时候模块要做的事情就是:
* 停止正在执行的操作(本来这个时候不应该有了,但是以防万一...)
* 释放申请的所有资源
当CAM_Finalize返回后,模块加载程序就可以放心的卸载模块DLL了。
在细节里面滚打了大半圈,让我们重新回到高处,俯瞰CAS架构。
--- CAS程序的组装 ---
因为运行级别3的CAS总线的操作和使用环境与前两个有一定不同,我们分开来看。
前两个级别的CAS总线需要由独立于总线的管理操作控制(加载/卸载应用模块),因此CAS应用程序的(建议)组装结构应该像下图:
(图1) CAS应用程序
* 处理应用程序图形界面(UI),同时负责初始化的线程最先被创建(主线程);
这个线程创建一个管理线程,用于初始化和终止CAS架构,同时负责响应抽象用户请求。
* 初始化开始,总线模块首先载入,各个模块依次载入并在总线上注册,相应的模块线程被创建;
初始化最后,第二个UI线程被创建(图中与主线程重合),用于将UI事件转换成抽象用户请求,以及将结果反映到UI上。
(创建第二个UI线程的原因是,Windows有一套自己的窗体消息机制,(主要)用于和用户输入打交道,因此主线程有自己的消息循环需要处理。)
什么?你问既然有了Window消息为什么还要CAS?晕...两套消息机制的设计目的以及用途非常不同,其他的(比如多线程同步效率)不说,一个很简单的例子就能说明问题:当你的鼠标划过一个窗口时,这个窗口的消息队列就会接受到成百条鼠标位置的信息,如果这个时候另外几个模块正在通过Window消息队列频繁的通讯的话,这个队列就会更长,导致所有的消息都得不到及时处理,这个时候的现象就是程序的窗口响应迟钝同时后台计算也变慢...可见将图形界面的消息和内部运算的消息混在一起的话,两者的处理都会受到影响。
* 回到刚才说的,所有线程都进入自己的消息循环,程序正式开始工作:
用户的窗体操作被包装成为高级的应用请求消息,传递到管理线程;
管理线程的程序将高级请求转换成为一系列的模块功能请求消息,然后分发到各个模块;
(对于有顺序要求的消息,状态机也好,跨模块调用(IMC)也好,根据个人的风格和期望的效果实现,CAS是一个非常自由的架构! :D)
根据各个模块反馈的结果,管理线程组装一个UI反馈消息,传递给UI线程,然后由其将消息解释成为图形界面的状态,反馈给用户。
* 最后,程序即将结束,管理线程首先通知各个模块队列取下,接下来依次注销消息队列,然后卸载各个功能模块,总线模块,最后返回;
而主线程则一直等待线程池将所有线程回收,最后清理资源,终止程序。
注意图中有几条虚线,他们表示的意思是,UI线程和管理线程是作为功能模块挂接到总线上的,但是实际上他们并没有一个单独的DLL文件,因此称为"伪模块"(Pseudo-Module)。
级别2的CAS总线(管理总线)一般应用在大型的服务程序中。因为其本身具有一定的管理能力,因此CAS服务程序的(建议)组装结构应该像下图:
(图2) CAS服务程序
* 一般情况下,CAS消息总线将会和主程序形成一个模块;
主线程创建后,首先创建消息总线(同前一种程序一样,可以创建多个,根据用途而定)
然后,作为启动(Bootstrap)过程,主线程试图载入预先配置好的一个模块管理模块,注册并挂接;
接下来,根据需要,主线程可以注册一个伪模块,以便和总线交互。(图中虚线省略了)
* 挂接后,具有管理功能的总线将向模块发出通知消息,模块管理模块捕获这个消息,并开始工作;
模块管理模块可按照预设定或者某种特定顺序载入各个模块,每个模块独自捕获总线载入通知开始动作。
注册/注销等消息队列管理操作都将以请求的方式发送给总线,由总线管理线程执行,并且消息队列取下的通知也由总线发出,各个模块应该独立相应。
因此模块管理模块需要做的操作仅仅是载入,通知挂接,和卸载模块。
* 一般情况下,主线程没有其他事情可做,只需要等待总线的"结束程序"消息(CAS消息或者同步事件);
然后,首先卸载模块管理模块(在此之前,总线已经通知其队列取下),接下来释放资源,结束程序。
如果需要用户界面,可以将其单独包装在一个模块中予以加载。
可以看得出来,服务程序的流程更加结构化;当然,这主要是因为总线实现了许多附加的功能。
------
现在,坚持跟进的各位应该对CAS架构有了一个初步的了解。如果各位还有什么不清楚的地方,欢迎回复反馈,我在合适的时候将集中解答。
最近正在对底层消息的实现作比较大的改变,主要是软件工程上的,比如统一总线内部对各种消息的处理等;
其中包括前面提到的,将总线对于IMC(跨模块调用)消息的支持通用化,使得基于CAS的开发者能够写出自定义的IMC消息。
当然,这些更改对于前面介绍的抽象概念和高层操作基本没有影响,请各位放心消化。 :P