AUDACIA Software

External Exception EEFFACE

Warum Delphi C++-Exceptions nicht behandeln kann - und wie sich das ändern läßt.
Moritz Beutel, 02.10.2008, 28.02.2010



  1. Einführung
  2. Woher kommt diese Fehlermeldung?
  3. Was passiert, wenn die Exception geworfen wurde?
  4. Warum kann Delphi C++-Exceptions nicht behandeln?
  5. Delphi-Exceptions in C++Builder
  6. C++-Exceptions in Delphi - Der Ansatz von Early Ehlinger
  7. C++-Exceptions in Delphi - mein Ansatz
  8. Advanced Exception Dispatching
  9. Und warum hat Borland das nicht von Anfang an so implementiert?
  10. Windows Error Reporting
  11. Zusammenfassung
  12. Referenzen
  13. Kommentare



Einführung


So gut wie alle C++Builder-Programmierer - und unglücklicherweise auch viele der Benutzer ihrer Programme - kennen die leidige "External Exception EEFFACE"-Fehlermeldung. Sie erscheint, wenn eine C++-Exception nicht explizit gefangen wird und in VCL-Code gerät.

Der Fehler läßt sich sehr einfach reproduzieren. So etwa könnte er in einem Projekt auftreten:
#include <vector>
// ...
void __fastcall TFrmMain::BtnThrowExceptionClick(TObject* Sender)
{
    std::vector <String> myStrings;

    myStrings.push_back ("Some string");
    Caption = myStrings.at (1);
}

Wird der Button angeklickt, so erscheint diese Meldung:

External Exception EEFFACE

Nach dem Schließen des Dialoges läuft die Anwendung normal weiter.


Woher kommt diese Fehlermeldung?


Verursacht wird sie, wie oben erklärt, von einer C++-Exception. Weshalb obiges Beispiel eine Exception wirft, sollte offensichtlich sein; wir benutzen den Debugger, um dies genauer nachzuverfolgen. Wenn wir in std::vector<>::at() steppen, landen wir hier:
// vector, l.640ff
	const_reference at(size_type _Pos) const
		{	// subscript nonmutable sequence with checking
		if (size() <= _Pos)
			_Xran();
		return (*(begin() + _Pos));
		}

Nun springen wir in _Xran() hinein und treffen auf den Verursacher:
// vector, l. 1231ff
	void _Xran() const
		{	// report an out_of_range error
		_THROW(out_of_range, "invalid vector<T> subscript");
		}

std::vector<>::at() wirft also eine Exception vom Typ std::out_of_range, wenn wir einen ungültigen Index übergeben. Die Fehlermeldung ist eindeutig und gibt dem Programmierer einen Hinweis, wo er nach dem Problem suchen müßte - würde sie nur angezeigt!


Was passiert, wenn die Exception geworfen wurde?


Solche Dinge lassen sich ganz hervorragend im Disassembler nachverfolgen. Der Compiler generiert einen Aufruf der RTL-Funktion _ThrowExceptionLDTC(), deren Implementation sich, wie der Großteil des Exception-Handling-Mechanismus, in $(BDS)\source\cpprtl\Source\except\xx.cpp einsehen läßt. Diese Funktion gibt ihre Argumente an tossAnException() weiter, das seinerseits die Win32-Funktion RaiseException() aufruft. RaiseException() sucht nach einem passenden Exception-Handler, und der nächste, den es findet, ist dieser hier:
// Controls.pas, l. 9059ff
procedure TWinControl.MainWndProc(var Message: TMessage);
begin
  try
    try
      WindowProc(Message);
    finally
      FreeDeviceContexts;
      FreeMemoryContexts;
    end;
  except
    Application.HandleException(Self);
  end;
end;

Wie zu erkennen ist, befindet sich in der Window-Prozedur der VCL ein unkonditioneller Handler; alle Exceptions werden hier gefangen. Handelt es sich um eine Delphi-Exception, wird sie von Application.HandleException() bei Bedarf weiterverteilt, ansonsten eine hübsche Fehlermeldung aussagekräftigen Inhalts generiert. OS-Exceptions übersetzt der Handler in die Delphi-Äquivalente, und wenn Delphi die Exception nicht erkennt, wird eine Standardfehlermeldung mit dem Exception-Code generiert. Ein paar dieser Exception-Codes sind in System.pas definiert:
const { copied from xx.h }
  cContinuable        = 0;
  cNonContinuable     = 1;
  cUnwinding          = 2;
  cUnwindingForExit   = 4;
  cUnwindInProgress   = cUnwinding or cUnwindingForExit;
  cDelphiException    = $0EEDFADE;
  cDelphiReRaise      = $0EEDFADF;
  cDelphiExcept       = $0EEDFAE0;
  cDelphiFinally      = $0EEDFAE1;
  cDelphiTerminate    = $0EEDFAE2;
  cDelphiUnhandled    = $0EEDFAE3;
  cNonDelphiException = $0EEDFAE4;
  cDelphiExitFinally  = $0EEDFAE5;
  cCppException       = $0EEFFACE; { used by BCB }

0x0EEFFACE ist der Code, den C++Builder für C++-Exceptions benutzt. Dies resultiert, wie nun nachvollziehbar, in der bekannten Fehlermeldung.


Warum kann Delphi C++-Exceptions nicht behandeln?


