理解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的擁護者.)
   爲了使應用能夠解釋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對象的方法:

    在文獻1中提到上面方法的語境(context)如下表. 這個語境感覺好象是專門用來顯示的指出對象方法的作用域. --- 如果是這樣的話, OTcl的名字空間管理好象有些問題.



    $self類似於C++類中的this指針, $proc給出方法名, $next是指父類的同名方法, 就是C++中的函數重載, 這關係到OTcl對象的多繼承機制.

OTcl的語法的進一步解釋

    在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.0a12
下面是實現代碼, 構造函數直接以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基礎上的封裝,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
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章