天天看點

了解NS2中的OTcl/tclCL(zt)了解NS2中的OTcl/tclCL

了解NS2中的OTcl/tclCL

題記

真正觸動我寫這篇短文的原因是試圖了解NS2的基本原理. 在"the NS2 manual"中, 解釋了為什麼采用了兩種語言來建構整個系統, 然後在第三章描述了tclcl的六個類. 這個手冊中的對各個類描述性文字讓我如墜霧裡, 不明是以. 我查找了一些NS2的文章和站點, 有一些ppt倒是很形象, 但我的認識上總有些模糊. 後來, 我逐漸明白到OTcl/Tcl的嵌入特性. --- 這才是了解NS2架構的關鍵.

Abstract

本文的主要目的是了解NS2的architecture, 了解NS2的基本原理. NS2采用了Tcl/C++分裂的模型, 在這種模型中OTcl是處于比較關鍵的位置, NS2采用了Tcl的程式設計模式. 使用C++來編寫應用執行個體, 使用OTcl來操縱這些執行個體. 了解了OTcl就了解了NS2的架構. 本文先簡述Tcl語言的嵌入特性, 然後描述了NS2的應用場景, 進而分析NS2的架構, 以及實作該架構采用的技術.

Introduction

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的仿真開發過程.

嵌入的Tcl

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庫的一些函數接口如下:

  • Tcl_CreateInterp 建立Tcl的Parser;
  • Tcl_CreateCommand/Tcl_CreateObjCommand 注冊應用相關的指令;
  • Tcl_Eval 執行一條Tcl指令;
  • Tcl_LinkVar 将應用程式中的變量與Tcl庫環境中的變量綁定;
  • Tcl_GetVar/Tcl_SetVar 設定/擷取Tcl庫環境中的變量;

下面是一個取自文獻2的例子:

/*      
* Example 47-1      
* The initialization procedure for a loadable package.      
*/      
/*      
* random.c      
*/      
#include <tcl.h>      
/*      
* 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的實作, 我們在後面談.

OTcl的文法設計

這一部分的内容主要取自文獻1, 放在這裡的目的有兩個: 一是為了了解OTcl的面向對象擴充, 進而能夠友善的了解一些NS2的代碼; 二是了解OTcl的實作原理.

文獻1中主要介紹的是對Tcl語言本身的面向對象擴充, 它沒有講述實作如何操縱一個C++的對象. 從代碼上來看OTcl本身好象并沒有實作對C++對象的操縱.

OTcl和tclCL的相關文獻都比較少, 這也許就是對NS2诟病較多的原因. OTcl是MIT的一個流媒體項目VuSystem的輔産品, 它并不是最早提出的Object概念的Tcl, 文獻1認為OTcl的特點是Dynamic, 能夠動态地建立一個對象.

OTcl的文法擴充

對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的文法的進一步解釋45678

在OTcl中, 類(Class)和對象(objects)是區分開來的. 類表示一種類型, 對象是類的執行個體. 在OTcl中, 類可以看成是一種特殊的對象. 類标志對象的類名, 類中可以放置所有對象共享的變量, 類似于C++中的靜态變量.

OTcl的基類稱為Object, 注意它可不是前面所說的對象. 所有的類都是從Object派生而來.

OTcl的屬性都是C++意義上的public的.

instproc用來定義類的方法, 而proc用來定義對象方法. 後者定義的方法隻能用于該對象.

unset用來undefine一個變量.

OTcl中, 類的instproc函數init相當于C++中的構造函數.

superclass用于繼承.

OTcl的繼承機制

文獻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給出了一個簡潔的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中尋找我們感興趣的内容.

NS2的應用場景與設計

有了上面對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++對象, 這包括:

  1. 動态建立一個新的C++對象,
  2. 通路這個C++對象的屬性,
  3. 調用該C++對象的方法.

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個問題解決.

通路C++對象的屬性

下一個問題是要從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對象的屬性進行通路.

  • 對C++對象非static屬性的通路

現在來考慮一下實作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++對象static屬性的通路

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++編譯器卻照樣編譯通過, 有意思.

調用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 <streams.h>        /* 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;
          
  • 暴露C++對象方法

為了能夠從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? <agentAddress>
          

另一種是隐式的調用:

        $srmObject distance? <agentAddress>
          

在隐式調用方式下, Tcl的Parser先檢查有OTcl對象沒有distance?這樣的指令, 這裡顯然沒有. 然後Parser會把解釋權傳遞給OTcl對象的unknown{}函數, unknown{}函數會使用上面的顯示調用的方式來調用C++對象的command()函數.

  • 在OTcl對象中實作指令的例子

雖然在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

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手頭必備的工具應該詳細研讀.

Class Tcl

Tcl類提供如下的方法

  • 擷取Tcl執行個體句柄

Tcl類隻有一個執行個體, 該執行個體在NS2啟動時初始化. 擷取Tcl類的執行個體方法是:

        Tcl& tcl = Tcl::instance();
          
  • 解析執行Tcl腳本指令
  • 傳回結果給Parser, 設定Tcl的環境變量

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這個錯誤, 然後打出出錯的調用棧桢; 後者則不行.

  • 存儲并搜尋TclObject對象

Tcl類中有一個C++對象的hash表, hash鍵是對象名. 這裡值得注意的是C++對象的hash表而不是OTcl對象的hash表. 有些奇怪, 難道OTcl對象的hash表在OTcl子產品中?

  • 插入定制的Parser

tcl.interp(void)函數是tcl的Parser句柄, 可以修改它, 加入定制的Parser.

Class TclObject

在文獻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中做.

  • 變量跟蹤
  • 指令方法

Class TclClass

Class TclCommand

Class EmbeddedTcl

Class InstVar

兩個例子

本來打算寫一兩完整的例子, 展示如何向NS2中添加一個新的子產品, 但我發現網上有兩個比較好的資源可以借用910, 這裡就免了.

結束語

這篇文章的後面兩節實在沒時間寫下去了, 就省略, 省略, 再省略, ...

等我以後有時間在續吧.

References

[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