Das ist begründet im speziellen Verhältnis von Delphi und C++. Zu Anfang der neunziger Jahre war das dominierende GUI-Framework in Borland C++ die OWL. Als diese aber ihre Marktanteile nach und nach an Microsofts Visual C++ mit MFC verlor, entschied sich Borland, die Weiterentwicklung der OWL aufzugeben und stattdessen das VCL-Framework, das seit dem erstmaligen Erscheinen 1995 in Delphi 1 den IDE-Markt revolutioniert hatte, auch für C++ zu adaptieren. (Es ist nicht nur möglich, sondern auch sehr wahrscheinlich, daß dies bereits während der Entwicklung von Delphi entschieden wurde, aber das weiß ich nicht.) Also mußte irgendeine Möglichkeit der Interaktion zwischen Pascal- und C++-Code gefunden werden. Dies ist gar nicht so einfach: das Konzept der Interface-Sektionen in Pascal-Units war wohldefiniert und eindeutig, die C-/C++-Entsprechung, also Header-Dateien, waren alles andere. Hätte man den Pascal-Compiler in die Lage versetzen wollen, C++-Header-Dateien verstehen zu können, so müßte er in der Lage sein, Abhängigkeiten aller Art zu anderen Headerdateien aufzulösen, er hätte irgendwie die Zusammengehörigkeit von Header-Dateien und Quelldateien erkennen müssen (denn in C++ ist es problemlos möglich, einen Header zu schreiben und die zugehörigen Implementationen auf beliebig viele Module zu verteilen - oder auch umgekehrt). Auch müßte er alle C++-Sprachmittel, z.B. Templates, beherrschen, ja sogar in der Lage sein, Template-Metaprogramme aufzulösen (auch wenn man das 1995 sicherlich noch nicht zu berücksichtigen hatte :D). Kurzum, es ist praktisch unmöglich, selbst für eine C-Headerdatei automatisch ein äquivalentes Delphi-Unit-Interface zu generieren. Es gab diverse Ansätze, so einen Header-Converter zu schreiben, und es gibt auch Tools, die den manuellen Konversionsprozeß vereinfachen, aber all diese Rudimentarien sind von einer vollständigen Lösung weit entfernt.

Sicherlich auch aufgrund dieser Komplikationen entschied man sich, die Sache andersherum anzugehen. Als 1997 mit C++Builder Borlands erstes VCL-gestütztes C++-Produkt veröffentlicht wurde, war diesem ein Delphi-Compiler beigelegt, der in der Lage war, Header-Dateien zu generieren, die den Interface-Abschnitten von Delphi-Units entsprachen. Um dies zu ermöglichen, mußte Borlands C++-Dialekt um einige der das Interface betreffenden Sprachkonstrukten Delphis erweitert werden; dies waren vor allem das Objektsystem, das auf der gemeinsamen Basisklasse TObject basierte, RTTI (__classid, __published), einige Sprachkonstrukte, für die C++ kein Äquivalent hatte und die sich mit C++-Mitteln auch nicht nachbilden ließen (__property, __closure), eine neue Aufrufkonvention (__fastcall) sowie eine Handvoll von #pragma-Direktiven. Das Ergebnis dieser Entwicklung war, daß die sprachlichen Möglichkeiten von Delphi, soweit sie die Schnittstelle betrafen, quasi zu einer Untermenge der sprachlichen Möglichkeiten von Borland C++ wurden.

(Natürlich ist diese Darstellung beschönigend. Komplexere Zusammenhänge erfordern manchmal den Rückgrif auf Delphi-Compilerdirektiven wie $HPPEMIT oder $EXTERNALSYM, und einige weniger weit verbreiteten Sprachmerkmale von Delphi, darunter der Metaklassen-Polymorphismus oder die etwas neueren Class-Helpers, können nicht direkt in C++ benutzt werden. Abgesehen von den Class-Helpers, die hauptsächlich dem Zweck dienten, Delphi beim Erscheinen von Delphi 8 für .NET in das .NET-Typsystem einzupassen und in Win32-Code eine geringe Relevanz haben, wird jedoch daran gearbeitet, diese Diskrepanzen zu beseitigen. In C++Builder 2009 wurde beispielsweise das __classmethod-Schlüsselwort eingeführt, mit dem es auch in C++ möglich ist, statische virtuelle Methoden zu deklarieren und zu überladen.)

Aus diesem Verhältnis ergibt sich auch, daß C++Builder die Exception-Typen und das Exception-Handling von Delphi kennt und beherrscht, umgekehrt jedoch nicht. Der C++-Exception-Mechanismus ist zudem ja nicht auf Objekte vom Typ std::exception und abgeleiteten Klassen limitiert, sondern praktisch jeder Typ kann für eine Exception mißbraucht werden. Zwar habe ich nicht die leiseste Ahnung, weshalb man ints oder floats als Exceptions benutzen wollte, aber es entspricht leider der Philosophie von C++, den Exception-Mechanismus nicht auf eine bestimmte Basisklasse zu beschränken.


Delphi-Exceptions in C++Builder


