|
|||||||||||||||||||||||||||||
|
External Exception EEFFACEWarum Delphi C++-Exceptions nicht behandeln kann - und wie sich das ändern läßt.
EinführungSo 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: Wird der Button angeklickt, so erscheint diese Meldung: ![]() 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: Nun springen wir in _Xran() hinein und treffen auf den Verursacher: 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: 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: 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++BuilderIn 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: Dieses kleine Beispiel erzeugt eine hilfreiche Debug-Ausgabe, und der Handler in der VCL-Window-Prozedur zeigt diese Meldung: ![]() 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): 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 EhlingerWie 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: ![]() 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: 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:
Besonderer Beachtung bedarf dieser Abschnitt: 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:
Dies waren genug Probleme, um mich auf die Suche nach einer anderen Lösung zu machen. C++-Exceptions in Delphi - mein AnsatzAus 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:
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:
Dies ist der Interface-Abschnitt von 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: ![]() Der Exception-Typ wird nur im Debug-Build der Exception-Meldung vorangestellt; im Release-Build erscheint er nicht in der Meldung. Advanced Exception DispatchingDie 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: Das Beispiel mit std::vector<>, das ich zu Anfang zeigte, resultiert nun in der folgenden Fehlermeldung (im Release-Build): ![]() 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: ![]() 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:
Addendum vom 28.02.2010: Windows Error ReportingDie 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: ![]() Zur Kompilierung muß die aktuellste Version von SystemCppException verfügbar sein. ZusammenfassungDieses 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
|
||||||||||||||||||||||||||||