Manuale di Network Simulator v.2

Appendice 1 - Linguaggi Tcl e OTcl (cenni)

a cura di Sandro Petrizzelli


 

Introduzione

 

 

NS è un simulatore di reti basate su IP. La versione 2 di NS è stata progettata in modo che l’utilizzatore possa definire una simulazione usando un semplice linguaggio di script. In generale, il linguaggio di script fa in modo che vengano creati sia gli oggetti costituenti la simulazione sia i collegamenti esistenti tra di essi. Una volta definito, tramite sempre gli script, lo scenario della simulazione, quest’ultima può essere eseguita senza far ricorso al linguaggio di script (che è lento nell’esecuzione), bensì facendo ricorso al C++.

Il proposito di questo tutorial è quello di presentare alcuni aspetti di Tcl e OTcl.

 

 

Relazione tra Tcl/OTcl e ns

 

 

Tcl è un linguaggio di script che consente un semplice accesso ad un insieme di funzioni di libreria. OTcl è invece una estensione di Tcl, che fornisce le funzionalità necessarie per ottenere una programmazione orientata agli oggetti: consente perciò di definire classi OTcl e supporta metodi per gestire tali classi. La relazione tra Tcl e OTcl è dunque simile a quella esistente tra C e C++.

Esiste in NS un componente, chiamato TclCl, che viene usato per fornire un collegamento (linkage) tra le classi definite in C++ e le corrispondenti classi istanziate in OTcl.

NS può essere visto come una particolare shell di Tcl: questa include le aggiunte di OTcl, che consistono fondamentalmente nelle librerie dei componenti di rete (link, code, ecc.) che possono essere creati e manipolati. L’uso di OTcl abilita quindi alla definizione dei componenti di rete come classi OTcl.

La sintassi base del linguaggio Tcl è interamente applicabile negli script NS; in aggiunta, gli script NS possono anche seguire la sintassi OTcl quando si tratta di manipolare classi di tale linguaggio.

 

 

Costrutti base di Tcl

 

 

Variabili e vettori

 

La definizione di una variabile in Tcl è molto semplice:

 

set var1 1

set var2 "Tcl Variable 2"

 

Le variabili possono essere referenziate anteponendo, al loro nome, il simbolo $. Ad esempio, per stampare le variabili definite poco fa, possiamo usare la seguente sintassi:

 

puts "var1=$var1, var2=$var2"

 

In generale, in tutte le situazioni in cui è richiesto di utilizzare il valore di una data variabile è necessario usare il simbolo $. Ci sono del resto altre situazioni in cui è necessario usare solo il nome della variabile; ad esempio, l’istruzione incr var1 può essere usata per incrementare di uno la variabile var1.

Si hanno, dunque, chiamate per riferimento e chiamate per valore: nel primo caso si usa il simbolo $ davanti al nome della variabile, mentre nel secondo caso si usa direttamente il nome.

Un possibile modo per assegnare un valore ad una variabile è quello di assegnarle il risultato di una funzione. Ad esempio:

 

set var3 [expr 5*10]

 

Con questa istruzione, la variabile var3 assume il risultato della chiamata della funzione denominata expr, alla quale viene passato l’argomento 5´10. La funzione expr tenta di usare l’argomento fornitole per ottenere un conseguente valore da attribuire a var3.

Il Tcl interpreta le parentesi quadre come delimitatori per il comando al loro interno: esso esegue il comando all’interno delle parentesi quadre e assegna il risultato a var3 nel nostro esempio. In particolare, il risultato del nostro esempio è il valore numerico 50, che viene assegnato a var3.

In Tcl, tutte le variabili sono rappresentate internamente come stringhe. La decisione se una data stringa possa essere vista come un intero o come un numero a virgola mobile o altro è un problema che si pone solo quando si utilizza una funzione che richiede argomenti numerici.

Il Tcl supporta anche i vettori. Ad esempio, in NS i vettori sono molto utili per memorizzare l’elenco dei nodi di una data topologia. Il Tcl supporta vettori che possono essere indicizzati tramite semplici argomenti numerici, così come nella maggior parte degli altri linguaggi; tuttavia, Tcl supporta anche vettori che possono essere indicizzati tramite stringhe arbitrarie. Inoltre, non è necessario dichiarare in anticipo la dimensione di un vettore. Riportiamo due esempi di array:

 