In Delphi sind Exception-Objekte auf von Sysutils::Exception abgeleitete Klassen beschränkt. (Es ist zwar prinzipiell möglich, jedes Objekt an Raise() zu übergeben, doch ist das bei weitem nicht so problematisch wie die totale Typagnostizität von throw in C++, denn über die Methoden von TObject und mit is und as läßt sich dynamisches Dispatching trotzdem leicht implementieren.) Sysutils::Exception und die abgeleiteten Klassen sind für C++Builder über die von Delphi generierten Headerdateien zugänglich. Demnach ist es möglich, Delphi-Exceptions in C++Builder zu werfen und zu fangen:
void __fastcall TFrmMain::BtnThrowDelphiExceptionClick(TObject* Sender)
{
    try
    {     // Wir können Delphi-Excepions per Wert werfen...
        throw ERangeError ("You are one off!");
    }
    catch (Exception& e) // ...per Referenz fangen...
    {
        OutputDebugString ((String (e.ClassName ()) + ": " + e.Message).c_str ());
        throw; // ...und bei Bedarf weiterwerfen.
    }
}

Dieses kleine Beispiel erzeugt eine hilfreiche Debug-Ausgabe, und der Handler in der VCL-Window-Prozedur zeigt diese Meldung:

You are one off!

Wer mit den Konventionen im Umgang mit Delphi-Klassen in C++ vertraut ist, fragt sich vielleicht, weshalb der Compiler uns erlaubt, eine temporäre Instanz von ERangeError auf dem Stack anzulegen. Immerhin ist ERangeError von TObject abgeleitet, und wenn man versucht, von derartigen Typen ein Stackobjekt anzulegen, erhält man üblicherweise folgenden Fehler:
[BCC32 Fehler] main_unit.cpp(16): E2459 Klassen im Delphi-Stil müssen mit dem Operator new erstellt werden


Diese Einschränkung existiert aus vielen guten Gründen, auf die ich jetzt nicht näher eingehen werde (vielleicht aber in einem zukünftigen Artikel).

Unser obiger Fall bildet keine Ausnahme, zumindest nicht aus technischer Sicht. Betrachtet man den Code, den der Compiler für unsere throw-Anweisung generiert, so stellt man fest, daß das Objekt gar nicht auf dem Stack, sondern wie üblich auf dem Heap konstruiert wird; der Code bewirkt dasselbe wie der folgende (lediglich die RTTI, die an _ThrowExceptionLDTC() übergeben wird, unterscheidet sich):
        throw new ERangeError ("You are one off!");

Dies ist ein Zugeständnis an das C++-Idiom, Exceptions per Wertübergabe zu werfen (würde man Zeiger auf Exception-Objekte werfen, so wäre nicht eindeutig, wie diese wieder zerstört und freigegeben werden müßten). Delphi-Exceptions werden hingegen per Definition als Zeiger geworfen und vom Exception-Handler mittels TObject::Free() freigegeben.


Man stelle sich auf dieser Grundlage vor, welcher Aufwand nötig wäre, damit Delphi C++-Exceptions behandeln könnte. Der Delphi-Compiler müßte über alle Typen, die als Exception geworfen werden könnten, in Kenntnis gesetzt werden - da es in dieser Hinsicht aber keine Einschränkungen gibt, wären das alle möglichen C++-Typen. Wir haben letztlich das gleiche Problem, das ich oben ansprach: es ist praktisch unmöglich, C++-Headerdateien in Delphi-Unit-Interfaces zu konvertieren.


C++-Exceptions in Delphi - Der Ansatz von Early Ehlinger


Wie der Untertitel des Artikels nahelegt, kann man diese Beschränkung umgehen. Zwar wird Delphi C++-Exceptions nach wie vor nicht behandeln können, aber es ist möglich, in einem Exception-Filter Wrapperklassen eines von Sysutils::Exception abgeleiteten Typs zu erzeugen. Diesen Ansatz wählte Early Ehlinger in seinem TranslateStandardExceptions-Unit.
Wenn wir dieses Unit zu unserem Projekt hinzufügen, erhalten wir beim Klick auf den Button diese Fehlermeldung:

invalid vector<> subscript

Das ist genau das, was wir wollten; es sieht so aus, als hätten wir die Lösung gefunden.
Dies ist jedoch ein vorschneller Schluß. Ich werde versuchen, die Gründe zu erläutern.

Zunächst sehen wir uns den Quelltext an, aus dem ich die relevanten Teile hier zusammengefaßt habe:
// Copyright (c) 2003 Early Ehlinger
// GNU Lesser General Public License, v2.1
// http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html

typedef Exception* (__fastcall* ExceptObjProcType)( Sysutils::TExceptionRecord* P );
ExceptObjProcType pOldExceptObjProc = NULL;

class not_standard_exception
  {
  public:
    virtual void foo() = 0;
  };

std::exception const* get_std_exception( not_standard_exception const* p_exception )
  {
  std::exception const* p_std_exception = 0;
  __try
    {
    p_std_exception = dynamic_cast< std::exception const* >( p_exception );
    }
  __except( EXCEPTION_EXECUTE_HANDLER )
    { }
  return p_std_exception;
  }


Exception* __fastcall GetCppExceptionObject(Sysutils::TExceptionRecord* P)
{
  // ...
      if ( std::exception const* p_std_exception = get_std_exception( p_exception ) )
  // ...
}

void install_exception_object_handler( )
  {
  pOldExceptObjProc = reinterpret_cast< ExceptObjProcType >( ExceptObjProc );
  System::ExceptObjProc = GetCppExceptionObject;
  }
#pragma startup install_exception_object_handler

void uninstall_exception_object_handler( )
  {
  if ( pOldExceptObjProc )
    System::ExceptObjProc = pOldExceptObjProc; // System.pas
  }
#pragma exit uninstall_exception_object_handler


