Auslösen von Laufzeitfehlern In Windows werden Fehler entweder von der Hardware direkt oder über die Funktion RaiseException, die in der kernel32.dll enthalten ist, ausgelöst. Die Deklaration der Funktion für Visual Basic ist im Folgenden angegeben: Private Declare Sub RaiseException Lib "kernel32.dll" ( _
ByVal dwExceptionCode As Long, _
ByVal dwExceptionFlags As Long, _
ByVal nNumberOfArguments As Long, _
ByRef lpArguments As Long _
) Listing 1: Deklaration der API-Funktion RaiseException Im ersten Parameter der Funktion RaiseException, dwExceptionCode gibt man die Nummer der auszulösenden Exception an. Will man in der eigenen Anwendung solche Exceptions benutzen, dann sollte man als Nummer eine selbstgewählte Zahl benutzen. Bestimmte Werte sind bereits für Windows-eigene Fehler vorbelegt. Die entsprechenden Werte kann man den Header-Dateien WinBase.h entnehmen (Definitonen für EXCEPTION_ACCESS_VIOLATION etc.). Der Parameter dwExceptionflags kann entweder den Wert der Konstante EXCEPTION_NONCONTINUABLE (= 1) annehmen, was so viel bedeutet, wie dass die Anwendung nach Eintreten der Exception beendet werden muss. Gibt man stattdessen den Wert 0 an, dann kann die Anwendung die Exception behandeln und muss nicht zwingend beendet werden. Bei genauerer Betrachtung bemerkt man, dass die meisten "Standardfehler" in Visual Basic, also beispielsweise Fehler 11 (Division durch Null) nur eine Verkapselung der Windows-eigenen Exceptions darstellen. Nehmen wir als Beispiel den folgenden Aufruf der API-Funtkion RaiseException und sehen wir uns die Reaktion der VB-Anwendung auf den Aufruf genauer an: Call RaiseException(EXCEPTION_INT_DIVIDE_BY_ZERO, 0&, 0&, 0&) Listing 2: Simulation einer Division durch Null Die Reaktion von Visual Basic auf diesen Aufruf ist so, wie man es erwartet hätte: Es wird Fehler Nummer 11, also der Fehler für die Division durch Null in Visual Basic ausgelöst. Der Grund dafür liegt darin, dass die Laufzeitumgebung für VB-Anwendungen bereits einige der Fehler "filtert" und in VB-eigene Fehler umwandelt. Allerdings wird dies nicht bei allen möglichen Fehlern getan, weshalb es auch immer wieder zu Fehlern kommt, für die VB keine vordefinierte Meldnungsbox anzeigt und die Anwendung mit einer entsprechenden Box von Windows beendet. Das ist beispielsweise bei Exceptions vom Typ EXCEPTION_ACCESS_VIOLATION der Fall. Diese Unzulänglichkeit von Visual Basic kann man jedoch auch in den Griff bekommen und bei einigen Exceptions kann die Anwendung sogar weiterlaufen. Natürlich sollte man die Anwendung aus Sicherhheitsgründen nach einem unbehandelten und damit "unbekannten" Fehler beenden, damit der Benutzer nicht aufgrund daraus resultierender Fehler Daten verliert. Viele Programmierer werden sich an dieser Stelle denken, dass sie ohnehin beim Schreiben von Code aufpassen, dass es zu keinen unerwarteten oder gar unbehandelten Fehlern kommen kann. Jedoch darf man nicht ausser Acht lassen, dass auch in Programmen verwendete Third-Pary-Controls, also Steuerelemente und DLLs, die von einem anderen Hersteller stammen, solche Fehler auslösen können. Auch diese Fehler können mit der beschriebenen Methode in den Griff bekommen werden. Von Visual Basic nicht behandelte Laufzeitfehler behandeln Um von Visual Basic nicht in das Gewand eines Visual Basic-Fehlers gehüllte Laufzeitfehler zu behandeln und damit einen Programmabsturz zu verhindern, kann die Win32-API-Funktion SetUnhandledExceptionFilter aus der kernel32.dll herangezogen werden. Diese Funktion ersetzt die durch Windows vorgegebene Standardfehlerbehandlung durch eine benutzerdefinierte Fehlerbehandlungsroutine in der eigenen Anwendung. Standardmäßig zeigt Windows bei Eintreten eines unbehandelten Fehlers, der nicht von Visual Basic erkannt wird, ein Meldungsfeld an, in dem Informationen zum Fehler zu finden sind und das in Windows XP die Möglichkeit bietet, einen Fehlerbericht an Microsoft zu übermitteln: Private Declare Function SetUnhandledExceptionFilter Lib "kernel32.dll" ( _
ByVal lpTopLevelExceptionFilter As Long _
) As Long Listing 3: Deklaration der Funktion SetUnhandledExceptionFilter SetUnhandledExceptionFilter erwartet im Parameter lpTopLevelExceptionFilter einen Zeiger auf eine Prozedur, die folgende Schnittstelle aufweist: Public Function ExceptionHandler( _
ByRef lpException As EXCEPTION_POINTERS _
) As Long
End Function Listing 4: Prototyp der Fehlerbehandlungsprozedur Häufig soll die Anwendung bei Auftreten eines Fehlers nicht sofort beendet werden. Zu diesem Zweck muß die Funktion den Wert EXCEPTION_CONTINUE_EXECUTION zurückgeben. Dies kann durch die Anweisung ExceptionHandler = EXCEPTION_CONTINUE_EXECUTION erreicht werden. Das Umleiten der Fehlerbehandlung in die Funktion ExceptionHandler kann folgendermaßen erfolgen: m_lpOldExceptionProc = SetUnhandledExceptionFilter(AddressOf ExceptionHandler) Listing 5: Umleiten der Fehlerbehandlung in eine benutzerdefinierte Prozedur Der Rückgabewert von SetUnhandledExceptionFilter ist ein Funktionszeiger auf die Windows-eigene Standardprozedur zur Fehlerbehandlung. Dieser Zeiger wird wieder benötigt, wenn die Fehlerbehandlung an Windows übergeben werden soll. Damit die Fehlermeldung von Windows nicht angezeigt wird, sollte ein Aufruf der API-Funktion SetErrorMode mit SEM_NOOPENFILEERRORBOX als Wert des ersten Parameters erfolgen. Tests unter Windows XP zeigten, dass dies nicht erforderlich ist -- bei Umleitung der Fehler in eine eigene Fehlerbehanlungsroutine zeigte Windows auch keine Fehlermeldungen für die Anwendung an. Die API-Funktion wird folgendermassen deklariert: Private Declare Function SetErrorMode Lib "kernel32.dll" ( _
ByVal wMode As Long _
) As Long
Private Const SEM_FAILCRITICALERRORS As Long = &H1&
Private Const SEM_NOGPFAULTERRORBOX As Long = &H2&
Private Const SEM_NOALIGNMENTFAULTEXCEPT As Long = &H4&
Private Const SEM_NOOPENFILEERRORBOX As Long = &H8000& Listing 6: Deklaration von SetErrorMode und mögliche Konstanten Folgendes Listing zeigt die Deklarationen der für die Fehlerbehandlung erforderlichen Strukturen. Die benutzerdefinierte Fehlerbehandlungsprozedur, die wir in unserem Beispiel ExceptionHandler nennen, bekommt von Windows im Parameter lpException ein Objekt vom Typ EXCEPTION_POINTERS übergeben, das wiederum Referenzen auf Objekte der Typen EXCEPTION_RECORD sowie CONTEXT besitzt. Die Struktur EXCEPTION_RECORD enthält Informationen über den aktuellen Fehler sowie einen Verweis auf ein weiteres EXCEPTION_RECORD-Objekt in Form eines Zeigers, der über den Member pExceptionRecord implementiert wird. Die Struktur CONTEXT ist je nach eingesetztem Prozessor anders aufgebaut. Das Aussehen kann den entsprechenden C-Header-Dateien entnommen werden.
Private Type EXCEPTION_RECORD
ExceptionCode As Long
ExceptionFlags As Long
pExceptionRecord As Long ExceptionAddress As Long
NumberParameters As Long
Information(0 To EXCEPTION_MAXIMUM_PARAMETERS - 1) As Long
End Type
Public Type EXCEPTION_POINTERS
ExceptionRecord As Long ContextRecord As Long End Type Listing 7: Strukturdefinitionen für die verwendeten Win32-API-Funktionen Für die Fehlerbehandlung sind die Member der Struktur EXCEPTION_RECORD von besonderem Interesse, da in ihnen Informationen zum eingetretenen Fehler zu finden sind. Der Member ExceptionCode enthält den Fehlercode der aufgetretenen Ausnahme. Dabei kann es sich entweder um einen der Standardfehlernummern handeln, die beispielsweise bei Überlauf, Unterlauf und Divison durch Null auftreten, oder um einen benutzerdefinierten Fehler, der beispielsweise in einer in der Anwendung verwendeten ActiveX-Komponente, die mit C++ geschrieben wurde, auftritt. ExceptionFlags beinhaltet Informationen darüber, ob die Anwendung weiterhin ausgeführt werden kann, oder ob ein Beenden unumgänglich ist. Das Mitglied pExceptionRecord enthält, wie bereits angeführt, einen Zeiger auf ein weiteres Fehlerobjekt. Beim Wert in ExceptionAddress handelt es sich um die Speicheradresse, an der die Anweisung steht, die den Fehler verursacht hat. Die anderen Member können weiterreichende Informationen enthalten, auf die wir hier nicht weiter eingehen wollen. Besondere Aufmerksamkeit muß den verschachtelten Fehlern gewidmet werden. Im Falle verschachtelter Fehlerenthält die im Parameter lpException der Prozedur ExceptionHandler übergebene Struktur nicht einen Verweis auf den eigentlichen Fehler, sondern auf einen Fehler, der durch den entstandenen Fehler ausgelöst wurde. Es kann sich dabei um eine ganze Kette von Fehlern handeln, in der jeder Fehler durch eine EXCEPTION_RECORD-Struktur dargestellt wird. Die einzelnen Fehler sind in Reihenfolge ihres Auftretens über Zeiger im Mitglied pExceptionRecord miteinander verbunden. Um nun an den ursprünglichen Fehler zu gelangen, muß die einfach verkettete Liste von Fehlern bis an ihr Ende, das eigentlich der Anfang ist (der am weitesten zurückliegende Fehler), vordringen. Dazu kann man eine abgewandelte Form der Win32-API-Funktion RtlMoveMemory, die auch unter dem Namen CopyMemory bekannt ist, eingesetzt werden:
Private Declare Sub CopyExceptionRecord Lib "kernel32.dll" Alias "RtlMoveMemory" ( _
ByRef Destination As EXCEPTION_RECORD, _
ByVal Source As Long, _
ByVal Length As Long _
) Listing 8: Deklaration der Funktion CopyMemory zum Durchlaufen einer Liste von Fehlereinträgen. Folgendes Listing zeigt, wie der ursprüngliche Fehler ermittelt wird: Dim er As EXCEPTION_RECORD
Call CopyExceptionRecord(er, lpException.ExceptionRecord, LenB(er))
Do Until er.pExceptionRecord = 0&
Call CopyExceptionRecord(er, er.pExceptionRecord, LenB(er))
Loop Listing 9: Bewegung an den Anfang der Fehlerliste Praktische Verwendung Für den praktischen Einsatz der beschriebenen Art der Fehlerbehandlung in Visual Basic-Anwendungen bestehen zwei Möglichkeiten. So können innerhalb der Fehlerbehandlungsprozedur über die Methode Raise des Err-Objekts entsprechende Visual Basic-eigene Laufzeitfehler ausgelöst werden. Bestehende Fehlerbehandlungsroutinen müssen in diesem Fall nicht erweitert werden, da der Fehler wie jeder andere Visual Basic-eigene Fehler behandelt werden kann. Für die Beispielanwendung wurde ein anderer Ansatz gewählt: Eine Klasse löst bei Auftreten eines Laufzeitfehlers ein Ereignis aus, in das Informationen über den Fehler übergeben werden. Dadurch besteht bei der Fehlerbehandlung auch Zugriff auf Informationen zur Anweisung, welche den Laufzeitfehler ausgelöst hat. |