set n(0) [$ns node]

set n(1) [$ns node]

 

set opts(bottlenecklinkrate) 1Mb

set opts(ECN) "on"

 

Nel primo esempio, il vettore è chiamato n e il suo indice è numerico. Nel secondo esempio, invece, il vettore è chiamato opts e il suo indice non è numerico.

 

 

Cicli FOR

 

Questo tipo di cicli sono molto utili in NS e possono essere usati congiuntamente ai vettori per creare facilmente grandi topologie di rete. Ad esempio, per generare una topologia con 100 nodi, si può usare il seguente codice:

 

for {set i 0}{$i < 100}{incr i} {

    set n($i) [$ns node]

}

 

Cicli WHILE

 

Questi cicli sono molto simili a quelli del precedente paragrafo. La sintassi è la seguente:

 

set i 0

while {$i < 10} {

    set n($i) [new Node]

    incr i

}

 

 

Istruzioni IF

 

Anche questo tipo di istruzioni è molto semplice:

 

if {$i < 10} {

    puts "i is less than 10"

}

if {$var2 == "Tcl Variable 2"} {

    puts "var2 = Tcl Variable 2"

}

 

 

Procedure

 

Le procedure sono un componente essenziale di Tcl e possono essere usati per facilitare la programmazione tramite questo linguaggio. Come in ogni linguaggio di programmazione funzionale, le procedure possono essere usate per compiti ripetitivi o semplicemente per distinguere logicamente i compiti basilari assolti da un programma.

Le procedure sono definite, in Tcl, nel modo seguente:

 

proc proc1 {} {

    puts "in procedure proc1"

}

 

Queste righe di codice definiscono una procedura che non prende in ingresso alcun parametro e si occupa semplicemente di stampare la frase “in procedure proc1”.

Per richiamare questa procedura basta scrivere:

 

proc1

 

Una procedura che invece richiede dei parametri in ingresso può essere definita nel modo seguente:

 

proc proc2 {parameter1} {

    puts "the value of parameter1 is $parameter1"

}

 

In questo caso, il parametro è uno solo. La chiamata della procedura deve allora essere effettuata nel modo seguente:

 

proc2 10

 

Una procedura che restituisce un risultato può invece essere definita nel modo seguente:

 

proc proc3 {min max} {

    set randomvar [rand $min $max]

    return $randomvar

}

 

Questa procedura genera un numero variabile e lo restituisce alla funzione chiamante. Quest’ultima invoca la procedura proc3 nel modo seguente:

 

set randomvar [proc3 0 1]

 

In questo modo, la procedura proc3 restituisce un valore casuale all’interno dell’intervallo [0,1], i cui estremi sono parametri passati a proc3 all’atto della chiamata.

A volte è necessario, all’interno di una procedura, referenziare una variabile che deve avere visibilità globale (ossia anche all’esterno di quella procedura). Questo lo si ottiene con la parola chiave global. Ad esempio, in uno script per NS, l’oggetto simulatore viene generalmente chiamato ns ed ha generalmente scopi globali; di conseguenza, esso può essere referenziato, all’interno di una procedura, nel modo seguente:

 

proc proc4 {} {

    global ns

    $ns at 10.0 "exit 0"

}

 

Un modo logico di suddividere uno script NS potrebbe essere il seguente:

 

set ns [new Simulator]

create_topology

create_agents

create_sources

create_recorders

$ns run

 

dove, evidentemente, create_topology, create_agents, create_sources and create_recorders sono tutte procedure costruite così come descritto prima.

 

 

Costrutti OTcl

 

 

Oggetti e classi in OTcl

 

OTcl ha delle parole chiave per registrare il nome di una classe e per definire una classe con essa imparentata (parent class). Si può infatti scrivere quanto segue:

 

class MyNode -superclass Node

 

