Durch die im Laufe der Arbeit zunehmende Mächtigkeit der Skriptsprache CPL, hat sich gezeigt, daß die angedachten und teilweise realisierten Dienste viel flexibler mit CPL realisierbar sind. Deshalb soll hier nur noch der Skript-Service (iSrvScript.c) beschrieben werden.
Abbildung [pic-arch] zeigt die Server-Architektur, einschließlich des geplanten Tcl-Einsatzes (gestrichelte Linie). Die dick umrandeten Linien kennzeichnen die von mir implementierten Komponenten. Die Implementierung umfaßt im wesentlichen:
Der Laufzeitgewinn durch den Wegfall des Übersetzungsvorgang ist im Verhältnis zur Gesamtzeit für das Abarbeiten eines Skriptes jedoch so gering, daß er vernachlässigt werden kann. Um Skripte sowohl auf syntaktische Richtigkeit als auch deren Ablauf zu testen, gibt es das Programm cpldebug (siehe [cpldebug]).
So werden CPL-Skripte interpretiert, was beim geplanten Einsatz des Tcl-Interpreters ohnehin unumgänglich ist. Jedoch erfolgt die Interpretation nicht, wie in Kommando-Shells üblich, zeilenorientiert, sondern das CPL-Skript wird zunächst in einen Pseudo-Code überführt, der in einem zweiten Schritt abgearbeitet wird. Hierfür zerlegt ein Scanner die Zeichen der Skriptdatei in einen Symbol- oder Token-Strom (lexikalische Analyse). Dieser wird einem Parser zugeführt, der die Syntaxanalyse vornimmt (siehe auch [Aho88:Comp]). Nach bestandener Syntaxprüfung wird ein Pseudo-Code (P-Code) erzeugt, der anschließend von einem Interpreter ausgeführt (siehe [p-code-int]). Für den Übersetzungsvorgang wird die Skriptdatei nur einmal durchlaufen (1-Pass-Compiler).
Parser und Scanner sind in iSrvCplComp.c enthalten. Folgender Ausschnitt zeigt ein rudimentäres C-Programm, das das Skript test.cpl mit Hilfe der in iSrvCplComp.c exportierten Funktionen in P-Code überführt:
#include "iSrvCplComp.h"
main()
{
FILE* logf;
t_script* script;
char* fn = "test.cpl";
...
if (Cpl_NewScript (script, fn, logf) == CPL_ERROR)
exit (1);
if (Cpl_ParseMain (script) != CPL_OK)
exit (1);
...
}
Zunächst wird mit Cpl_NewScript() eine neue script-Struktur erzeugt. Außer dem Namen des Skriptes muß ein Pointer auf ein bereits geöffnetes Logfile übergeben werden. Der anschließende Aufruf von Cpl_ParseMain() öffnet die Skriptdatei und startet die Übersetzung. Im Falle von Fehlern werden diese auf stderr ausgegeben, und Cpl_ParseMain() terminiert mit CPL_ERROR.
Während des Übersetzungsvorganges wird eine Symboltabelle aufgebaut, auf die über die script-Struktur zugegriffen wird. Diese Tabelle enthält Einträge für Variablen, Zustandsnamen, Prozedurnamen und Konstanten und wird in Cpl_NewScript() mit den sys-Variablen vorbelegt. In einer weiteren Tabelle werden alle im Skript definierten Zustände sowie deren Startadressen (1. Op-Code im Anweisungsblock hinter ) gesammelt. In jedem Zustand werden die definierten Events mit den Startadressen der Anweisungsblöcke festgehalten. Alle Datenstrukturen, die CPL betreffen, sind in iSrvCpl.h vereinbart. Zusammen mit einer Variablen, die den aktuellen Zustand kennzeichnet, bildet die Zustandstabelle die State-Machine, die nun vom Interpreter ausgeführt wird.
Neben dem scheinbar höheren Aufwand für das Erzeugen des P-Codes, hat diese Form der Skriptabarbeitung mehrere Vorteile:
Vor allem der letzte Punkt hat mich dazu gebracht, CPL-Skripte nicht zeilenorientiert abzuarbeiten.
Nach erfolgreicher Übersetzung steht der P-Code in einer Tabelle auf die über die script-Struktur zugegriffen wird. Letztere enthält zusätzlich einen Programcounter, ein Index, der auf den jeweils nächsten auszuführenden Op-Code in die Code-Tabelle zeigt. Ein Op-Code ist lediglich eine Struktur mit einem Op-Code und drei optionalen Operanden. Dies können Offsets in die Symboltabelle oder Sprungadressen sein. Tabelle [tab-opcodes] zeigt die Umsetzung von CPL Anweisungen in Op-Codes, dabei kennzeichnet sym Operanden, die in der Symboltabelle liegen. Sprungadressen sind mit PC gekennzeichnet.
Nach dem Compilieren des Skriptes ist die State-Machine bereits initialisiert, so kann nun die Interpretation des P-Codes mit Cpl_Schedule() gestartet werden. Als Parameter wird die script-Struktur diesmal in einer übergeordneten call-Struktur übergeben, welche die Kontextinformationen des aktuellen Anrufes enthält.
main ()
{
...
Cpl_Schedule (&call, evEnter, NULL);
IsdnMainLoopp ();
} /* main() */
Zusätzlich erhält Cpl_Schedule() einen Event-Typ und einen optionalen String-Parameter übergeben. Nach der Ausführung des ersten Op-Codes setzt Cpl_Schedule einen Timer mit 0 und terminiert. Dadurch, daß nur ein Befehl ausgeführt wird, ist sichergestellt, daß die Kontrolle so schnell wie möglich an die Isdn-Main-Loop zurückgegeben wird, damit ggf. anstehende ISDN-Events behandelt werden können. Da der mit 0 gesetzte Timer sofort abgelaufen ist, wird durch die Handler-Routine erneut Cpl_Schedule aufgerufen. Die CPL-Anweisungen, bei denen so verfahren wird, sind in Tabelle [tab-opcodes] ohne aufgeführt.
CPL statement | generated op codes | CPL statement | generated op codes |
accept | opAccept sym | exec | opExec sym |
redirect | opRedirect sym | audiorec | opAudiorec sym |
<proc-call> | opCall sym | cvt | cvt sym sym sym |
enter | enter sym | exit | exit |
hangup | opHangup sym | if | opCond PC |
opGoto PC | incr | opIncr sym sym | |
invite | opInvite | proc | opReturn |
timer local/global | opStartTimer sym sym | timer stop | opStopTimer sym |
Die mit gekennzeichneten Befehle gelten hingegen als kritisch, d.h. ihre Ausführungszeit kann nicht vorhergesagt werden. Aus diesem Grunde wird für und ein eigener Prozeß erzeugt, dessen stdout/stdin jeweils über eine Pipe mit dem Hauptprozeß verbunden ist. Bei der Ausführung dieser beiden Befehle ist der Hauptprozeß also nur für die Zeit der Prozeßerzeugung blockiert.
Neue Events, wie beispielsweise das Auftreten von Laufzeitfehlern, bei denen sysStatus 0 wird, können also eingeführt werden, indem
Da cpldebug ohne ISDN und Netzwerkzugang arbeitet, beschränkt sich die Ausführung der einzelnen Kommandos auf deren Ausgabe am Bildschirm. Folgender Ausschnitt zeigt die Abarbeitung des Skriptes red.cpl (Benutzereingaben sind unterstrichen):
BEGIN CODE EXECUTION:
resuming...
PC=0000 call 'SayHello': branching 0008
PC=0008 log 'HELLO'
PC=0009 return
PC=0001 redirect 'rtp://becks:9090'
>>>S000: Enter event id
(0=evEnter, 1=Disc, 2=Timeout, 3=DTMF, 4=speech, 5=Reschedule):
Während der Code-Ausführung wird der Program-Counter (PC=xxxx) mit dem dazugehörigen Befehl ausgegeben. Nach dem Abarbeiten des Blocks des ersten Zustands, bleibt das Programm stehen und wartet auf Eingabe eines Events. S000 ist der augenblickliche Zustand, in dem sich die State-Machine befindet. Alle Zustände eines Skriptes sind beginnend bei 0000 fortlaufend durchnumeriert. Das Skript red.cpl sieht wie folgt aus:
state FirstState {
on enter {
SayHello
redirect "rtp://becks:9090"
}
on disconnect {
enter FinalState
}
}
state FinalState {
on enter {
hangup
exit
}
}
proc SayHello {
log "HELLO"
}
Da in diesem Skript außer Enter keine anderen Events definiert sind, führt lediglich die Eingabe von 0 bzw. 1, für die Simulation des Enter- bzw. Disconnect-Events zu einer Reaktion des Programms. Durch Angabe eines optionalen Parameters (Integer > 0) beim Aufruf von cpldebug, können Symbol- und Zustandstabelle des Interpreters ausgegeben werden:
SYMBOL TABLE:
ind line type current value
000 00001 var 'sysVersion'
001 00001 var 'sysCallDuration'
002 00001 var 'sysDate'
003 00001 var 'sysDay'
004 00001 var 'sysTime'
005 00001 var 'sysStatus'
006 00001 var 'sysCallingParty'
007 00001 var 'sysCalledParty'
008 00001 var 'sysDtmf'
009 00001 var 'sysHostname'
010 00001 var 'sysTrace'
011 00001 var 'sysUsername'
012 00002 state 'FirstState' StateId = 0
013 00005 proc 'SayHello' PC = 8
014 00005 const 'rtp://becks:9090'
015 00009 state 'FinalState' StateId = 1
016 00021 const 'HELLO'
STATE TABLE:
FirstState
ON ENTER PC=0000
ON DISCONNECT PC=0003
FinalState
ON ENTER PC=0005
CODE TABLE:
0000 CALL SayHello (PC=0008)
0001 REDIRECT @14
0002 STOP
0003 ENTER FinalState
0004 STOP
0005 HANGUP
0006 EXIT
0007 STOP
0008 LOG @16
0009 RET
Die Zustandstabelle enthält neben dem Zustandsnamen sowie den definierten Events deren Einstiegspunkte in die Code-Tabelle. Letztere wird am Ende ebenfalls ausgegeben. Die mit @ versehenen Argumente sind Referenzen in die Symboltabelle.