Um vollständig nachvollziehen zu können, was dieser Code macht, lese man die ausführliche Erklärung auf des Autors Internetseite und im Quelltext. Kurz zusammengefaßt passiert dies:
  • Beim Start der Anwendung wird install_exception_object_handler() aufgerufen und installiert einen Exception-Filter in Delphis Exception-System.
  • Wird der Filter vom Exception-System in der Delphi-RTL aufgerufen und ist der Exception-Code 0x0EEFFACE, so wird get_std_exception aufgerufen, um den Objektzeiger nach std::exception zu casten.
  • get_std_exception behandelt den Zeiger als Zeiger auf einen polymorphen Typ und benutzt dynamic_cast<>, um ihn nach std::exception zu casten. Schlägt das fehl, wird 0 zurückgegeben.
  • Wenn get_std_exception erfolgreich war, wird eine Meldung wie oben zu sehen aus dem Typ der Exception und dem Ergebnis eines Aufrufs von std::exception::what() zusammengestellt, andernfalls wird die Meldung gezwungenermaßen etwas allgemeiner gehalten.

Besonderer Beachtung bedarf dieser Abschnitt:
  __try
    {
    p_std_exception = dynamic_cast< std::exception const* >( p_exception );
    }
  __except( EXCEPTION_EXECUTE_HANDLER )
    { }

Sollte sich nicht sofort erschließen, weshalb so etwas niemals in Produktivsystemen eingesetzt werden darf, so empfehle ich die Lektüre von Raymond Chens "IsBadXxxPtr should really be called CrashProgramRandomly" und Larry Ostermans "Should I check the parameters to my function?".

Und es gab noch weitere Probleme, die mich davon abhielten, mich mit dieser Lösung zufriedenzugeben:
  • Earlys Unit übersetzt die grundlegenden Standard-Exceptions, soweit sie vom Standard vorgegeben werden, in äquivalente Delphi-Wrapperklassen:
    C++ ExceptionVCL Exception
    std::logic_error CppStdLogicError
    std::domain_error CppStdDomainError
    std::invalid_argument CppStdInvalidArgument
    std::length_error CppStdLengthError
    std::out_of_range CppStdOutOfRange
    std::runtime_error CppStdRuntimeError
    std::range_error CppStdRangeError
    std::overflow_error CppStdOverflowError
    any other exception derived from std::exception CppStdException
    any c++ exception not derived from std::exception CppException

    Das ist ein pragmatischer Weg, um das einfache Dispatching dieser grundlegenden Typen zu ermöglichen, aber es führt zu viel Redundanz und ist andererseits nicht sehr einfach erweiterbar.
  • Das Exception-Objekt wird nicht freigegeben; es bleibt ein Speicherleck.
  • Auch hilft dieser Ansatz nichts, wenn es eine Delphi-Anwendung in die Lage zu versetzen gilt, C++-Exceptions behandeln zu können. Ein gutes Beispiel für eine solche Anwendung wäre C++Builder selbst. (Und es ist keine Lösung, das Unit zu allen C++Builder-Packages hinzuzufügen, da Packages zur Laufzeit wieder entladen werden können; für eine Beschreibung des sich dabei ergebenden Problems lese man das "Homework assignment about window subclassing" von Raymond Chen.)
  • Außerdem entwickeln die meisten C++Builder-Benutzer kommerzielle Anwendungen und können GPL- und LGPL-lizenzierten Quellcode nicht verwenden.


Dies waren genug Probleme, um mich auf die Suche nach einer anderen Lösung zu machen.


C++-Exceptions in Delphi - mein Ansatz


Aus dem Unit Early Ehlingers entnahm ich die Idee, den Exception-Filter der Delphi-RTL für die Generierung von Wrapper-Objekten zu benutzen. Außerdem wollte ich meinen Exception-Filter in Delphi schreiben, so daß er auch in Delphi-Projekten einsetzbar wäre.

Als ich also _ThrowExceptionLDTC() im Debugger verfolgte, stellte ich fest, daß der C++-Compiler dieser Funktion einen Zeiger auf die RTTI des Exception-Objekts übergibt. Dieser Zeiger wird im Exception-Descriptor gespeichert, der an RaiseException() übergeben wird. Auf die Parameter von RaiseException() können wir über das Argument des Exception-Filters zugreifen. Das ermöglicht uns, sowohl auf den Exception-Descriptor (wodurch wir das Objekt korrekt freigeben können) als auch auf die RTTI (zwecks Downcasting und Destruktion des Objektes) zuzugreifen. All das ist wohldokumentiert und für die Besitzer einer Professional Edition oder höher in $(BDS)\source\cpprtl\Source\except\xx.cpp und $(BDS)\source\cpprtl\Source\except\xxtype.cpp nachlesbar.

Und das ist auch schon alles, was mein Unit tut. Ich gab ihm den Namen 'SystemCppException', da es primär eine Erweiterung der Exception-Handling-Mechanismen im System-Unit ist. Um die übergebene Information wie gewünscht auswerten zu können, schrieb ich weiter ein paar Funktionen, die die Exception-Handling-Funktionalität der C++-RTL nachbilden. Mithilfe dieser Funktionen ist der Exception-Filter in der Lage, C++-Exceptions zu erkennen, ihren Typ festzustellen, sie anhand der verfügbaren RTTI zu jeder beliebigen Basisklasse zu casten und sie schließlich zu zerstören und den Speicher freizugeben.

