Behandlung der von Visual Basic nicht behandelten Ausnahmen

Einleitung

Visual Basic erlaubt zwar die Behandlung von Laufzeitfehlern, einige Fehler werden aber von der Visual-Basic-eigenen Fehlererkennung nicht automatisch in das Gewand eines Visual-Basic-Fehlers gekleidet. In diesem Artikel wird beschrieben, wie man diese Fehler trotzdem behandeln und damit unter Umständen den unkontrollierten Absturz der Anwendung verhindern kann. In den meisten Projekten sind die vorgestellten Techniken nicht erforderlich, da die Fehlerbehandlung von Visual Basic ausreichend ist.

Fehlerbehandlung in Visual Basic

Visual Basic besitzt einen Mechanismus zur Fehlerbehandlung, der als Frame Based Exception Handling bezeichnet wird. Dabei bestehen Prozeduren aus der eigentlichen Implementierung der Funktionalität und Bereichen, in denen die Fehlerbehandlung durchgeführt wird. Die Fehlerbehandlung erfolgt über On Error GoTo…. Mittels On Error Resume Next können Laufzeitfehler unterdrückt und das Beenden der Anwendung mit einer Fehlermeldung unterbunden werden. Durch das Behandeln von Laufzeitfehlern kann eine Anwendung mit einem definierten Zustand beendet werden.

Auch wenn der Mechanismus von Visual Basic bei einfachen Anwendungen, die ohne die Benutzung von Win32-API-Funktionen auskommen, ausreicht, gibt es Laufzeitfehler, die nicht von Visual Basic selbst in einen Visual-Basic-Fehler verpackt werden. Sie überraschen den Benutzer der Anwendung unliebsam mit einer vom Betriebssystem angezeigten Fehlermeldung. Diese Fehler, beispielsweise Zugriffskonflikte, können auch nicht mit den vorgestellten Methoden zur Fehlerbehandlung unterdrückt oder behandelt werden.

Insbesondere bei Verwendung von Win32-API-Funktionen kommt es durch unsachgemäße Anwendung zu Laufzeitfehlern, meist sogenannte Schutzverletzungen (engl.: protection faults). Ein Beispiel dafür ist die Verwendung der Funktion CopyMemory zum Kopieren eines bestimmten Speicherbereichs an einen anderen. Fehlen der Anwendung die Rechte, um auf die betreffenden Bereiche zuzugreifen, wird eine Schutzverletzung ausgelöst. Im Sinne des Benutzers wäre es wünschenswert, wenn in solchen Fällen das Programm vor dem Beenden Aufräumarbeiten wie das Speichern von Änderungen in eine Sicherungsdatei durchführen könnte.

Laufzeitfehler in Visual-Basic-Anwendungen können in folgende Kategorien eingeteilt werden:

  1. Behandelte Visual-Basic-eigene Laufzeitfehler. Bei Auftreten dieser Fehler kann die Ausführung der Anwendung fortgesetzt werden.

  2. Unbehandelte Visual-Basic-eigene Laufzeitfehler. Die Anwendung bricht mit einer Visual-Basic-eigenen Fehlermeldung ab und kann keinen Code zum Aufräumen ausführen.

  3. Von Visual Basic nicht behandelte Laufzeitfehler. Die Anwendung bricht mit einer Windows-eigenen Fehlermeldung ab und kann keine Aufräumarbeiten erledigen.

In der Praxis sind besonders Fehler der Typen 2 und 3 störend. Fehlertyp 2 kann vermieden werden, indem entsprechende Fehlerbehandlungsroutinen dem Quellcode hinzugefügt werden, beispielsweise On Error GoTo… und On Error Resume Next. Um Fehlertyp 3 in eine handhabbare Form überzuführen, muß man auf die Win32-API zurückgreifen.

Laufzeitfehler unter Microsoft Windows

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 dieser 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 _
)
Die Deklaration der Funktion RaiseException.

Im ersten Parameter der Funktion RaiseException, dwExceptionCode wird die Nummer der auszulösenden Ausnahme angegeben. Will man in der eigenen Anwendung solche Ausnahmen werfen, sollte man als Nummer eine selbst gewählte Zahl festlegen. Bestimmte Werte sind bereits für Windows-eigene Fehler vorbelegt. Die entsprechenden Werte kann man der Headerdatei WinBase.h entnehmen (Definitionen für EXCEPTION_ACCESS_VIOLATION etc.). Der Parameter dwExceptionflags kann entweder den Wert der Konstanten EXCEPTION_NONCONTINUABLE (= 1) annehmen, der besagt, daß die Anwendung nach Eintreten der Ausnahme beendet werden muß. Wird stattdessen der Wert 0 angegeben, kann die Anwendung die Ausnahme behandeln und muß nicht zwingend beendet werden.

