Was sind Exceptions?
Das Wort "
Exception" heißt übersetzt "Ausnahme". Man könnte sagen, wenn eine
Exception auftritt, hat man einen "Ausnahmezustand". In der Regel deutet eine
Exception darauf hin, daß etwas ziemlich schief gegangen ist.
Wie können Exceptions entstehen?
Es gibt grundsätzlich zwei verschiedene Möglichkeiten, wie Exceptions entstehen können:
- Vom Betriebssystem ausgelöste Exceptions. Zum Beispiel durch Schreibzugriff auf einen geschützten Speicherbereich:
integer(nil^) := 0;
Als Ergebnis bekommen wir eine "Zugriffsverletzung", in Englisch eine "Access Violation", oft auch als "AV" abgekürzt. Die entsprechende von Delphi gezeigte Meldung sieht dann normalerweise so aus:

Andere Beispiele für vom Betriebssystem ausgelöste Exceptions sind zum Beispiel Stacküberläufe ("Stack overflow") oder Division durch Null.
- Man kann Exceptions auch manuell auslösen. Vom Quellcode her funktioniert es wie folgt:
raise Exception.Create('Beispiel-Exception');
Delphi zeigt das normalerweise dann so:

Warum das manuelle Auslösen von Exceptions Sinn machen kann, wird später noch erklärt.
Was passiert mit dem Programm, wenn eine Exception auftritt?
Wenn eine
Exception ausgelöst wird, sucht das Betriebssystem nach einem
Exception-Handler. Das heißt, das Betriebssystem sucht nach jemandem, der sich für die
Exception zuständig fühlt und sich um das Problem kümmert.
In den Tiefen des Delphi-Quellcodes sind entsprechende
Exception-Handler eingebaut, die die meisten anfallenden Exceptions abfangen und dann versuchen, sinnvoll zu reagieren. Im Normalfall zeigen die Delphi-
Exception-Handler eine kleine Box an (siehe Screenshots oben) und führen dann den normalen Programmverlauf fort.
Es gibt aber einige Situationen, in denen das nicht so gut klappt, wie gewohnt. In diesen Fällen kommen dann weniger aussagekräftige Boxen wie z.B. diese hier:
Oder in noch schlimmeren Fällen kann es dazu kommen, daß ein Thread einfach stillschweigend beendet wird. Oder daß Windows selbst (bzw Dr. Watson) sich über das abgestürzte Programm beschwert. In Windows 9x kann es in Ausnahmefällen nach Exceptions sogar mal zu blauen Bildschirmen kommen.
Wieso klappt das behandeln von Exceptions manchmal besser und manchmal schlechter? Diese Frage werden wir uns später nochmal stellen. Im Moment wissen wir noch nicht genug, um das zu verstehen.
Wir können wir selbst mit aufgetretenen Exceptions umgehen?
Manchmal weiß man (oder befürchtet man), daß ein bestimmter Code eine
Exception auslösen kann. In solchen Fällen kann es erwünscht sein, die mögliche
Exception selbst abzufangen und entsprechend zu reagieren. Hier ein kleines Beispiel dafür:
Delphi-Quellcode:
function CreateEmptyFile(fileName: string) : boolean;
begin
try
TFileStream.Create(fileName, fmCreate).Free;
result := true;
except
result := false;
end;
end;
Wenn innerhalb eines "try..except"-Blocks eine
Exception auftritt, wird diese abgefangen und der Code im "except..end"-Block wird ausgeführt. Das Programm wird direkt nach dem "except..end"-Block fortgesetzt. Die
Exception selbst wird in dem Moment aufgelöst. Falls keine
Exception auftritt, wird der Code im "except..end"-Block *nicht* ausgeführt.
Wichtig zu beachten ist, daß beim Auftreten einer
Exception das Betriebssystem nach so einem "try..except"-Block sucht. Die aufgerufenen Funktionen werden dabei rückwärts durchgegangen, solange bis endlich ein "try..except"-Block gefunden wurde. Zur Verdeutlichung ein weiteres Beispiel:
Delphi-Quellcode:
procedure Function3;
begin
ShowMessage('
3.1');
if Random(10) = 5
then
raise Exception.Create('
Zufall!');
ShowMessage('
3.2');
end;
procedure Function2;
begin
ShowMessage('
2.1');
Function3;
ShowMessage('
2.2');
end;
procedure Function1;
begin
ShowMessage('
1.1');
try
ShowMessage('
1.2');
Function2;
ShowMessage('
1.3');
except
ShowMessage('
1.4');
end;
ShowMessage('
1.5');
end;
Was passiert in diesem Beispiel, wenn *keine*
Exception ausgelöst wird? Die "except..end"-Blöcke werden in dem Fall nicht durchlaufen. Also würden wir die Meldungen in folgender Reihenfolge sehen: 1.1, 1.2, 2.1, 3.1, 3.2, 2.2, 1.3 und 1.5.
Was passiert, wenn eine
Exception ausgelöst wird? In dem Fall bekommen wir 1.1, 1.2, 2.1, 3.1, 1.4 und 1.5. Im dem Moment wo die
Exception ausgelöst wird, sucht das Betriebssystem nach dem nächst gelegenen "try..except" Block. Solange keiner gefunden wird, werden alle aufgerufenen Funktionen (also hier Function3 und Function2) sofort verlassen. Sobald ein "try..except"-Block gefunden wird, wird der entsprechende "except..end"-Block ausgeführt. Das Programm wird dann direkt nach diesem Block fortgeführt.
In bestimmten Ausnahmefällen kann es für ein Programm Sinn machen, die Ausführung einer bestimmten Kette von Funktionen sofort zu unterbrechen und direkt an eine gewünschte Code-Stelle in einer tiefer liegenden Funktion zurückzuspringen. Solche Fälle kann man lösen, indem man manuell eine
Exception auslöst und dann an der gewünschten Stelle abfängt.
Was passiert mit Exceptions, die ich selbst nicht behandle?
Die meisten Exceptions passieren während der Phase eines Programmes, wo das Hauptformular aktiv ist. Diese Phase wird durch "Application.Run" in der Projekt-Datei (*.dpr) gestartet. Delphi hat in neueren Versionen die Haupt-Message-Behandlungs-Schleife in "Application.Run" durch einen "try..except"-Block geschützt. Das heißt, die meisten unbehandelten Exceptions enden in diesem "try..except"-Block. Delphi zeigt dann eine einfache Box an mit einer kleinen Beschreibung der
Exception. Nachher läuft die Message-Schleife in "Application.Run" einfach weiter.
Wieso kommt Delphi mit manchen Exceptions nicht klar?
Es gibt Fälle, in denen Delphi nur einen "Runtime error xxx" anzeigt, statt eine vernünftige Meldung zu bringen. Manchmal fängt Delphi eine
Exception auch gar nicht ab, so daß Windows selbst die
Exception-Behandlung übernimmt. In beiden Fällen ist eine vernünftige Fortführung des Programmes nicht mehr möglich. Wie kommt es zu diesen Situationen?
Die Antwort ist relativ einfach: Exceptions, die außerhalb von "Application.Run" auftreten, sind nicht mehr durch den "try..except"-Block in "Application.Run" geschützt. Delphi fängt manche dieser Exceptions dann in einer noch tieferen Ebene ab. Das funktioniert in etwa so:
Delphi-Quellcode:
program blabla;
begin
try
Application.Initialize;
Application.CreateForm(...);
Application.Run;
except
ShowMessage('Runtime error xxx');
end;
ExitProcess(0);
end.
Wie man deutlich erkennen kann, gibt es nach dem Anzeigen einer "Runtime error xxx"-Meldung gar keine andere Möglichkeit mehr, als das Programm sofort zu beenden.
Eine weitere Art von
Exception, die Delphi oft nicht abfängt, sind Exceptions in Threads. In neueren Delphi-Versionen sind TThread-Threads intern durch einen "try..except"-Block geschützt. Abgefangene Exceptions werden dann zum Hauptthread geschickt, während der TThread einfach beendet wird. In D5 war es noch so, daß der TThread einfach kommentarlos beendet wurde und die aufgetretene
Exception totgeschwiegen wurde. In D4 wurden Exceptions in TThread gar nicht abgefangen, sondern Windows (bzw Dr. Watson) meldete sich dann und wollte das Programm beenden.
Threads, die nicht über TThread, sondern direkt über CreateThread erzeugt werden, sind auch in neusten Delphi-Versionen nicht automatisch durch einen "try..except"-Block geschützt. Falls in so einem Thread eine
Exception auftritt, meldet sich wiederum Windows/Dr. Watson und möchte das ganze Programm beenden.
Was mache ich, wenn mein Programm bei jemand anderem abstürzt - aber nicht bei mir?
Die meisten Programmierer kennen wahrscheinlich das Problem: Auf dem eigenen PC läuft das selbst geschriebene Programm problemlos. Aber auf dem PC eines anderen stürzt es ständig ab. Und meistens wohnt dieser andere dann irgendwo am anderen Ende der Welt, so daß eine direkte Untersuchung des Problems vor Ort nicht möglich ist. Was kann man dann tun?
Abstürze in Windows-Programmen sind meistens mit Exceptions gleichzusetzen. Das heißt, wenn ein Programm abstürzt, liegt meistens eine
Exception vor. Was wir nun brauchen, um den Abstürzen auf den Grund zu gehen, sind mehr (viel mehr) Informationen darüber, was genau passiert ist. Die mit Abstand wichtigste Information ist: An welcher Stelle im Quellcode ist die
Exception genau ausgelöst worden? Die zweitwichtigste Information ist wahrscheinlich: Über welchen Weg ist das Programm an diese Stelle gekommen? Beides sind Informationen, die uns Delphi leider nicht gibt.
Bei Zugriffsverletzungen (aber auch nur da) meldet uns Delphi eine Code-Adresse, die wir mit etwas Aufwand so interpretieren können, daß wir die Stelle im Quellcode finden, die die Zugriffsverletzung ausgelöst hat. Das klappt allerdings nur, wenn wir genau (100%ig, ohne auch nur die geringste Abweichung) die gleichen Quellen und die gleichen Versionen aller Drittanbieter-Komponenten zur Verfügung haben, mit denen das abgestürzte Programm kompiliert wurde. Und selbst dann ist es schwierig genug, die Stelle im Quellcode zu finden.
An dieser Stelle darf ich (mit Erlaubnis der Forum-Verantwortlichen

etwas Werbung für meine "madExcept"-Komponente machen.
Was ist madExcept und was macht es?
madExcept integriert sich automatisch in jedes Delphi-Programm (nur
win32, nicht DotNet) und übernimmt die komplette
Exception-Verarbeitung von Delphi. Dabei werden folgende Verbesserungen erzielt:
- Zu jeder Exception werden detaillierte Informationen über die Code-Stelle, wo die Exception ausgelöst wurde, ermittelt.
- Der ganze Weg ("Callstack"), wie das Programm zur Absturz-Stelle gekommen ist, wird ermittelt.
- Sämtliche noch laufenden Threads werden aufgelistet - mit komplettem Callstack (also womit sie gerade beschäftigt sind).
- Haufenweise zusätzliche Informationen (geladene Dlls, Speicherverbrauch, Betriebssystem, Sprache, Hardware, usw) werden gesammelt.
- Alle Exceptions aller Threads werden automatisch mit abgefangen.
- Auch Exceptions außerhalb von "Application.Run" werden mit vollständigem Funktionsumfang behandelt. Keine "Runtime error xxx"-Meldungen mehr.
- Die ganzen Informationen werden in Form eines Fehlerberichts aufbereitet und können samt optionalem Screenshot direkt an den verantwortlichen Programmierer gemailt werden. Die entsprechenden Mail-Routinen usw sind alle bereits in madExcept enthalten.
Wie kann ich madExcept in meine Anwendung einbinden?
Zuerst einmal muß der Installer her:
http://madshi.net/madCollection.exe
Nach der Installation von madExcept bitte Delphi starten und das gewünschte Projekt laden. Im Delphis "Projekt"-Menü gibt es jetzt einen neuen Eintrag:
Diesen Eintrag bitte anklicken. Es erscheint folgendes Bild:
Bitte dort die beiden Haken "
handle exceptions" und "append map file to binary" anklicken. Danach das Projekt neu kompilieren - fertig!
Wie wirkt sich madExcept zur Laufzeit aus?
Erst einmal merkt man keinen Unterschied, da madExcept nur dann eingreift, wenn wirklich eine
Exception auftritt. Nehmen wir an, wir drücken auf den "Button1" bei folgendem Code:
Delphi-Quellcode:
procedure TForm1.Button1Click(Sender: TObject);
begin
integer(nil^) := 0;
end;
Innerhalb der Delphi
IDE meldet sich dann erst den Debugger. Bitte mit F9 das Programm weiter laufen lassen. Dann kommt die madExcept
Exception-Box. Außerhalb der Delphi
IDE kommt sofort die madExcept
Exception-Box. Das sieht dann so aus:
Wem die englischen Texte nicht gefallen, kann das ganze im (weiter oben schon gezeigten) Einstellungsfenster eindeutschen. Der Endbenutzer kann jetzt die Anwendung fortführen, neu starten oder beenden. Außerdem kann er sich einen detaillierten Fehlerbericht anzeigen lassen oder den Fehlerbericht an den Programmierer mailen. Der Fehlerbericht sieht dann wie folgt aus (stark gekürzte Fassung):
Code:
physical memory : 717/1022 MB (free/total)
free disk space : (C:) 68.54 GB
allocated memory : 3.44 MB
executable : Project1.exe
exception class : EAccessViolation
exception message : Zugriffsverletzung bei Adresse 00472910 in Modul 'Project1.exe'. Schreiben von Adresse 00000000.
main thread ($5d4):
00472910 Project1.exe Unit1 28 TForm1.Button1Click
00471302 Project1.exe Forms TApplication.Run
00472be7 Project1.exe Project1 14 initialization
disassembling:
0047290c public TForm1.Button1Click: ; function entry point
0047290c 28 xor eax, eax
0047290e xor edx, edx
00472910 > mov [eax], edx
00472912 29 ret
Den vollständige Fehlerbericht habe ich als Attachment angehängt (siehe bugReport.txt). Die einzelnen Bestandteile des Fehlerberichts lassen sich auf Wunsch auch ausschalten oder nach Wunsch einstellen.
Was kostet madExcept und wo finde ich mehr Informationen?
madExcept ist kostenlos für nicht-kommerzielle Zwecke. Für kommerzielle Zwecke kostet es USD 50 pro Programmierer. Die vollständige Dokumentation ist auch online zugänglich, und zwar hier:
http://help.madshi.net/madExcept.htm
[edit=sakura] Schönheitsfehler beseitigt. Mfg, sakura[/edit]