Damit Delphi sie behandeln kann, erzeugt der Exception-Filter für C++-Exceptions Wrapperklassen vom Typ ECppException und ECppStdException. Die Properties und Methoden dieser Klassen sind größtenteils selbsterklärend, aber die wichtigsten will ich kurz aufzählen:
  • ECppException::TypeName resultiert in typeid (<C++-Exception-Objekt>).name().
  • ECppException::CppExceptionObject gibt einen Zeiger auf das C++-Exception-Objekt zurück.
  • Mit ECppException::IsCppClass kann festgestellt werden, ob es sich bei dem Exception-Objekt um eine Klasse handelt, und mit ECppException::AsCppClass() kann der Objektzeiger zu einer beliebigen Basisklasse (auch zu Interfaces, von denen der Objekttyp ableitet) gecastet werden. (Das Klassentemplate CppExceptionWrapper vereinfacht das Dispatching auf diesem Wege etwas.)

Wenn das Exception-Objekt von std::exception ableitet, haben wir eine gemeinsame Basisklasse. In diesem Fall erstellt der Exception-Filter ein Wrapperobjekt vom Typ ECppStdException, der seinerseits von ECppException ableitet und ein weiteres Property enthält:
  • ECppStdException::StdException gibt einen Zeiger vom Typ std::exception* zurück, der auf das C++-Exception-Objekt zeigt. Der Zeiger kann zwecks Dispatching wie gewohnt mit dynamic_cast<> in speziellere Exception-Klassen gecastet werden.

Dies ist der Interface-Abschnitt von SystemCppException:
unit SystemCppException;

interface

uses
  SysUtils;

type
  PCppStdException = type Pointer; { mapped to std::exception* via $HPPEMIT }
  {$EXTERNALSYM PCppStdException}

  { C++ exception of any type }
  ECppException = class (Exception)
    private
      FTypeName: AnsiString;
      FExcDesc:  Pointer;

      constructor CreateTypeNamed (_TypeName: PAnsiChar; ExcDesc: Pointer); overload;
      function GetCppExceptionObject: Pointer;
      function GetThrowLine: Integer;
      function GetThrowFile: AnsiString;

    public
      property CppExceptionObject: Pointer    read GetCppExceptionObject;
      property ThrowLine:          Integer    read GetThrowLine;
      property ThrowFile:          AnsiString read GetThrowFile;
      property TypeName:           AnsiString read FTypeName;

      function IsCppClass: Boolean;

      { ATTENTION: this function does perform downcasts only! }
      function AsCppClass (CppClassName: AnsiString): Pointer;

      destructor  Destroy; override;
  end;

  { C++ exception derived from std::exception }
  ECppStdException = class (ECppException)
    private
      FExcObj: PCppStdException;

      constructor Create (AExcObj: PCppStdException; Msg: String;
                          _TypeName: PAnsiChar; ExcDesc: Pointer); overload;
      function GetStdException: PCppStdException;

    public
      { This property returns a pointer to the wrapped exception. }
      property    StdException: PCppStdException read GetStdException;

      destructor  Destroy; override;
  end;

  (*$HPPEMIT '#include <typeinfo>'*)
  (*$HPPEMIT '#include <exception>'*)
  (*$HPPEMIT ''*)
  (*$HPPEMIT 'namespace Systemcppexception'*)
  (*$HPPEMIT '{'*)
  (*$HPPEMIT ''*)
  (*$HPPEMIT 'class DELPHICLASS ECppException;'*)
  (*$HPPEMIT ''*)
  (*$HPPEMIT 'template <class E>'*)
  (*$HPPEMIT '    class CppExceptionWrapper'*)
  (*$HPPEMIT '{'*)
  (*$HPPEMIT 'private:'*)
  (*$HPPEMIT '    E* internalClass;'*)
  (*$HPPEMIT '    struct _safebool_t;'*)
  (*$HPPEMIT ''*)
  (*$HPPEMIT 'public:'*)
  (*$HPPEMIT '    CppExceptionWrapper (ECppException* exception)'*)
  (*$HPPEMIT '     : internalClass (static_cast <E*> (exception->AsCppClass (typeid (E).name ())))'*)
  (*$HPPEMIT '    {}'*)
  (*$HPPEMIT '    operator _safebool_t* (void) const'*)
  (*$HPPEMIT '    { return (_safebool_t* ) internalClass; }'*)
  (*$HPPEMIT '    E& operator * (void)'*)
  (*$HPPEMIT '    { return *internalClass; }'*)
  (*$HPPEMIT '    E* operator -> (void)'*)
  (*$HPPEMIT '    { return internalClass; }'*)
  (*$HPPEMIT '};'*)
  (*$HPPEMIT ''*)
  (*$HPPEMIT 'typedef std::exception* PCppStdException;'*)
  (*$HPPEMIT ''*)
  (*$HPPEMIT '}	/* namespace Systemcppexception */'*)

Das Unit kann im Software-Bereich heruntergeladen werden. Um es einzusetzen, ist es ausreichend, es einfach dem Projekt, ob nun Delphi oder C++Builder, hinzuzufügen. (Allerdings sollte es nicht in Package-Projekten, die mit rtl*.bpl gelinkt sind, verwendet werden; die Gründe habe ich oben erläutert.)

Wie auch das Unit von Early Ehlinger installiert dieses Unit in der initialization-Sektion einen Exception-Filter und entfernt ihn bei der Finalisierung wieder. Fügen wir es zu obigem Beispielprojekt hinzu, so erhalten wir diese Meldung:

invalid vector<> subscript

Der Exception-Typ wird nur im Debug-Build der Exception-Meldung vorangestellt; im Release-Build erscheint er nicht in der Meldung.


Advanced Exception Dispatching


Die korrekte Darstellung von C++-Exception-Meldungen ist aber nicht alles; SystemCppException kann mehr. Benutzt man beispielsweise das TApplicationEvents::OnException-Event, so kann man einen globalen Exception-Dispatcher einrichten, der grundlegende Dinge wie das Senden von Bug-Reports, Einträge in Log-Dateien und die Darstellung übersichtlich formatierter, informativer Benutzerbenachrichtigungen übernimmt.

Bevor ich das demonstriere, möchte ich kurz auf die Existenz eines kleinen, aber nützlichen Features des C++Builder-Compilers aufmerksam machen: der "-xp"-Switch aka "Positionsinformation". Ist diese Option aktiviert, dann ist in jedem Exception-Descriptor auch ein Verweis auf die Datei und die Zeilennummer, wo die Exception geworfen wurde, enthalten. Die Wrapperklassen in SystemCppException.pas stellen diese Informationen über Read-only-Properties bereit.

Dieses Beispiel demonstriert nun, wie dynamisches Exception-Dispatching unter Ausnutzung des erwähnten Compiler-Switches aussehen könnte:
class silent_exception : public std::runtime_error
{
public:
    silent_exception (void)
     : std::runtime_error ("")
    {}
};

class user_notification_exception : public std::runtime_error
{
public:
    user_notification_exception (const char* arg)
     : std::runtime_error (arg)
    {}
};

bool displayExceptionDialog (HANDLE hParentWnd, Exception* E,
                             bool sendReport)
{
    ECppException* ce = dynamic_cast <ECppException*> (E);
    String typeName;
    String throwFile;
    int throwLine = 0;

    if (ce)
    {
        typeName = ce->TypeName;
        throwFile = ce->ThrowFile;
        throwLine = ce->ThrowLine;
    }
    else
        typeName = E->ClassName ();

    String Message = Format ("Unhandled exception of type %s:\n\n%s",
        ARRAYOFCONST ((typeName, E->Message)));
    DWORD questionstyle = 0;

    if (!throwFile.IsEmpty ())
    {
        Message += Format ("\n\nThrown in: %s\nLine: %d",
            ARRAYOFCONST ((throwFile, throwLine)));
    }
    if (sendReport)
    {
        questionstyle = MB_YESNO;
        Message += "\n\nSend report?";
    }
    return (MessageBox (hParentWnd, Message.c_str (), Application->Title.c_str (),
                        MB_ICONERROR | questionstyle) == IDYES);
}

void sendBugReport (Exception* E)
{
    // ...
}

void writeToLogFile (Exception* E)
{
    // ...
}


void __fastcall TFrmMain::ApplicationEventsException(TObject* Sender,
    Exception* E)
{
    ECppStdException* csew;
    ECppException* cew;

    if ((csew = dynamic_cast <ECppStdException*> (E)) != 0) // Standard-C++-Exception
    {
        std::exception* cse = csew->StdException; // Gibt das tatsächliche Exception-
                                                  // Objekt zurück. Ein Cast ist
                                                  // nicht notwendig.

            // Stille Exceptions werden "geschluckt".
            // (Es mag diskutabel sein, ob der Einsatz stiller Exceptions
            // gutes Design ist, aber gleichviel: sie werden benutzt, und
            // sie bieten eine gute Gelegenheit für die Demonstration meines
            // Anliegens.)
        if (dynamic_cast <silent_exception*> (cse))
        {
            ApplicationEvents->CancelDispatch ();
            return;
        }

            // Als User Notification Exceptions bezeichnen wir diejenigen
            // Exceptions, die keine Programmfehler sind, sondern durch Fehl-
            // bedienung des Benutzers verursacht werden. Man stelle sich vor,
            // der Benutzer benennt im Projekt-Manager der IDE eine Datei mit
            // F2 um. Diese Operation kann eine ganze Reihe von Folgeoperationen
            // nach sich ziehen, und wenn in einer dieser Folgeoperationen eine
            // Mehrdeutigkeit auftritt - der Name ist bereits vergeben -, so
            // wird eine User Notification Exception geworfen, die den Abbruch
            // aller vom Vorgang des Umbenennens induzierten Operationen zur
            // Folge hat und den Benutzer über die Ursache des Abbruchs
            // benachrichtigt.
            // Aus diesem Grund sollte für diese Exceptions weder ein Bug-Report
            // noch ein Eintrag in die Log-Datei erstellt werden.
        if (dynamic_cast <user_notification_exception*> (cse))
        {
            Application->ShowException (E);
            ApplicationEvents->CancelDispatch ();
            return;
        }
    }
    else if ((cew = dynamic_cast <ECppException*> (E)) != 0)
    { // C++-Exception eines beliebigen Typs
    }
    else
    { // Delphi- or OS-Exception
    }

    if (displayExceptionDialog (Handle, E, true))
        sendBugReport (E);
    writeToLogFile (E);
}

Das Beispiel mit std::vector<>, das ich zu Anfang zeigte, resultiert nun in der folgenden Fehlermeldung (im Release-Build):

invalid vector<> subscript - reloaded

