Die Community zu .NET und Classic VB.
Menü

Behandlung von Exceptions in VB-Anwendungen

 von 

Inhalt  

In dieser Ausgabe der Kolumne betrachten wir einen speziellen Aspekt des Themas Fehlerbehandlung. Dabei zeigen wir eine Methode, die verwendet werden kann, um Ausnahmefehler, die in einer mit Visual Basic geschriebenen Anwendung auftrefen, behandelt werden können. Der Inhalt dieses Artikels richtet sich an fortgeschrittene Programmierer und ist für Einsteiger nicht von Bedeutung. Ausserdem sollten die hier vorgestellten Methoden nur mit grösster Vorsicht in der Praxis eingesetzt werden.

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 Implementierung eines solchen Fehlerhandlers geschieht generell über On Error GoTo.... Weiters besteht die Möglichkeit, mit On Error Resume Next Fehler zu unterdrücken und das Terminieren der Anwendung mit einer entsprechenden Meldungsbox zu unterbinden. In der Regel ist es wohl sinnvoller, die Anwendung trotz eines Fehlers weiterlaufen zu lassen, als ohne irgendwelche Backups etc. anzulegen, das Programm von der Laufzeitumgebung beenden zu lassen.

Auch wenn der Mechanismus von Visual Basic bei einfachen Anwendungen, die ohne die Benutzung von API-Funktionen auskommen, ausreicht, gibt es trotzdem Fehler, die nicht von Visual Basic selbst in einen VB-Fehler "gepackt" werden und dann den Benutzer der Anwendung unliebsam mit einer Windows-Fehlermeldung überraschen. Diese Fehler können auch nicht mit den vorhin besprochenen Methoden unterdrückt oder behandelt werden. Ein Beispiel dafür wären Zugriffskonflikte.

Besonders bei Verwendung von API-Funktionen in Programmen, um verschiedene Dinge durchführen zu können, die auf einem anderen Weg nicht möglich sind, kommt es bei unsachgemässer Verwendung oder aber auch in wirklich unerwarteten Situationen zu Ausnahmefehlern, meist sog. Allgemeinen Schutzverletzungen (engl.: General Protection Faults). Bestes Beispiel dafür ist der Einsatz von CopyMemory (auch als RtlMoveMemory bekannt) zum Kopieren eines bestimmten Speicherbereichs an einen anderen. Besitzt die Anwendung nicht die Rechte, um auf die betreffenden Bereiche zuzugreifen, dann kommt es zu einer allgemeinen Schutzverletzung. Im Sinne des Benutzers wäre es wünschenswert, wenn in solchen Fällen das Programm vor dem wirklichen Beenden noch Aufräumarbeiten (z.B. Speichern von Änderungen in eine Backupdatei) erledigen würde.

Klassifikation von Fehlern in VB  

Den Ausführungen des vorigen Abschnitts zufolge können Fehler in VB-Anwendungen demnach in folgende Kategorien eingeteilt werden:

  1. Behandelte VB-eigene Laufzeitfehler
    In diesem Fall kann die Ausführung der Anwendung fortgesetzt werden.
  2. Unbehandelte VB-eigene Laufzeitfehler
    Die Anwendung bricht mit einer VB-eigenen Meldnungsbox ab. Es kann kein Code mehr zum Aufräumen ausgeführt werden.
  3. Durch VB nicht behandelte Exceptions
    Die Anwendung bricht mit einer Windows-eigenen Fehlermeldung ab. Es kann kein Code mehr zum Aufräumen ausgeführt werden.

In praktischen Anwendungen stören die Fehler vom Typ 2 und 3. 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 erträglichere Form überzuführen, muss man auf das Windows32-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 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.

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

Public Type EXCEPTION_POINTERS
    ExceptionRecord As Long     ' Zeiger auf eine 'EXCEPTION_RECORD'-Struktur.
    ContextRecord As Long       ' Zeiger auf eine 'CONTEXT'-Struktur.
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:

' Angepasste 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 _
)

Listing 8: 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

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.

Schlusswort  

Dieser Artikel deckt die für Visual Basic relevanten Bereiche der erweiterten Fehlerbehandlung zu einem grossen Teil ab. Allerdings ist es auch möglich, erweiterte Informationen zum eingetretenen Fehler mittels spezieller API-Funktionen zu ermitteln. Zu diesem Thema existiert auf der MSDN-Homepage ein Artikel aus der Artikelreihe Bugslayer, MSJ, August 1998. Dort wird eine in C++ entwickelte DLL vorgestellt, die auch von VB-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. 117ff.) berücksichtigt.

Downloads  

Hier besteht die Möglichkeit, ein vollständiges VB6-Beispiel herunterzuladen, um einen Einblick in den Einsatz der Technik in eigenen Projekten zu erhalten:

VB6-Beispiel zum Artikel [8130 Bytes]

Ihre Meinung  

Falls Sie Fragen zu diesem Artikel haben oder Ihre Erfahrung mit anderen Nutzern austauschen möchten, dann teilen Sie uns diese bitte in einem der unten vorhandenen Themen oder über einen neuen Beitrag mit. Hierzu können sie einfach einen Beitrag in einem zum Thema passenden Forum anlegen, welcher automatisch mit dieser Seite verknüpft wird.

Archivierte Nutzerkommentare 

Klicken Sie diesen Text an, wenn Sie die 2 archivierten Kommentare ansehen möchten.
Diese stammen noch von der Zeit, als es noch keine direkte Forenunterstützung für Fragen und Kommentare zu einzelnen Artikeln gab.
Aus Gründen der Vollständigkeit können Sie sich die ausgeblendeten Kommentare zu diesem Artikel aber gerne weiterhin ansehen.

Kommentar von Herfried K. Wagner [MVP] am 13.07.2004 um 18:58

Dies ist leider, soweit mir bekannt ist, nicht möglich.

Kommentar von Peter Körner am 13.08.2003 um 12:25

Hallo.
Ist es auch möglich normale VB-Laufzeitfehler zu handlen??
Also wenn ich auf der Form (im Beispiel) einen Button einbaue und dann

MsgBox 0 / 1

in das Click-Event sachreibe, dann wird dieser Fehler nämlich nicht abgefangen...

Thx, Peter