Die meisten „Standardfehler“ in Visual Basic, also beispielsweise Fehler 11 (Division durch Null), stellen nur eine Kapselung der Windows-eigenen Ausnahmen dar. Der nachfolgende Aufruf der API-Funktion RaiseException führt in Visual Basic zum Auslösen von Laufzeitfehler 11:

Call RaiseException(EXCEPTION_INT_DIVIDE_BY_ZERO, 0&, 0&, 0&)
Auslösen von Laufzeitfehler 11 mittels der Funktion RaiseException.

Die Laufzeitumgebung für Visual-Basic-Anwendungen filtert bereits einige der Laufzeitfehler und wandelt sie in Visual-Basic-eigene Fehler um. Dies wird jedoch nicht bei allen möglichen Fehlern getan. In Folge werden für nicht von der Laufzeitumgebung umgewandelte Fehler von Visual Basic keine vordefinierten Meldungsfelder anzeigt und die Anwendung mit einer entsprechenden Fehlermeldung von Windows beendet. Das ist beispielsweise bei Ausnahmen des Typs EXCEPTION_ACCESS_VIOLATION der Fall.

Diese Unzulänglichkeit von Visual Basic kann man jedoch in den Griff bekommen und im Falle einiger Ausnahmen kann die Anwendung sogar weiterlaufen. Dennoch sollte nach Auftreten eines unbehandelten und damit nicht bekannten Fehlers die Anwendung aus Sicherheitsgründen beendet werden, damit der Benutzer nicht aufgrund daraus resultierenden Folgefehlern Daten verliert. Ein Sichern des Programmstatus ist daher in diesem Fall unerläßlich. Die vorgestellte Methode eignet sich auch zur Behandlung von Fehlern, die in Komponenten von Drittanbietern, also Steuerelementen und DLLs, auftreten oder von diesen ausgelöst werden.

Von Visual Basic nicht behandelte Laufzeitfehler behandeln

Um von Visual Basic nicht in das Gewand eines Visual-Basic-Fehlers gekleidete Laufzeitfehler zu behandeln und damit einen Programmabsturz zu verhindern, kann die Win32-Win32-API-Funktion SetUnhandledExceptionFilter aus der kernel32.dll herangezogen werden. Diese Funktion ersetzt die durch Windows vorgegebene Standardfehlerbehandlung durch eine Fehlerbehandlungsroutine in der eigenen Anwendung. Standardmäßig zeigt Windows bei Eintreten eines unbehandelten Fehlers, der nicht von Visual Basic erkannt wird, ein Meldungsfeld mit Informationen zum Fehler an, 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
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
Prototyp der Fehlerbehandlungsprozedur.

Häufig soll die Anwendung bei Auftreten eines Fehlers weiterlaufen und 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 vorgenommen werden:

m_lpOldExceptionProc = _
    SetUnhandledExceptionFilter(AddressOf ExceptionHandler)
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 Win32-API-Funktion SetErrorMode mit SEM_NOOPENFILEERRORBOX als Wert des ersten Parameters erfolgen. Tests unter Windows XP zeigten, daß dies nicht erforderlich ist – bei Umleitung der Fehler in eine eigene Fehlerbehandlungsroutine zeigte Windows auch keine Fehlermeldungen für die Anwendung an. Die Win32-API-Funktion SetErrorMode wird folgendermaßen 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&
Deklaration von SetErrorMode und möglicher Konstanten.

Folgendes Listing zeigt die Deklarationen der für die Fehlerbehandlung nötigen Strukturen. Die benutzerdefinierte Fehlerbehandlungsprozedur, die im Beispiel ExceptionHandler heißt, bekommt von Windows im Parameter lpException eine Struktur des Typs EXCEPTION_POINTERS übergeben, die wiederum Zeiger auf Strukturen der Typen EXCEPTION_RECORD sowie CONTEXT enthält. Die Struktur EXCEPTION_RECORD besteht aus Informationen über den aktuellen Fehler sowie einen Zeiger auf eine weitere EXCEPTION_RECORD-Struktur, der über das Mitglied pExceptionRecord zugänglich ist. Die Struktur CONTEXT ist je nach eingesetztem Prozessor unterschiedlich aufgebaut. Das Aussehen kann den entsprechenden C-Headerdateien entnommen werden:


' Datenstruktur zum Speichern von Informationen über den eingetretenen Fehler.
Private Type EXCEPTION_RECORD
    ExceptionCode As Long
    ExceptionFlags As Long
    
    ' Zeiger auf einen anderen 'EXCEPTION_RECORD'-Eintrag.
    pExceptionRecord As Long
    
    ExceptionAddress As Long
    NumberParameters As Long
    Information(0 To EXCEPTION_MAXIMUM_PARAMETERS - 1) As Long
End Type

Private Type EXCEPTION_POINTERS
    
    ' Zeiger auf eine 'EXCEPTION_RECORD'-Struktur.
    ExceptionRecord As Long
    
    ' Zeiger auf eine 'CONTEXT'-Struktur.
    ContextRecord As Long
End Type
Strukturdefinitionen für die verwendeten Win32-API-Funktionen.

Für die Fehlerbehandlung sind die Mitglieder der Struktur EXCEPTION_RECORD von besonderem Interesse, da sie Informationen zum eingetretenen Fehler bereitstellen. Das Mitglied 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 auftritt. ExceptionFlags bietet Informationen darüber, ob die Anwendung weiter ausgeführt werden kann oder ob ein Beenden unumgänglich ist. Das Mitglied pExceptionRecord enthält, wie bereits angeführt, einen Zeiger auf eine weitere Fehlerstruktur. Beim Wert in ExceptionAddress handelt es sich um die Speicheradresse der Anweisung, die den Fehler ausgelöst hat. Die anderen Mitglieder können weiterreichende Informationen enthalten, auf die hier nicht näher eingegangen werden soll.

Besondere Aufmerksamkeit muß den verschachtelten Fehlern gewidmet werden. Im Falle verschachtelter Fehler enthä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:


' Angepaßte Version von 'CopyMemory' zum Folgen der Zeiger bei
' 'EXCEPTION_RECORD'-Ketten.
Private Declare Sub CopyExceptionRecord Lib "kernel32.dll" _
    Alias "RtlMoveMemory" _
( _
    ByRef Destination As EXCEPTION_RECORD, _
    ByVal Source As Long, _
    ByVal Length As Long _
)
Deklaration der Funktion CopyMemory zum Durchlaufen einer Liste von Fehlereinträgen.

Folgendes Listing zeigt, wie der ursprüngliche Fehler ermittelt wird:


' Der Anwendung mitteilen, daß es zu einer Ausnahme gekommen ist. Wenn
' 'lpException.ExceptionRecord' ungleich 0 ist, handelt es sich um eine Kette
' von verschachtelten Ausnahmen. In diesem Fall muß den Zeigern zurück bis zur
' ursprünglichen Ausnahme gefolgt werden.
Dim er As EXCEPTION_RECORD

' Ermitteln des aktuellen Ausnahmeeintrags.
Call CopyExceptionRecord(er, lpException.ExceptionRecord, LenB(er))

' Folgen der Zeiger bis zur ursprünglichen Ausnahme.
Do Until er.pExceptionRecord = 0&
    Call CopyExceptionRecord(er, er.pExceptionRecord, LenB(er))
Loop
Bewegung an den Anfang der Fehlerliste.

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.

Schlußwort

Dieser Artikel deckt die für Visual Basic relevanten Bereiche der erweiterten Fehlerbehandlung zu einem großen Teil ab. Zusätzliche Informationen zu eingetretenen Fehlern können mittels eigener Win32-API-Funktionen ermittelt werden. Zu diesem Thema existiert auf der MSDN-Website ein Artikel aus der Artikelreihe Bugslayer, MSJ, August 1998. Darin wird eine in C++ entwickelte DLL vorgestellt, die auch von Visual-Basic-Anwendungen aus genutzt werden kann, um an zusätzliche Informationen zum eingetretenen Fehler zu gelangen.

Weiterführende Informationen

Weitere Informationen zur Behandlung von Laufzeitfehlern, die von Visual Basic nicht behandelt werden, finden sich im Artikel No Exception Errors, My Dear Dr. Watson von Jonathan Lunman, der in der Ausgabe des Visual Basic Programmer’s Journal vom Mai 1999, S. 108, erschienen ist. Außerdem wurden Informationen aus dem in der selben Zeitschrift erschienenen Artikels Swat Tough Bugs von Ken Cowan (Ausgabe Juli 1998, S. 117 ff.) berücksichtigt.

Downloads

Beispielprojekt (ExceptionHandler.zip)

Projekt im Visual-Basic-6.0-Format.