真正触动我写这篇短文的原因是试图理解NS2的基本原理. 在"the NS2 manual"中, 解释了为什么采用了两种语言来建构整个系统, 然后在第三章描述了tclcl的六个类. 这个手册中的对各个类描述性文字让我如坠雾里, 不明所以. 我查找了一些NS2的文章和站点, 有一些ppt倒是很形象, 但我的认识上总有些模糊. 后来, 我逐渐明白到OTcl/Tcl的嵌入特性. --- 这才是理解NS2框架的关键.
本文的主要目的是理解NS2的architecture, 了解NS2的基本原理. NS2采用了Tcl/C++分裂的模型, 在这种模型中OTcl是处于比较关键的位置, NS2采用了Tcl的编程模式. 使用C++来编写应用实例, 使用OTcl来操纵这些实例. 理解了OTcl就理解了NS2的框架. 本文先简述Tcl语言的嵌入特性, 然后描述了NS2的应用场景, 进而分析NS2的架构, 以及实现该架构采用的技术.
NS2是MIT的一个作品, 它是一个面向对象的网络仿真工具. 使用NS2可以完整的仿真整个网络环境, 只要你的机器足够快 :-) NS2使用一整套C++类库实现了大多数常见的网络协议以及链路层的模型, 使用这些类的实例我们就可以搭建起整个网络的模型, 而且包括了各个细节. --- 这简直就是一种梦想的实现, 试想如果手头能有这样一个工具, 我们就可以在单机环境中模拟网络的各个元素, 加深对网络的了解和认识; 同时, 加快我们开发新协议的速度.
与NS2类似的软件有OPNET, 这是一个商用的网络仿真软件, 据说它能够针对各款交换机和路由器来搭建网络, 够牛x. 与之相比, NS2是一个免费的软件, 它可以在Windows/Unix上运行, 我们可以看到NS2的所有源代码, 另外在学术界更多的是采用NS2来做仿真.
NS2采用了Tcl/C++分裂的模型来建构它的应用, 这样做的好处是显而易见的. 使用Tcl语言我们可以灵活的配置网络环境, 定制系统; 而采用C++来编程满足了我们对仿真效率的需要. 缺点也是明了的, 要同时维护两套代码, 对使用者要求较高.
NS2的Tcl/C++架构与Windows下的COM/VBScript编程模式有些类似, 使用VC来编写和实现COM对象, 然后使用VB来操纵COM对象. Windows提供了COM接口, 这就在系统范围内保证了这种机制的有效性. --- 这就是Windows的高明之处. 与之相比, NS2则能够使Tcl脚本解到它的C++类库结构, 同时按照它的类分级来创建对象. --- 这也很了不起.
要使用NS2来仿真开发一个新协议, 就必须对NS2的类库进行某些扩展. 撇开各个协议或链路的细节不谈, 对NS2的实现机制的了解是一个关键, 否则难免会疏漏百出. 如果不了解NS2的机制, 在刚开始开发协议时, 你看着NS2的代码可能会感觉到无处下手.
NS2的手册中对它的机制和原理主要在"the NS manual"一书的第三章, 但是这一章的内容写来就象tclcl类的简单介绍, 读来非常费解. 它对NS2的整体设计思路并没有交待的很清楚, 本文就打算解析这第三章背后的话.
以下的内容安排如下, 首先简单介绍Tcl语言的嵌入特性, 然后描述NS2的应用场景, 分析NS2的框架, 然后考察实现这个NS2框架所遇到的问题. 最后以一个新协议的添加作为例子, 感受NS2的仿真开发过程.
OTcl称为Objected Tcl, 它是在Tcl基础上的一个面向对象的封装. Tcl语言本身比较简单, 在NS2的主页上有一些相关的链接, 可以快速了解它的基本语法, 这里对Tcl的语法并不感兴趣.
Tcl是一种脚本语言, Tool Cammand Language. Tcl语言的吸引人之处在于它简单同时支持嵌入式应用. 大家都用过Ms Word或Emacs, 这两种编辑器之所以如此强大, 很大的原因是因为它们内嵌了VB或Lisp语言. 这些内嵌的脚本能够定制编译器的环境, 使应用变的非常灵活.
你可能还听说过Windows下的脚本引擎, 把一个脚本引擎嵌入到Windows应用中, 就可以使这个应用具有类似Word的这种能力. 在Unix下类似的脚本语言嵌入很早就有, 上面提到的Emacs就是一个例子. Tcl语言也支持内嵌, 具体做法是把Tcl的库链接到应用程序中去, 从而使应用具有解释Tcl语言的能力. 当然仅仅这么做是不够的, 你还要为这个应用定制开发一些Tcl命令.
(NS2为什么不使用Lisp作为内嵌? 这可能与OTcl语言本身也是MIT的作品有关, 而且是MIT的近期作品. 但是如果采用Lisp的话, 就可以在Emacs做这个仿真了, 呵呵, 想想都要偷笑了, 可惜呀. 毕竟我是个Emacs的拥护者.)
Figure 1是Tcl C与应用程序集成的原理图. Figure 1中黑线表示C的调用, 红线表示Tcl脚本的调用. 为了使应用能够解释Tcl语言, 必须在应用程序的源代码中嵌入Tcl C库, 这个库的主要目的是实现一个Tcl语言的Parser, 并且实现了Tcl语言的一些关键命令, 如set, while, proc等. 应用程序必须还要编写一些针对应用扩展的Tcl命令, 然后注册进Tcl C库, 同时, 应用程序可以使用Tcl_LinkVar使某些C变量与Tcl库中的环境变量绑定起来.
Tcl C库的一些函数接口如下:
下面是一个取自文献2的例子:
/*
* Example 47-1
* The initialization procedure for a loadable package.
*/
/*
* random.c
*/
#include
/*
* Declarations for application-specific command procedures
*/
int RandomCmd(ClientData clientData,
Tcl_Interp *interp,
int argc, char *argv[]);
int RandomObjCmd(ClientData clientData,
Tcl_Interp *interp,
int objc, Tcl_Obj *CONST objv[]);
/*
* Random_Init is called when the package is loaded.
*/
int Random_Init(Tcl_Interp *interp) {
/*
* Initialize the stub table interface, which is
* described in Chapter 46.
*/
if (Tcl_InitStubs(interp, "8.1", 0) == NULL) {
return TCL_ERROR;
}
/*
* Register two variations of random.
* The orandom command uses the object interface.
*/
Tcl_CreateCommand(interp, "random", RandomCmd,
(ClientData)NULL, (Tcl_CmdDeleteProc *)NULL);
Tcl_CreateObjCommand(interp, "orandom", RandomObjCmd,
(ClientData)NULL, (Tcl_CmdDeleteProc *)NULL);
/*
* Declare that we implement the random package
* so scripts that do "package require random"
* can load the library automatically.
*/
Tcl_PkgProvide(interp, "random", "1.1");
return TCL_OK;
}
这个例子完整的体现了如何初始化Tcl C库, 如何向Tcl库中注册命令. 这里不打算继续讨论Tcl C库的详细问题了. 毕竟, 我们的目的是为了理解Tcl的嵌入能力, 上面的原理图已经足够.
NS2采用的是OTcl来实现它的脚本语言内嵌, 原因是NS2还有一套C++类库. 这个C++类库实现了网络仿真的各个元素, 仅仅是Tcl来操纵这套类库有些困难. 这关系到NS2的实现, 我们在后面谈.
这一部分的内容主要取自文献1, 放在这里的目的有两个: 一是为了了解OTcl的面向对象扩展, 从而能够方便的理解一些NS2的代码; 二是理解OTcl的实现原理.
文献1中主要介绍的是对Tcl语言本身的面向对象扩展, 它没有讲述实现如何操纵一个C++的对象. 从代码上来看OTcl本身好象并没有实现对C++对象的操纵.
OTcl和tclCL的相关文献都比较少, 这也许就是对NS2诟病较多的原因. OTcl是MIT的一个流媒体项目VuSystem的辅产品, 它并不是最早提出的Object概念的Tcl, 文献1认为OTcl的特点是Dynamic, 能够动态地创建一个对象.
对OTcl的语法描述, 更详细的见网页45678.
OTcl的语言设计采用了称为"Object Command"的方法. 每个命令可能被解释成对象, 而子命令被解释成传递给对象的消息. 文献1中认为这样做可以比较方便的实现Tk的消息机制达到某种一致, 同时对象作为Tcl语言中array, list, proc等要素的补充, 这种扩展显得比较自然.
下面是OTcl一个一段示例代码, 我们可以看到对象astack的子命令set, proc等作为消息传递给对象. 而且要注意它确实是动态的, 在代码解释过程中动态的添加属性和方法.
Object astack
astack set things {}
astack proc put {thing} {
$self instvar things
set things [concat [list $thing] $things]
return $thing
}
astack proc get {} {
$self instvar things
set top [lindex $things 0]
set things [lrang $things 1 end]
return $top
}
astack put toast ==> toast
astack get ==> toast
astack destroy ==> {}
注意上面的instvar方法, 代码的前面用set定义了一个things的变量, 在方法内要操纵它必须使用instvar来声明, 有些怪, 是不是? 否则things将是一个方法内的局部变量. 下表是OTcl对象的方法:
name |
description |
class |
创建一个对象 |
destroy |
销毁一个对象 |
proc |
定义Tcl对象方法 |
set |
定义Tcl对象变量 |
instvar |
绑定实例变量 |
info procs |
列出Tcl对象的所有方法 |
info args |
列出Tcl对象方法的参数格式 |
info body |
列出Tcl对象方法的函数主体 |
info commands |
列出Tcl/C的命令 |
info vars |
列出Tcl变量 |
inof class |
获取class名 |
在文献1中提到上面方法的语境(context)如下表. 这个语境感觉好象是专门用来显示的指出对象方法的作用域. --- 如果是这样的话, OTcl的名字空间管理好象有些问题.
name |
description |
type |
self |
对象名 |
变量 |
proc |
方法名 |
变量 |
class |
定义的类型方法 |
变量 |
next |
下一个影子方法 |
方法 |
$self类似于C++类中的this指针, $proc给出方法名, $next是指父类的同名方法, 就是C++中的函数重载, 这关系到OTcl对象的多继承机制.
在OTcl中, 类(Class)和对象(objects)是区分开来的. 类表示一种类型, 对象是类的实例. 在OTcl中, 类可以看成是一种特殊的对象. 类标志对象的类名, 类中可以放置所有对象共享的变量, 类似于C++中的静态变量.
OTcl的基类称为Object, 注意它可不是前面所说的对象. 所有的类都是从Object派生而来.
OTcl的属性都是C++意义上的public的.
instproc用来定义类的方法, 而proc用来定义对象方法. 后者定义的方法只能用于该对象.
unset用来undefine一个变量.
OTcl中, 类的instproc函数init相当于C++中的构造函数.
superclass用于继承.
文献1对OTcl的继承机制描述的很清晰. OTcl支持类的多继承, 唯一的要求是继承关系满足DAG(有向无环图). OTcl的类继承可以简单地通过下面这个例子来理解.
Class Safety
Safety instproc init {} {
$self next
$self set count 0
}
Safety instproc put {thing} {
$self instvar count
incr count
$self next $thing
}
Safety instproc get {} {
$self instvar count
if {$count == 0} then { return {empty!} }
incr count -1
$self next
}
Class Stack
Stack instproc init {} {
$self next
$self set things {}
}
Stack instproc put {thing} {
$self instvar things
set things [concat [list $thing] $things]
return $thing
}
Stack instproc get {} {
$self instvar things
set top [lindex $things 0]
set things [lrange $things 1 end]
return $top
}
Class SafeStack -superclass {Safety Stack}
SafeStack s
s put toast ==> toast
s get ==> toast
s get ==> empty!
s destroy ==> {}
上面的例子中, SafeStack从两个类派生而来Safety, Stack. OTcl使用"方法分发(Method Dispatch)"来描述如何从子类访问父类的重载方法. 如Figure 3所示.
从Figure 3可以了解到类是如何通过next方法来访问父类的重载方法的.
OTcl给出了一个简洁的C Api接口, 通过这个接口我们可以在应用中访问OTcl中的对象. 主要接口描述见7和otcl.h文件. 从这些接口我们可以了解到OTcl本身并没有提供操纵C++类的方法, 这留给了tclCL来完成这一工作.
检查OTcl的Makefile, 可以看到它有三个目标: libotcl.a, owish, otclsh. 后两个都是shell程序, libotcl.a是我们关心的, 它就是OTcl的库, 当它被链接到应用程序中后, 应用程序就有了OTcl脚本内嵌的功能.
libotcl.a: otcl.c
rm -f libotcl.a otcl.o
$(CC) -c $(CFLAGS) $(DEFINES) $(INCLUDES) otcl.c
ar cq libotcl.a otcl.o
$(RANLIB) libotcl.a
可以看到libotcl.a只由一个文件生成otcl.c, 这个c文件有两千多行, 好在otcl.h头文件很清晰. otcl.h一开始就声明了两个结构, 然后开始声明一些函数, 这些函数的名字很self meaning.
这里不打算继续分析otcl.c的源码了, 可以从otcl.h和OTcl的主页上了解一下C api的相关内容. 下面开始在tclcl中寻找我们感兴趣的内容.
有了上面对Tcl/OTcl的了解, 我们开始探询NS2仿真系统的设计原理. NS2的目标是仿真实际网络的各个元素, 对这些网络元素的描述有相关的资源可以获取. 如TCP协议的实现, 就可以从FreeBSD上获取, 由于TCP协议的实现版本不同, 就有了Reno发布等. 对链路的仿真, 主要是通过队列, 延迟等.
如何仿真这些网络元素是一回事, 对于NS2的系统架构是另一回事. 如此多的网络元素, 它们有可能功能重叠, 特定的仿真对象有不同的要求. 要满足这种灵活性, 一个可行的方案就是采用脚本定制仿真环境. 从而导致NS2有内嵌脚本语言的要求.
另一方面, 这些网络元素是分门别类的, 再有从实现效率上考虑, 使NS2采用了C++来对这些网络仿真对象建模. 这样就对应的要求脚本语言能够有与C++对象交互的能力. --- 这个要求可不低. 而MIT正好又有一种新开发的面向对象的脚本语言OTcl, 这些都促成了NS2采用OTcl.
NS2的应用场景是这样的: 用户在一个Tcl脚本中给出对仿真环境的描述, 键入ns xx.tcl启动NS2, NS2先完成一些初始化工作, 然后按照脚本的描述实例化各个仿真元素, 并把这些仿真元素连接起来, 在启动事件发生器, 触发网络元素动作, 中间有一些记录工作, 把这些仿真信息保存在磁盘上留待继续分析.
现在我们考虑这样一个系统如何设计. OTcl本身有对象的机制, Tcl脚本可以描述我们要仿真的对象. 但是我们遇到的问题是如何从OTcl来操纵NS2的C++对象, 这包括:
首先, 我们首先需要一种机制, 能够从一个描述类的字符串来动态的创建一个C++对象. 如果退到Tcl的方式, 我们就必须为每一个类写这样一种Tcl命令接口, 这不是一个好的解决方案, 它最大的问题是丧失了C++的继承关系. NS2的解决方案称为"Split Model", 它在OTcl上建立Tcl下的类, 与C++类分级保持一致, 每个C++类都对应一个OTcl的类, 但是问题还没有完全解决. 你在OTcl上初始化了一个对象, 必须要同时初始化一个对应的C++的对象.
为了解决这个问题, NS2在C++上使用了TclClass类来封装注册机制. 每个C++类都有一个对应的从TclClass派生而来的对象, 注意这里是对象, 是一个实例, NS2一启动就会实例化它. 该对象的主要目的是封装注册C++类的动态创建函数, 注册信息维护在一个hash表中, 该hash表是tclCL包的一部分, hash键是描述类的一个字符串计算而来.
接下来的问题是, 如何调用这个C++类的创建函数. NS2的方法很技巧, 前面所说的OTcl继承关系是一个关键, OTcl的对象的初始化函数都是init, 一个派生的OTcl对象首先是调用它的父类的init函数. 如前面的代码:
Stack instproc init {} {
$self next
$self set things {}
}
这样, 一个OTcl的初始化肯定要调用到OTcl的Object类的init, 如果这个Object能够在此时初始化这个C++对象将是再理想不过. 这样一来, C++对象的初始化就对用户来说不可见了, 他只看到的是一个OTcl对象被初始化. NS2使用了TclObject而不是Object来派生所有的OTcl对象, 对应的, C++仿真对象也从一个C++的TclObject类派生. 由OTcl的根类TclObject来搜索C++类名hash表, 完成C++对象的创建工作. 还有一个小问题, init必须带入C++类名的字符串作为参数, 否则就没法查hash表了.
OTcl对象/C++对象的删除也是类似的道理. 至此, 第1个问题解决.
下一个问题是要从OTcl上操纵C++对象的属性. OTcl对象的属性并不一定要完全照抄C++对象的属性, OTcl对象属性的设计原则是能够方便的完成对C++属性的设置即可. 所以一般来说, OTcl对象属性集合要小于C++对象属性集合.
在分析如何访问C++对象属性之前, 先澄清一些OTcl名字空间的概念. OTcl是一个脚本程序, 它传统的继承了Unix下环境变量的概念, 变量的名字空间是扁平的. 而引入了对象机制后, 名字空间就有些复杂了. 显然一个OTcl对象的属性的名字与环境变量下的名字有可能重叠. 在OTcl中, 这称为名字的"context"语境.
对比C++对象的名字, 它是确定的, 这是因为有编译器的帮助, 编译器在编译一个C++源代码的时候它可以根据上下文来判断这里变量指的是什么. 而在OTcl的环境中, 也需要类似的机制, 由OTcl的Parser动态的确定一个名字的含义.
显然要确定一个名字的含义, 可以通过对象名来帮助作到这一点. 在OTcl中还有一个对象的hash表, 对象创建后要注册到这个hash表中. 对一个名字解释, 首先是要搜索对象hash表, 再搜索该对象的class及其父类, 参照前面的next指针.
在下面的代码中, $self instvar count这条语句就是切换context的. 如果不切换context, 我们将不知道是环境变量名还是其他的对象的属性.
Safety instproc put {thing} {
$self instvar count
incr count
$self next $thing
}
对一个C++对象属性的访问, 有读/写两种操作. 由于存在OTcl/C++两个对象, 有可能它们的属性并不一致. 但是要注意到OTcl的对象属性是给用户看的, 只要保证在读/写OTcl对象属性的时候能够作到与C++对象一致就行了, 完全保持二者的一致性是没有必要的. 有了这样的要求, 下面的方式才是可行的.
NS2采用了一种trap机制来捕获对OTcl对象属性的访问. 具体来说, 在tclCL中是以InstVar类对象来封装这种Trap机制. Trap的位置安装在语境切换的时刻, 因为只有在语境切换后, 才有可能对该OTcl对象的属性进行访问.
现在来考虑一下实现trap或者说C++对象属性绑定的细节问题. 首先, 绑定的是一个C++对象的属性(对绑定一个C++类的static属性问题在后面谈), 这意味着要知道该C++对象属性的位置, 所以C++对象属性的位置是绑定的一个必要条件.
其次, 如何设计这个Trap? 假设我们要对某个OTcl的变量进行写操作, 一般的操作是利用Tcl的Parser直接写, 但是这里我们要保持与某个位置上的信息同步, 就还需要向这个位置写相应的信息. 要解决问题, 可以修改Tcl的Parser, 让它写同步位置即可. --- 这就是InstVar的思路, 我们可以在语境切换的时刻安装一个定制的Parser, 让这个定制的Parser来向C++对象属性的位置写, 问题就解决了.
剩下的问题是, 何时安装这样一个定制Parser? 原则上任何时候都是可以的, 只要你知道这个C++对象属性的位置. 但是一个方便的做法是在该C++对象构造函数内做. 当调用这个binding函数的时候, 它会在OTcl对象属性位置上做一个标记, 表示有绑定的属性. 当进行语境切换的时候, 如果在该OTcl对象的属性位置上有绑定标志, 则OTcl动态安装定制的Parser, 这个定制的Parser就是tclCL的InstVar的一个成员函数.
由于OTcl是脚本语言, 是若类型的语言, 它用字符串表示所有的变量, 只有当它在eval的时候才会知道它具体是char/int/real等. 所有InstVar有几个派生类, 原因是这个定制的Parser要向C++属性写不同类型的信息.
给个例子
ASRMAgent::ASRMAgent() {
bind("pdistance_", &pdistance_); /* real variable */
bind("requestor_", &requestor_); /* integer variable */
bind_time("lastSent_", &lastSessSent_); /* time variable */
bind_bw("ctrlLimit_", &ctrlBWLimit_); /* bandwidth variable */
bind_bool("running_", &running_); /* boolean variable */
}
C++对象的静态属性是放在局部堆上的, 而且是该类的所有对象共享的. 如果在C++对象的构造函数中绑定这个静态属性, 显然有效率上的问题, 在同时存在多个该类的C++对象时, 就会多次绑定. 更严重的是, 它绑定到的对应的是OTcl对象上, 这样在OTcl对象上该static属性就不同一了.
注意到NS2启动时, C++类对应的TclClass把C++类要注册到OTcl的类hash表上, 这个时刻是做对C++类工作的绝好机会.
分析OTcl的类/对象机制, 可以看到OTcl无法象C++对象一样有静态变量, 这算是OTcl设计的一个缺陷? 用OTcl类初始化一个OTcl对象, 意味着完整拷贝OTcl的信息. 对次, NS2采用了一种变通的方法. 它首先在OTcl类上添加了一个属性, 以后用该类初始化OTcl对象时, 所有对象都有这样一个属性. 然后在该属性上注册一个定制的Parser到这个属性上, 这个Parser直接访问了C++对象静态变量. ---这样就在OTcl对象上作出了一个静态变量. 见下面的从文献3中摘录的代码.
假设C++类的static变量为
class Packet {
......
static int hdrlen_;
};
Packet的TclClass中定义如下:
class PacketHeaderClass : public TclClass {
protected:
PacketHeaderClass(const char* classname, int hdrsize);
TclObject* create(int argc, const char*const* argv);
/* These two implements OTcl class access methods */
virtual void bind();
virtual int method(int argc, const char*const* argv);
};
void PacketHeaderClass::bind()
{
/* Call to base class bind() must precede add_method() */
TclClass::bind();
add_method("hdrlen");
}
int PacketHeaderClass::method(int ac, const char*const* av)
{
Tcl& tcl = Tcl::instance();
/* Notice this argument translation; we can then handle them as if in TclObject::command() */
int argc = ac - 2;
const char*const* argv = av + 2;
if (argc == 2) {
if (strcmp(argv[1], "hdrlen") == 0) {
tcl.resultf("%d", Packet::hdrlen_);
return (TCL_OK);
}
} else if (argc == 3) {
if (strcmp(argv[1], "hdrlen") == 0) {
Packet::hdrlen_ = atoi(argv[2]);
return (TCL_OK);
}
}
return TclClass::method(ac, av);
}
OTcl脚本如下, 这个脚本模拟了读写两种操作.
PacketHeader hdrlen 120
set i [PacketHeader hdrlen]
定制Parser在上面的代码中体现无疑.
上面在对C++对象非static属性访问的代码中bind函数值得回味. 它把一个private/protected的属性给暴露出去了, 而C++编译器却照样编译通过, 有意思.
在NS2中, 很少有OTcl本身再实现一个对象的方法的, 因为从效率的角度考虑, 这样做会得不偿失. 一般的情况都是直接调用C++对象的方法来处理. 从实现上来看, 要调用一个对象的方法并不困难, 只要在合适的语境中, 给出参数直接调用就可以了. 所以NS2中实现对C++对象的方法的引用至多也就是定制Tcl的Parser.
OTcl继承了Tcl的某些特性, 可以通过TclCommand类定制一个顶级命令注册到OTcl的Parser中, 不过这种方法不值得推荐. 下面的例子3给出了实现方法:
要在OTcl中注册的顶级命令是hi:
% hi this is ns [ns-version]
hello world, this is ns
2.0a
12
下面是实现代码, 构造函数直接以TclCommand的构造函数注册"hi"命令, command()函数是命令的实现部分.
class say_hello : public TclCommand {
public:
say_hello();
int command(int argc, const char*const* argv);
};
say_hello() : TclCommand("hi") {}
#include /* because we are using stream I/O */
int say_hello::command(int argc, const char*const* argv) {
cout << "hello world:";
for (int i = 1; i < argc; i++)
cout << ' ' << argv;
cout << '/bs n';
return TCL_OK;
}
然后在NS2的init_misc(void)初始化函数中实例化该类.
new say_hello;
为了能够从OTcl对象调用C++对象, OTcl使用了一种固定的路线来完成这一工作. 首先, 所有OTcl的TclObject派生类都有一个方法cmd{}, 它作为一个hook来勾住从C++对象注册的command()函数. 除此外, 每个OTcl对象还有一个unknown{}的函数. 要注意OTcl对象的cmd{}与C++对象的command()都是约定好的.
现在的问题是, C++对象的方法是注册到OTcl对象上还是OTcl的类上? 更进一步的问题是, 如何注册父类的command()函数? 再有, 注册是在OTcl对象的初始化中做, 还是在TclClass中做?
要回答这个问题, 下面我们先看tclcl.h中关于command()的定义.
class TclObject {
public:
virtual ~TclObject();
inline static TclObject* lookup(const char* name) {
return (Tcl::instance().lookup(name));
}
inline const char* name() { return (name_); }
void name(const char*);
/*XXX -> method?*/
virtual int command(int argc, const char*const* argv);
virtual void trace(TracedVar*);
...
可以看到, command()是一个虚函数, 这么做的目的是为了保证所有的command()函数都一样. 再看看3中给的一个例子:
int ASRMAgent::command(int argc, const char*const*argv) {
Tcl& tcl = Tcl::instance();
if (argc == 3) {
if (strcmp(argv[1], "distance?") == 0) {
int sender = atoi(argv[2]);
SRMinfo* sp = get_state(sender);
tcl.tesultf("%f", sp->distance_);
return TCL_OK;
}
}
return (SRMAgent::command(argc, argv));
}
注意到最后一行return (SRMAgent::command(argc, argv)); --- 问题清楚了, command()函数是在C++对象一侧上溯到它的父类的, 这样做显然效率要高一些. 因此, ccommand()应该是在C++对象初始化的时候, 被注册到OTcl对象的cmd{}上.
最后交待一下OTcl如何调用cmd{}. 有两种方式, 一种是显示的调用cmd{}
$srmObject cmd distance?
另一种是隐式的调用:
$srmObject distance?
在隐式调用方式下, Tcl的Parser先检查有OTcl对象没有distance?这样的命令, 这里显然没有. 然后Parser会把解释权传递给OTcl对象的unknown{}函数, unknown{}函数会使用上面的显示调用的方式来调用C++对象的command()函数.
虽然在OTcl对象中实现方法的例子不常见, 文献3还是给出了一个例子:
Agent/SRM/Adaptive instproc distance? addr {
$self instvar distanceCache_
if ![info exists distanceCache_($addr)] {
set distanceCache_($addr) [$self cmd distance? $addr]
}
set distanceCache_($addr)
}
这个例子说明了在OTcl对象中中重载C++对象的命令.
到现在为止, 我们已经大致了解了NS2的基本架构和设计原理. 还有一些细节问题留到下一节对tclCL模块的类说明.
tclCL是在OTcl基础上的封装, 它们与Tcl之间的关系如图Figure 2. tclCL实际上搭建了NS2的框架, NS2的类库都是建立在tclCL基础上的. 在文献3第三章简单介绍了tclCL的六个类: Tcl, TclObject, TclClass, TclCommand, EmbeddedTcl, InstVar.
其中, Tcl类可以看成是一个Tcl的C++接口类, 它提供C++访问Tcl库的接口. TclObject是Tcl/C++两个面向对象语言的类库的基类, 在最新的tclcl中, 采用了SplitObject的术语. TclClass注册编译分级, 保持了编译分级的层次结构, 同时给OTcl对象提供了创建C++对象的方法. TclCommand用于定义简单的全局解释命令. EmbeddedTcl是定制的Tcl命令. InstVar类包含了从Tcl访问C++类成员变量的方法.
这里不打算对tclcl的六个类再详细介绍了, 这篇文章到此为止已经基本达到它的目的. 下面的内容只挑选我认为是容易遗漏的地方. 文献3作为开发NS2手头必备的工具应该详细研读.
Tcl类提供如下的方法
Tcl类只有一个实例, 该实例在NS2启动时初始化. 获取Tcl类的实例方法是:
Tcl& tcl = Tcl::instance();
Tcl类的结果是指tcl_->result, 与脚本执行的退出码不同. 如下的代码
if (strcmp(argv[1], "now") == 0) {
tcl.resultf("%
.17g
", clock());
return TCL_OK;
}
tcl.result("Invalid operation specified");
return TCL_ERROR;
有两种错误退出方式, 一种是返回TCL_ERROR的退出码, 另一种是以tcl.error()函数退出, 两者稍有区别. 前者可以在解释器中trap这个错误, 然后打出出错的调用栈桢; 后者则不行.
Tcl类中有一个C++对象的hash表, hash键是对象名. 这里值得注意的是C++对象的hash表而不是OTcl对象的hash表. 有些奇怪, 难道OTcl对象的hash表在OTcl模块中?
tcl.interp(void)函数是tcl的Parser句柄, 可以修改它, 加入定制的Parser.
在文献3中, OTcl对象称为解释分级对象, C++对象称为编译分级对象. TclObject并不包括Simulator, Node, Link, rtObject等对象. 对用户来说, 如果只使用Tcl配置脚本的话, 一般只看得到OTcl对象的创建, 而C++对象的创建正如我们前面分析的, 对用户是不可见的.
OTcl对象包括TclObject, Simulator等都使用在~/tclcl/tcl-object.tcl文件中定义的new{}和delete{}函数来初始化和销毁. 这一部分的内容前面已经分析的比较清楚.
每个OTcl对象在创建的时候会获取一个id返回给用户. OTcl的基类TclObject使用create-shadow{}函数来创建C++对象. 在C++对象的构造函数中一般都会调用属性绑定函数, 进行C++/OTcl属性绑定. C++对象创建后, 被插入到Tcl类对象的hash表中. 然后用C++对象的command()函数在OTcl对象中注册cmd{}函数.
下面的这张图比较清楚的反映了对象创建的过程.
OTcl中TclObject对象的函数create-shadow{}是TclClass注册的一个命令. 虚线表示C++编译器自动的调用父类的初始化函数.
OTcl/C++对象的属性初始化在~ns/tcl/lib/ns-default.tcl中做.
本来打算写一两完整的例子, 展示如何向NS2中添加一个新的模块, 但我发现网上有两个比较好的资源可以借用910, 这里就免了.
这篇文章的后面两节实在没时间写下去了, 就省略, 省略, 再省略, ...
等我以后有时间在续吧.
[1] Extending Tcl for Dynamic Object-Oriented Programming
[2] Practical Programming in Tcl and Tk
[3] The ns Manual
[4] http://bmrc.berkeley.edu/research/cmt/cmtdoc/otcl/tutorial.html
[5] http://bmrc.berkeley.edu/research/cmt/cmtdoc/otcl/object.html
[6] http://bmrc.berkeley.edu/research/cmt/cmtdoc/otcl/class.html
[7] http://bmrc.berkeley.edu/research/cmt/cmtdoc/otcl/capi.html
[8] http://bmrc.berkeley.edu/research/cmt/cmtdoc/otcl/autoload.html
[9] http://nile.wpi.edu/NS/linkage.html
[10] http://140.116.72.80/~smallko/ns2/module.htm