Questo codice definisce una classe denominata MyNode, come classe derivata dalla classe base chiamata Node. Così come in C++, questo significa che ogni funzione membro di Node sarà disponibile anche in MyNode. Del resto, come già detto in precedenza, l’insieme dei dati membro di una classe non viene dichiarato in anticipo; al contrario, i dati membro di una classe possono essere creati solo quando ce n’è bisogno. La relazione padre/figlio tra due classi si applica perciò effettivamente all’insieme delle funzioni membro disponibili più che all’insieme dei dati membro.

Per creare un oggetto di classe MyNode, bisogna utilizzare la funzione new, nel modo seguente:

 

set mynodevar [new MyNode]

 

La variabile self può essere usata, all’interno delle funzioni membro di una classe, per riferirsi ad un particolare oggetto. Si tratta, perciò, dello strumento analoga al puntatore this usato in C++. In particolare, la variabile self deve essere usata per chiamare altre funzioni membro all’interno di una funzione membro.

 

 

Funzioni membro di una classe

 

Una funzione membro di una particolare classe può essere dichiarata nel modo seguente:

 

MyNode instproc memberfn1 {} {

    puts "in member function 1"

}

 

Questa funzione può poi essere richiamata usando il codice seguente:

 

set myobj [new MyNode]

$myobj memberfn1

 

 

Dati di una classe

 

I dati membro di una specifica classe sono definiti solo all’interno delle funzioni membro di quella classe. Questa costituisce una importante differenza rispetto al C++, in cui invece i dati membro di una classe sono specificati in anticipo, tipicamente nei file header.

I dati membro di una classe sono creati o referenziati usando la direttive instvar. Il seguente codice illustra l’uso dei dati membro di una classe:

 

MyNode instproc memberfn2 {} {

    $self instvar datamember1

    set datamember1 "data member - memberfn2"

}

MyNode instproc memberfn3 {} {

    $self instvar datamember1

    puts "datamember1 = $datamember1"

}

 

set myobj [new MyNode]

$myobj memberfn2

$myobj memberfn3

 

In questo esempio, un nuovo oggetto di classe MyNode viene creato e gli viene assegnato il nome myobj. Successivamente, per tale oggetto viene chiamata la funzione membro memberfn2: questa fa in modo da assegnare un valore al dato membro denominato datamember1. Successivamente, viene chiamata la funzione membro memberfn3: questa serve a stampare il valore della variabile datamember1, con l’accorgimento, però, che il valore stampato è quello assegnato dalla procedura memberfn2.

 

 

Inizializzazione degli oggetti

 

Ricordiamo che il C++ ha delle speciali funzioni costruttore che vengono chiamate all’atto della creazione degli oggetti di qualunque classe. Queste funzioni possono essere usate per inizializzare i dati membro degli oggetti che si stanno creando. In OTcl esiste la stessa funzionalità: infatti, anche qui viene usata una speciale funzione membro, con la differenza che, mentre in C++ il costruttore deve avere lo stesso nome della classe dell’oggetto da creare, in OTcl il costruttore si chiama init.

Ad esempio, la funzione di inizializzazione (o costruttore) per la classe OTcl denominata MyNode può essere definito nel modo seguente:

 

MyNode instproc init {} {

    $self next

    $self instvar datamember1

    set datamember1 0

}

 

La funzione di inizializzazione può anche ricevere in ingresso degli argomenti, che corrispondono a quelli forniti alla funzione new, nel modo seguente:

 

MyNode2 instproc init {arg1 arg2} {

    $self next

    $self instvar dm1 dm2

    set dm1 $arg1

    set dm2 $arg2

}

 

set mn2 [new MyNode2 1 2]

 

 

Linkage tra OTcl e C++

 

Quando gli oggetti OTcl vengono creati in NS, un oggetto C++ speculare viene generalmente istanziato a sua volta. Il C++ è il linguaggio usato nella simulazione, mentre invece OTcl fornisce una interfaccia tramite cui i parametri degli oggetti C++ possono essere manipolati (ad esempio per assegnare loro dei valori). Di conseguenza, esiste un meccanismo di linkage (collegamento) tra oggetti OTcl e corrispondenti oggetti C++.

 

 


Continua con:        Indice


Aggiornamento: 6 maggio 2001

home