In einem kommerziellen Projekt wäre es natürlich unangebracht, Quelldateiname, Zeilennummer und Exception-Typ in der Fehlermeldung für den Benutzer anzuzeigen. Statt dessen stelle man sich vor, diese Informationen einfach dem automatischen Bug-Report beizufügen ;)

(Für noch ausführlichere Bug-Reports mit Stack-Trace, Liste der geladenen Module, Systeminformation und dergleichen sollte die Verwendung eines ausgereiften Produkts von einem Drittanbieter in Betracht gezogen werden. Spontan fallen mir da JclDebug, madExcept und EurekaLog ein.)


Tritt nun eine User Notification Exception auf, so sieht das folgendermaßen aus:
void __fastcall TFrmMain::BtnUserNotificationExceptionClick(TObject* Sender)
{
    throw user_notification_exception ("Don't do that!");
}

Don't do that!


Und warum hat Borland das nicht von Anfang an so implementiert?


Disclaimer: bei diesem Abschnitt handelt es sich um bloße Spekulation.
Da die Generierung von Wrapperklassen für gute Integration sorgt und problemlos funktioniert, stellt sich die Frage, weshalb Borland es nicht gleich selbst so gemacht hat. Nun, Exception-Handling ist ein empfindliches Thema, und es gab sicherlich diverse gute Gründe dafür. Vermutlich hat es etwas mit den folgenden Punkten zu tun:
  • Der Ansatz, den ich wählte, erfordert den Nachbau einiger Funktionen aus der C++-RTL in Delphi. Code-Duplikation erschwert aber die Wartung des Quelltextes nicht unerheblich; jeder professionelle Programmierer vermeidet das, ganz besonders in derart empfindlichen Bereichen.
  • Vom Standpunkt des Sprach- oder Bibliotheksdesigners ist der Ansatz, den ich wählte, möglicherweise nicht optimal. Mein Unit mag seinen Zweck erfüllen und die "External Exception EEFFACE"-Meldungen aus der Welt schaffen, aber man sollte nicht vergessen, auf welchem Wege das erreicht wird: jede unbehandelte C++-Exception wird mittels eines Wrappers zu einer Delphi-Exception umgewandelt. Dadurch wird die strikte Trennung von Delphi- und C++-Exceptions untergraben; daß C++-Exceptions eigentlich keine Delphi-Exceptions sind und auch nicht sein sollen, wird übergangen.
    Allerdings bin ich nicht sicher, ob die Entwickler von Delphi und C++Builder das als Problem gesehen hätten. Tatsächlich wrappt Delphi, wie oben erwähnt, auch Betriebssystem-Exceptions (z.B. STATUS_INTEGER_DIVIDE_BY_ZERO oder STATUS_ACCESS_VIOLATION) in Delphi-Exceptions (EDivByZero, EAccessViolation). Zwar vertritt Raymond Chen die Auffassung "In the Win32 programming model, exceptions are truly exceptional. As a general rule, you shouldn't try to catch them", doch die Eigenheit des VCL-Frameworks, auch Betriebssystem-Exceptions zu fangen, ist eines der Dinge, die gute VCL-Programme tendentiell stabiler machen als Programme, die herkömmliche Frameworks benutzen. Das Delphi-Team war auf die Umsetzung dieses Features stolz, wie aus dem Launch-Skript für Delphi 1 unmißverständlich hervorgeht:
    I'm sure you've heard something like this before: "Yeah, this app is great, but boy when something goes wrong you get GP faults all over the place".

    Well not with Delphi.

    In fact, speaking of GP faults, let me show you what happens in Delphi.

    [...]

    As you can see, a GP fault occurred, and Delphi's default exception handler caught it and is showing an error dialog.
    But notice that the app is still running.

  • "Das Einbauen neuer Features ist wichtiger als das Beheben alter Probleme." Das wäre eine typische Borland-Antwort (man denke an C++BuilderX, Delphi 8, Delphi 2005), und glücklicherweise gilt das für CodeGear nicht mehr.



Addendum vom 28.02.2010:

Windows Error Reporting


Die Richtlinien für das Windows 7-Logo-Programm erfordern, daß unbehandelte Exceptions nicht vom Programm abgefangen werden, so daß WER (Windows Error Reporting) sie behandeln kann:
Microsoft:
Vendors must not hide unhandled exceptions from Windows error reporting (WER). ISVs must sign up to receive their crash data from WER.
You can do this by signing applications at Winqual. ISVs must map their applications carrying the Windows 7 logo to their company at this site and maintain these mappings while the product is supported.

Das Thema kam kürzlich in den Embarcadero-Newsgroups auf, und dort wurde auf die Variable System::JITEnable verwiesen, die das Verhalten eines Delphi- oder C++Builder-Programmes bei unbehandelten Exceptions konfigurierbar macht. Um die Auswirkungen verschiedener Kombinationen von JITEnable-Werten und SystemCppException zu illustrieren, habe ich ein kleines Beispielprogramm geschrieben, das eine einfache Möglichkeit bietet, die verschiedenen Szenarien auszuprobieren:

ExcTest

Zur Kompilierung muß die aktuellste Version von SystemCppException verfügbar sein.


Zusammenfassung


Dieses Dokument erläuterte die Ursachen der "External Exception EEFFACE"-Meldung und beschrieb Möglichkeiten, die Meldung loszuwerden.

