5. Fehlersuche und Debugging^5.1 Methoden zur FehlersucheUm einen Fehler einzugrenzen gibt es keinen allgemeingültigen Algorithmus. Es gibt jedoch einige Strategien, sie sich bewährt haben.Hilfreiche BefehleDie in der folgenden Tabelle aufgelisteten Befehle und globale Variablen können Ihnen bei der Fehlersuche behilflich sein.
Defensive ProgrammierungRechnen Sie jederzeit damit, dass etwas schief geht, auch wenn Sie der Meinung sind, dass gar nichts schief gehen kann.Beispiele:
Ausgabe von Log-InformationenUm zu überprüfen, ob ein bestimmter Programmteil auch genau so arbeitet, wie man es wünscht, kann man sich in bestimmten Abständen eine Information ausgeben lassen, dass dieser Programmteil gerade erfolgreich abgearbeitet wurde - oder eben nicht. Im einfachsten Fall verwendet man dazu den Befehl MsgBox, eventuell in Kombination mit dem Befehl ErrorText$. Die komfortable Variante sieht so aus, dass man sich ein eigenes Objekt zur Ausgabe von Log- oder Fehlermeldungen definiert (z.B. ein Memo oder ein BitmapContent) und sich eine Routine schreibt, die Text an dieses Objekt ausgibt. Auf diese Weise kann man auch den Zustand von bestimmten Variablen ausgeben. Für ein BitmapContent als Ausgabeobjekt könnte diese Routine so aussehen: |
SUB DebugLog(text$ as String) DIM scr as Object scr = Screen ' aktuellen Screen merken Screen = MyBitmapContent Print text$ Screen = scr ' Screen zurücksetzen End Sub |
Wenn Sie ein Memo Objekt zur Ausgabe der Informationen verwenden, müssen Sie die Print-Anweisung ersetzen und können sich das Umschalten des Screen ersparen. In einem Memo können Sie auch mehr Text anzeigen, Sie müssen jedoch selbst dafür sorgen, dass das Fassungsvermögen des Objekts nicht überschritten wird.
Verwenden der Error-Checking VersionR-BASIC bietet die Möglichkeit, bestimmte Codezeilen nur dann zu Compilieren, wenn man eine 'Error-Checking Version' (Version zur Entwicklung bzw. Fehlersuche) compiliert. Zwischen dieser und der 'normalen' Version kann mit wenigen Mausklicks umgeschaltet werden. In Kombination mit den oben genannten Methoden kann man damit eine extensive Fehlerkontrolle implementieren, ohne die damit verbundenen Nachteile in der Endversion des Programms zu haben. Die Verwendung von Error-Checking Code wird ausführlich im nächsten Kapitel beschrieben.Verwenden des DebuggersDer Debugger ist die leistungsfähigste Methode der Fehlersuche. Sie können damit zum Beispiel Ihren Code schrittweise abarbeiten, Variablen ansehen und ändern, überprüfen in welcher Reihenfolge Routinen aufgerufen werden und vieles mehr. Es wird dringend empfohlen, sich mit der Arbeitsweise des Debuggers vertraut zu machen. Auch wenn er anfangs relativ komplex erscheint, ist er doch sehr einfach und intuitiv zu bedienen. Der Debugger ist ausführlich im Kapitel 5.3 beschrieben.WaitForHandlesUnter GEOS ist es ein häufiges Problem, dass die Systemhandles knapp werden. Unglücklicher Weise verbrauchen bestimmte Operationen, insbesondere einige Dateioperationen, oder das Setzen bestimmter Instancevariablen, temporär einige Handles. Führt man diese Operationen oft hintereinander aus, z.B. in einer Schleife, so kann es zum berüchtigten 'zu wenig Handles' Fehler kommen. Das ist ein Problem auf Systemebene, tritt also auch bei reinen SDK-Programmen auf.R-BASIC bieten einen 'WorkAround' für dieses Problem. Falls bei Ihrem Programm ein solches Problem auftritt, können Sie den Befehl WaitForHandles verwenden. Dieser WorkAround wird z.B. auch im Uni-Installer Programm verwendet. R-BASIC Programme merken sich die Anzahl der freien Handles am Programmstart. WaitForHandles stoppt die Ausführung eines BASIC Programms für eine bestimmte Zeit, wenn die Anzahl der verfügbaren Handles unter einem bestimmten Wert liegt. Während dessen hat das System Zeit, die temporär benutzten Handles freizugeben, so dass das BASIC-Programm anschließend problemlos weiterarbeiten kann.
Beispiele |
WaitForHandles ' Defaultwerte verwenden
WaitForHandles 50, 6 ' Bei weniger als 50% der Handles
' 0,1 Sekunde warten
|
| Ein typisches Vorgehen, dieses Problem unter R-BASIC hervorzurufen, ist das sehr häufige Aufrufen einer Sequenz, in der eine Datei geöffnet, bearbeitet und wieder geschlossen wird. Für den Fall, dass man eine Datei benutzt, um die Schritte eines Programm zu dokumentieren, ist das ein durchaus realistisches Szenario. Die folgende Schleife ruft den 'zu wenig Handles' Fehler hervor, falls der Befehle WaitForHandles entfernt wird. |
FOR n = 10 TO 1000
FOR c = Asc("A") TO Asc("z")
fh = FileOpen "crash.txt"
FileWrite fh, c, 1
FileClose fh
WaitForHandles
NEXT
NEXT
|
|
Ein anderes Szenario ist das schnelle und periodische Setzen einer Instancevariablen, die ein visuelles Update eines oder mehrerer Objekte hervorruft. diese Objekte senden dann wieder Messages an andere Objekte usw. Da das Senden von Messages temporär Handles erfordert, kann das in seltenen Fällen zu einem Problem führen.
In allen Fällen gilt aber auch: ob das Problem auftritt oder nicht kann auch von der Systemgeschwindigkeit abhängen.
IgnoreWarningDer Compiler gibt in verschiedenen Situationen eine Warnung aus, um Sie auf potentiell fehlerhaften Code hinzuweisen. Ein typisches Beispiel ist der Vergleich von Strukturvariablen. Enthält die Strukur einen String, der kürzer als die maximale Länge ist, so befinden sich dahinter einige Bytes, die unbenutzt sind und so Datenmüll enthalten können. Hier kann es beim Vergleich zu einem falschen Ergebnis kommen, weil Strukturen Byte-für-Byte - einschließlich des Datenmülls - verglichen werden.Wenn Sie sicher sind, dass dieses Problem nicht auftreten kann, dann können Sie IgnoreWarning verwenden, um die Warnmeldung zu unterdrücken. IgnoreWarning wirkt nur auf die direkt auf IgnoreWarning folgende Anweisung.
Beispiel |
DIM t1, t2 AS GeodeToken IgnoreWarning IF t1=t2 then Print "ungleich" 'keine Warnmeldung IF t1=t2 then Print "ungleich" 'Jetzt wieder Warnmeldung |
^5.2 Die Error-Checking VersionSehr häufig kommt es vor, dass man während der Programmentwicklung bestimmte Fehlersituationen abfragen möchte, der entsprechende Code im fertigen Programm aber nur stören würde, weil er das Programm z.B. unnötig verlangsamt. Deswegen bietet R-BASIC die Möglichkeit, einzelne Codezeilen nur dann zu compilieren, wenn eine 'Error-Checking-Version' (EC-Version) compiliert wird. Alternativ kann man auch Codezeilen nur dann compilieren, wenn eine 'Non-Error-Checking-Version' (NC-Version) compiliert wird.Ob eine EC-Version compiliert wird oder nicht, können Sie im Menü 'Programm' unter 'Error-Checking Version' einstellen.
ECONECON aktiviert das Compilieren der EC-Version für die nächsten Codezeilen bis zum nächsten ECOFF-Befehl oder bis zum Ende des Codewindows. Zeilen mit vorangestellter EC Anweisung werden compiliert, Zeilen mit vorangestellter NC Anweisung werden als Kommentarzeilen behandelt.ECON kann sowohl im BASIC Code als auch im UI Code, auch innerhalb von Objektdeklarationen, verwendet werden. Damit ECON wirkt muss im Menü 'Programm'-'Error-Checking Version' die Option 'ECON/ECOFF benutzen' aktiv sein. Syntax: ECON Hinweis: Im Modus 'ECON/ECOFF benutzen' ist am Beginn jedes Codewindows die NC-Version aktiviert. EC-Code wird erst compiliert, wenn das erste ECON in diesem Codewindow ausgeführt wurde. ECOFFECOFF aktiviert das Compilieren der NC-Version für die nächsten Codezeilen bis zum nächsten ECON-Befehl oder bis zum Ende des Codewindows. Zeilen mit vorangestellter NC Anweisung werden compiliert, Zeilen mit vorangestellter EC Anweisung werden als Kommentarzeilen behandelt.ECOFF kann sowohl im BASIC Code als auch im UI Code, auch innerhalb von Objektdeklarationen, verwendet werden. Damit ECOFF wirkt, muss im Menü 'Programm'-'Error-Checking Version' die Option 'ECON/ECOFF benutzen' aktiv sein. Syntax: ECOFF ECEC markiert eine Zeile als 'zur EC-Version gehörig'. Diese Zeile wird nur compiliert, wenn eine EC-Version compiliert wird, entweder weil die Anweisung ECON ausgeführt wurde oder weil im Menü 'Programm'-'Error-Checking Version' die Option 'EC-Version compilieren' aktiv ist.Syntax: EC <Codezeile> NCNC markiert eine Zeile als 'zur NC-Version gehörig'. Diese Zeile wird nur compiliert, wenn eine NC-Version compiliert wird. Das ist in folgenden Fällen der Fall:
Syntax: EC <Codezeile> Beispiele: Beispiel 1: Die Sub DrawData funktioniert nur richtig, wenn der Parameter x kleiner als der Parameter y ist. Während der Programmentwicklung wollen wir das überwachen. |
ECON
SUB DrawData (x, y as Real)
EC IF x >= y THEN MsgBox("Parameterfehler in DrawData")
...
END SUB
|
|
Beispiel 2: In der EC-Version wollen wir eine Group mit einer Beschriftung versehen, die uns darauf aufmerksam macht, dass wir eine EC-Version vor uns haben. In der NC-Version soll es nur ein einfacher Text sein. |
Group InfoKasten EC Caption$ ="EC-VERSION Meldungen" NC Caption$ ="Meldungen" ... End Object |
|
Beispiel 3: Ein Objekt soll nur in der EC-Version sichtbar sein |
Button TestButton Caption$ ="Test" NC visible = FALSE ActionHandler = .... End Object |
|
Beispiel 4: Komplexe Verwendung von ECON und ECOFF |
ECON Print "Beispiel" EC Print "Error Checking Code 1" NC Print "Die Welt ist schön" ECOFF EC Print "Error Checking Code 2" NC Print "Der Himmel ist blau" |
|
Je nach Einstellung im Menü 'Error-Checking Version' werden folgende Texte ausgegeben: ECON/ECOFF benutzen |
Beispiel Error Checking Code 1 Der Himmel ist blau |
|
EC-Version compilieren |
Beispiel Error Checking Code 1 Error Checking Code 2 |
|
NC-Version compilieren |
Beispiel Die Welt ist schön Der Himmel ist blau |
^5.3 Der R-BASIC DebuggerDer Debugger ist das leistungsfähigste Werkzeug bei der Fehlersuche. Er ermöglicht es, das Programm an einer bestimmten Stelle anzuhalten, den Programmcode schrittweise auszuführen, die Aufrufreihenfolge der Routinen zu ermitteln, dabei die Werte von Variablen anzusehen und zu ändern und so einen Fehler komfortabel einzugrenzen.Breakpoints
Um ein Programm an einer bestimmten Stelle anzuhalten müssen Sie dort einen Breakpoint (einen Haltepunkt) setzen. Dazu klicken Sie mit der rechten Maustaste auf die Zeilennummer im Editorfenster.
Sie erkennen einen gesetzten Breakpoint an der rot markierten Zeilennummer. Der nächste rechte Mausklick markiert den Breakpoint als 'disabled' (d.h. inaktiv), der dritte löscht ihn wieder.
Wenn der Interpreter auf eine Zeile stößt, für die ein Breakpoint gesetzt ist, hält er die Ausführung an und das Debugger-Window öffnet sich. Das Programm befindet sich jetzt im Einzelschrittbetrieb. Sie können jetzt das Programm Schritt für Schritt abarbeiten, Variablen einsehen usw. Hinweise:
LaufzeitfehlerWenn es zu einem Laufzeitfehler kommt, z.B. beim Lesen aus einer nicht geöffneten Datei, wird ein Programm ohne Debugger automatisch beendet. Der Debugger greift hier ein und behandelt einen Laufzeitfehler wie einen Breakpoint. Damit haben Sie Zugriff auf alle Debugger-Funktionen - mit Ausnahme des Einzelschrittbetriebs. Häufig ist jedoch der Schalter 'Handler abbrechen' aktiv. Dieser unterbricht den laufenden Handler ohne das Programm zu beenden. In vielen Fällen ist das Programm dann trotz des Laufzeitfehlers weiter lauffähig, was bei der Fehlersuche nützlich sein kann.^5.3.1 Bedienung des Debuggers
Das ist das Debugger-Window. Von hier aus können Sie Ihr Programm steuern, die Variablen einsehen und vieles mehr. Sie können dieses Fenster auch jederzeit über die Taste F10 oder das 'Programm' Menü öffnen.
EinzelschrittbetriebSteht das Programm in einem Breakpoint so können Sie den Code Zeile für Zeile oder Routine für Routine abarbeiten. Dabei bietet der Debugger folgende Möglichkeiten:
Bereich Sonstiges
Elemente auf der rechten Seite
Elemente in der ReplyBar
Neben den selbst erklärenden Schaltern 'Fenster schließen' und 'Programm beenden' finden Sie hier Buttons mit den Aufschriften ' L ' und ' R '. Diese Buchstaben stehen für 'links' und 'rechts' und blenden die entsprechenden Bereiche des Debugger-Windows aus bzw. ein. Damit wird die Größe des Debugger-Windows reduziert, was von Vorteil sein kann, wenn neben dem Debugger-Window gleichzeitig das zu analysierende Programm auf dem Bildschirm zu sehen sein soll. Werden beide Bereiche ausgeblendet, wird trotzdem eine minimale UI angezeigt, mit der man das Programm im Einzelschrittbetrieb steuern kann.
Anmerkung: Die IDE selbst kann nicht auf diese Weise versteckt werden. Schieben Sie sie einfach an den unteren Bildschirmrand, wenn sie stört.
5.3.2 Debuggen von LibrariesDie aktuelle Version des Debuggers unterstützt das Debuggen von Code in Libraries nicht. Breakpoints innerhalb von Libraries werden ohne Warnung ignoriert. Um Code aus Libraries zu debuggen, müssen Sie ihn ins Hauptprogramm verschieben.^5.3.3 Interne OrganisationDieser Abschnitt enthält Hintergrundinformationen, deren Kenntnis oder Verständnis für die Arbeit mit dem Debugger nicht unbedingt erforderlich sind.Wenn der Compiler ein Programm übersetzt, ersetzt er die Variablennamen durch ihre Position im Variablenspeicher, beim Aufruf von Routinen wird statt des Namens die Position der aufgerufenen Routine im Code abgespeichert, usw. Wenn der Debugger auf die entsprechenden Namen zugreifen will, müssen sie extra gespeichert werden. Dazu legt der Compiler eine zusätzliche Datei an, die Debugger-Datei genannt wird. Sie wird nur benötigt, wenn das Programm aus der IDE heraus gestartet wird und enthält alle für den Debugger nötigen Informationen, die im eigentlichen Programm nicht benötigt werden. Dazu zählen insbesondere Listen mit den Namen der globalen Variablen, Konstanten und Strukturen, sowie aller Routinen und der dazugehörigen lokalen Variablen. Alle Programme benutzen die gleiche Debugger-Datei, sie heißt 'PROGRAM SYMBOL FILE', bzw. für die Libraries 'LIBRARY SYMBOL FILE'. Das hat folgende Konsequenz. Nehmen wir an, Sie compilieren zuerst Programm A. Wenn Sie im Anschluss daran das Programm B compilieren, so gehen die Debugger-Informationen von Programm A verloren. Falls Sie das Programm A danach erneut starten wollen, müssen Sie es erneut compilieren, selbst wenn Sie nichts daran geändert haben. Jedes Mal, wenn der Interpreter eine Codezeile zum Ausführen lädt, prüft er ab, ob das Programm unter der Kontrolle der IDE läuft. Das geht extrem schnell, weil nur ein einziges Bit abgefragt werden muss. Diese Abfrage verlangsamt ein eigenständiges Programm daher faktisch nicht. Nur wenn das Programm unter der Kontrolle der IDE läuft, wird eine Routine gestartet, die prüft, ob ein Breakpoint vorliegt und gegebenenfalls das Programm unterbricht und den Debugger startet. Auch diese Routine fragt einzelne Bits ab, ist also ebenfalls recht schnell. Ein Programm unter der Kontrolle der IDE läuft daher faktisch genauso schnell ab, wie ein eigenständiges Programm. Intern läuft bei einem Breakpoint der BASIC-Thread (das ist der Prozess-Thread des Launchers) in einer Schleife, die darauf wartet, dass ein bestimmtes Bit zurückgesetzt wird. Der UI-Thread des Launchers kommuniziert während dessen mit der IDE und nimmt Kommandos, wie z.B. 'Einzelschritt ausführen' entgegen. Daraufhin setzt er bestimmte Bits und gibt den BASIC Thread wieder frei. Der BASIC-Thread führt dann z.B. genau einen Befehl aus und landet dann wieder in der Warteschleife. Während der BASIC-Thread in der Warteschleife ist, können Sie auf die Variablen des Programms zugreifen. Auch das wird über den UI-Thread des Launchers abgewickelt, denn nur das BASIC-Programm (und nicht die IDE selbst) kennt die Position der Variablen im Variablenspeicher. Die IDE hingegen kennt über die Debugger-Datei den Namen und den Typ der Variablen. Analog verhält es sich mit dem Routinen-Stack. Der Launcher kennt die Aufrufreihenfolge und die Position bzw. die Returnadresse der Routinen, die IDE ermittelt daraus mit Hilfe der Debugger-Datei den Namen der Routine.
Bei einem Laufzeitfehler wird die gleiche Routine gerufen, die auch bei einem Breakpoint die Kommunikation mit der IDE abwickelt. Deswegen steht bei einem Laufzeitfehler der Zugriff auf Variablen und Routinenstack genauso zur Verfügung wie in einem Breakpoint. Nur das weitere Ausführen des Programms ist naturgemäß nicht möglich.
|