Wie ich im letzten Absatz zu verdeutlichen suchte, ist auch mein Ansatz nicht vollständig unproblematisch: er erstellt Delphi-Wrapper für unbehandelte C++-Exceptions, was aus der Designer-Perspektive fragwürdig sein mag, und zudem müssen einige Funktionen in meinem Unit mit den Entsprechungen in der C++-RTL synchronisiert werden. Das dürfte mindestens beim Erscheinen von Delphi und C++Builder für 64-Bit-Windows wieder notwendig sein - auch wenn ich die Hoffnung habe, daß die VCL dann endlich C++-Exceptions behandeln kann.

Der Artikel sollte genug Anhaltspunkte geben für die Entscheidung, ob das Mehrwert groß genug ist, daß die Schwächen dieses Ansatzes tragbar sind. Sollten Sie sich entscheiden, mein Unit einzusetzen, so finden Sie es im Software-Bereich. (In diesem Fall freue ich mich außerdem über eine kurze Mail oder einen Kommentar.)


Referenzen


[1] Early Ehlinger, Translate C++ Exceptions to VCL Exceptions
[2] Raymond Chen, IsBadXxxPtr should really be called CrashProgramRandomly, 27.09.2006
[3] Larry Osterman, Should I check the parameters to my function?, 18.05.2004
[4] Raymond Chen, Homework assignment about window subclassing, 10.11.2003
[5] Anders Hejlsberg, Historical Documents: Delphi 1 launch demos source code, launch script, and marketing video, 14.02.1995
[6] Der Quelltext der Runtime-Libraries von Delphi und C++Builder (in C++Builder enthalten)


Kommentare


Name: Jarek
24.05.2010 09:11:14
Hello Moritz,

I've found your article very interesting and I'm going to include your unit SystemCppException in my project. But do you know if this unit co-operates well with EurekaLog?

Best wishes,
Jarek

Name: Moritz
30.07.2010 17:12:01
Hello Jarek,

sorry, I don't know; I don't use EurekaLog.
But I'd be happy to hear about your experience with combining EurekaLog and SystemCppException.

Regards,
Moritz

Name: Ahmed
26.10.2012 00:00:21
habe es gelöst das probelm und zwar ganz einfach den pc 2mal formatieren zu mindest war das bei mir so wenn noch fragen sind kontaktieren sie mich auf:

king_of_xbox@hotmail.com

Name: Peter
05.09.2013 18:12:27
A very interesting read.
Unfortunately this is not really my cup of tea and I think I have to read it a couple more times before I even understand most of it.
As a test I included the .pas file to my project and built it (using C++ Builder 2009).
In the code I added throw(\"message\") ;
I noticed an improvement from EEFFACE to \"Unhandled C++ Exception of type \'const char *\' occurred.
I was expecting to see \"message\"

What am I missing or doing wrong ?

Name: Neiß
01.10.2013 12:58:08
Hallo,

interessanter Artikel, wenn auch eine Anmerkungen erst nach ausgiebiger Lektüre der entspr. anderen Artikel verständlich werden ;-)

Einen Bug habe ich im Code gefunden..
Dieser besteht ja zu großen Teilen aus \"Nachbauten\" der entspr. Funktionen aus den C++-Moulden der RTL.

{ ... This function should basically work like locateBaseClass() in xxtype.cpp }
function _LocateCppBaseClass (BaseList: PCppBaseList; VBase: Boolean;
BaseName: PAnsiChar; var Addr: Pointer) : Boolean;

Der 2. Parameter gibt an, ob BaseList eine VBase ist oder nicht.

in CppGetBase steht aber:
Result := _LocateCppBaseClass (BaseList, False, BaseName, Obj)
or _LocateCppBaseClass (VBaseList, False, BaseName, Obj);

richtig müsste hier stehen (und damit dem original auch entsprechen):
Result := _LocateCppBaseClass (BaseList, False, BaseName, Obj)
or _LocateCppBaseClass (VBaseList, True, BaseName, Obj);


Auch wenn es sehr unwahrscheinlich ist, das eine Exception mit virtuellen Basis-Klassen geworfen wird.
Sollte es doch jemand tuen, gibt das wohl eine Access violation!

Name: Neiß
01.10.2013 14:43:02
Hallo,

interessanter Artikel, wenn auch eine Anmerkungen erst nach ausgiebiger Lektüre der entspr. anderen Artikel verständlich werden ;-)

Einen Bug habe ich im Code gefunden..
Dieser besteht ja zu großen Teilen aus \"Nachbauten\" der entspr. Funktionen aus den C++-Moulden der RTL.

{ ... This function should basically work like locateBaseClass() in xxtype.cpp }
function _LocateCppBaseClass (BaseList: PCppBaseList; VBase: Boolean;
BaseName: PAnsiChar; var Addr: Pointer) : Boolean;

Der 2. Parameter gibt an, ob BaseList eine VBase ist oder nicht.

in CppGetBase steht aber:
Result := _LocateCppBaseClass (BaseList, False, BaseName, Obj)
or _LocateCppBaseClass (VBaseList, False, BaseName, Obj);

richtig müsste hier stehen (und damit dem original auch entsprechen):
Result := _LocateCppBaseClass (BaseList, False, BaseName, Obj)
or _LocateCppBaseClass (VBaseList, True, BaseName, Obj);


Auch wenn es sehr unwahrscheinlich ist, das eine Exception mit virtuellen Basis-Klassen geworfen wird.
Sollte es doch jemand tuen, gibt das wohl eine Access violation!

Neuer Eintrag:

Name:
E-Mail:
Website:
Datum:
Anzahl der Zeichen in Ihrem Namen:
Text: