diff --git a/docs/changes.txt b/docs/changes.txt index 5dfa97e803..a28a707bf6 100644 --- a/docs/changes.txt +++ b/docs/changes.txt @@ -32,6 +32,7 @@ Changes in behaviour which may result in build errors All: +- Add wxApp::StoreCurrentException() and RethrowStoredException(). - Allow iterating over wxCmdLineParser arguments in order (Armel Asselin). - Add wxScopedArray ctor taking the number of elements to allocate. - Add wxDynamicLibrary::GetModuleFromAddress() (Luca Bacci). diff --git a/docs/doxygen/overviews/exceptions.h b/docs/doxygen/overviews/exceptions.h index 474796b7c6..2682020182 100644 --- a/docs/doxygen/overviews/exceptions.h +++ b/docs/doxygen/overviews/exceptions.h @@ -72,6 +72,40 @@ the user about the problem (while being careful not to throw any more exceptions as otherwise @c std::terminate() will be called). +@section overview_exceptions_store_rethrow Handling Exception Inside wxYield() + +In some, relatively rare cases, using wxApp::OnExceptionInMainLoop() may not +be sufficiently flexible. The most common example is using automated GUI tests, +when test failures are signaled by throwing an exception and these exceptions +can't be caught in a single central method because their handling depends on +the test logic, e.g. sometimes an exception is expected while at other times it +is an actual error. Typically this results in writing code like the following: + +@code +void TestNewDocument() +{ + wxUIActionSimulator ui; + ui.Char('n', wxMOD_CONTROL); // simulate creating a new file + + // Let wxWidgets dispatch Ctrl+N event, invoke the handler and create the + // new document. + try { + wxYield(); + } catch ( ... ) { + // Handle exceptions as failure in the new document creation test. + } +} +@endcode + +Unfortunately, by default this example does @e not work because an exception +can't be safely propagated back to the code handling it in @c TestNewDocument() +through the system event dispatch functions which are not compatible with C++ +exceptions. Because of this, you need to override wxApp::StoreCurrentException() +and wxApp::RethrowStoredException() to help wxWidgets to safely transport the +exception from the event handler that throws it to the @c catch clause. Please +see the documentation of these functions for more details. + + @section overview_exceptions_tech Technicalities To use any kind of exception support in the library you need to build it diff --git a/include/wx/app.h b/include/wx/app.h index b8ae61fc22..bfed781ac6 100644 --- a/include/wx/app.h +++ b/include/wx/app.h @@ -298,10 +298,24 @@ public: // Function called if an uncaught exception is caught inside the main // event loop: it may return true to continue running the event loop or - // false to stop it (in the latter case it may rethrow the exception as - // well) + // false to stop it. If this function rethrows the exception, as it does by + // default, simply because there is no general way to handle exceptions, + // StoreCurrentException() will be called to store it because in any case + // the exception can't be allowed to escape. virtual bool OnExceptionInMainLoop(); + // This function can be overridden to store the current exception, in view + // of rethrowing it later when RethrowStoredException() is called. If the + // exception was stored, return true. The default implementation returns + // false, indicating that the exception wasn't stored and that the program + // should be simply aborted. + virtual bool StoreCurrentException(); + + // If StoreCurrentException() is overridden, this function should be + // overridden as well to rethrow the exceptions stored by it when the + // control gets back to our code, i.e. when it's safe to do it. The default + // version does nothing. + virtual void RethrowStoredException() { } #endif // wxUSE_EXCEPTIONS diff --git a/interface/wx/app.h b/interface/wx/app.h index 16e9d71008..b247efa38e 100644 --- a/interface/wx/app.h +++ b/interface/wx/app.h @@ -420,16 +420,47 @@ public: This function is called if an unhandled exception occurs inside the main application event loop. It can return @true to ignore the exception and to continue running the loop or @false to exit the loop and terminate the - program. In the latter case it can also use C++ @c throw keyword to - rethrow the current exception. + program. The default behaviour of this function is the latter in all ports except under Windows where a dialog is shown to the user which allows him to choose between the different options. You may override this function in your class to do something more appropriate. - Finally note that if the exception is rethrown from here, it can be caught in - OnUnhandledException(). + If this method rethrows the exception and if the exception can't be + stored for later processing using StoreCurrentException(), the program + will terminate after calling OnUnhandledException(). + + You should consider overriding this method to perform whichever last + resort exception handling that would be done in a typical C++ program + in a @c try/catch block around the entire @c main() function. As this + method is called during exception handling, you may use the C++ @c + throw keyword to rethrow the current exception to catch it again and + analyze it. For example: + + @code + class MyApp : public wxApp { + public: + virtual bool OnExceptionInMainLoop() + { + wxString error; + try { + throw; // Rethrow the current exception. + } catch (const MyException& e) { + error = e.GetMyErrorMessage(); + } catch (const std::exception& e) { + error = e.what(); + } catch ( ... ) { + error = "unknown error."; + } + + wxLogError("Unexpected exception has occurred: %s, the program will terminate.", error); + + // Exit the main loop and thus terminate the program. + return false; + } + }; + @endcode */ virtual bool OnExceptionInMainLoop(); @@ -452,6 +483,105 @@ public: */ virtual void OnUnhandledException(); + /** + Method to store exceptions not handled by OnExceptionInMainLoop(). + + This function can be overridden to store the current exception, in view + of rethrowing it later when RethrowStoredException() is called. If the + exception was stored, return true. If the exception can't be stored, + i.e. if this function returns false, the program will abort after + calling OnUnhandledException(). + + It is necessary to override this function if OnExceptionInMainLoop() + doesn't catch all exceptions, but you still want to handle them using + explicit @c try/catch statements. Typical use could be to allow code + like the following to work: + + @code + void MyFrame::SomeFunction() + { + try { + MyDialog dlg(this); + dlg.ShowModal(); + } catch ( const MyExpectedException& e ) { + // Deal with the exceptions thrown from the dialog. + } + } + @endcode + + By default, throwing an exception from an event handler called from the + dialog modal event loop would terminate the application as the + exception can't be safely propagated to the code in the catch clause + because of the presence of the native system functions (through which + C++ exceptions can't, generally speaking, propagate) in the call stack + between them. + + Overriding this method allows the exception to be stored when it is + detected and rethrown using RethrowStoredException() when the native + system function dispatching the dialog events terminates, with the + result that the code above works as expected. + + An example of implementing this method: + @code + class MyApp : public wxApp { + public: + virtual bool StoreCurrentException() + { + try { + throw; + } catch ( const std::runtime_exception& e ) { + if ( !m_runtimeError.empty() ) { + // This is not supposed to happen, only one exception, + // at most, should be stored. + return false; + } + + m_runtimeError = e.what(); + + // Don't terminate, let our code handle this exception later. + return true; + } catch ( ... ) { + // This could be extended to store information about any + // other exceptions too, but if we don't store them, we + // should return false to let the program die. + } + + return false; + } + + virtual void RethrowStoredException() + { + if ( !m_runtimeError.empty() ) { + std::runtime_exception e(m_runtimeError); + m_runtimeError.clear(); + throw e; + } + } + + private: + std::string m_runtimeError; + }; + @endcode + + @see OnExceptionInMainLoop(), RethrowStoredException() + + @since 3.1.0 + */ + virtual bool StoreCurrentException(); + + /** + Method to rethrow exceptions stored by StoreCurrentException(). + + If StoreCurrentException() is overridden, this function should be + overridden as well to rethrow the exceptions stored by it when the + control gets back to our code, i.e. when it's safe to do it. + + See StoreCurrentException() for an example of implementing this method. + + @since 3.1.0 + */ + virtual void RethrowStoredException(); + //@} diff --git a/samples/except/except.cpp b/samples/except/except.cpp index 9e9ed364d9..0fb1f55d5f 100644 --- a/samples/except/except.cpp +++ b/samples/except/except.cpp @@ -76,6 +76,11 @@ static void DoCrash() class MyApp : public wxApp { public: + MyApp() + { + m_numStoredExceptions = 0; + } + // override base class virtuals // ---------------------------- @@ -86,6 +91,12 @@ public: // event handler here virtual bool OnExceptionInMainLoop() wxOVERRIDE; + // 2nd-level exception handling helpers: if we can't deal with the + // exception immediately, we may also store it and rethrow it later, when + // we're back from events processing loop. + virtual bool StoreCurrentException() wxOVERRIDE; + virtual void RethrowStoredException() wxOVERRIDE; + // 3rd, and final, level exception handling: whenever an unhandled // exception is caught, this function is called virtual void OnUnhandledException() wxOVERRIDE; @@ -101,6 +112,11 @@ public: const wxChar *func, const wxChar *cond, const wxChar *msg) wxOVERRIDE; + +private: + // This stores the number of times StoreCurrentException() was called, + // typically at most 1. + int m_numStoredExceptions; }; // Define a new frame type: this is going to be our main frame @@ -155,6 +171,7 @@ public: // event handlers void OnThrowInt(wxCommandEvent& event); void OnThrowObject(wxCommandEvent& event); + void OnThrowUnhandled(wxCommandEvent& event); void OnCrash(wxCommandEvent& event); private: @@ -174,6 +191,9 @@ private: }; // Another exception class which just has to be different from anything else +// +// It is not handled by OnExceptionInMainLoop() but is still handled by +// explicit try/catch blocks so it's not quite completely unhandled, actually. class UnhandledException { }; @@ -236,6 +256,7 @@ wxEND_EVENT_TABLE() wxBEGIN_EVENT_TABLE(MyDialog, wxDialog) EVT_BUTTON(Except_ThrowInt, MyDialog::OnThrowInt) EVT_BUTTON(Except_ThrowObject, MyDialog::OnThrowObject) + EVT_BUTTON(Except_ThrowUnhandled, MyDialog::OnThrowUnhandled) EVT_BUTTON(Except_Crash, MyDialog::OnCrash) wxEND_EVENT_TABLE() @@ -291,6 +312,41 @@ bool MyApp::OnExceptionInMainLoop() return true; } +bool MyApp::StoreCurrentException() +{ + try + { + throw; + } + catch ( UnhandledException& ) + { + if ( m_numStoredExceptions ) + { + wxLogWarning("Unexpectedly many exceptions to store."); + } + + m_numStoredExceptions++; + + return true; + } + catch ( ... ) + { + // Don't know how to store other exceptions. + } + + return false; +} + +void MyApp::RethrowStoredException() +{ + if ( m_numStoredExceptions ) + { + m_numStoredExceptions = 0; + + throw UnhandledException(); + } +} + void MyApp::OnUnhandledException() { // this shows how we may let some exception propagate uncaught @@ -424,6 +480,10 @@ void MyFrame::OnDialog(wxCommandEvent& WXUNUSED(event)) dlg.ShowModal(); } + catch ( UnhandledException& ) + { + wxLogMessage("Caught unhandled exception inside the dialog."); + } catch ( ... ) { wxLogWarning(wxT("An exception in MyDialog")); @@ -457,8 +517,12 @@ void MyFrame::OnThrowFromYield(wxCommandEvent& WXUNUSED(event)) { #if wxUSE_UIACTIONSIMULATOR // Simulate selecting the "Throw unhandled" menu item, its handler will be - // executed from inside wxYield(), so we may not be able to catch the - // exception here under Win64 even in spite of an explicit catch. + // executed from inside wxYield() and as the exception is not handled by + // our OnExceptionInMainLoop(), will call StoreCurrentException() and, when + // wxYield() regains control, RethrowStoredException(). + // + // Notice that if we didn't override these methods we wouldn't be able to + // catch this exception here! try { wxUIActionSimulator sim; @@ -550,13 +614,15 @@ MyDialog::MyDialog(wxFrame *parent) wxSizer *sizerTop = new wxBoxSizer(wxVERTICAL); sizerTop->Add(new wxButton(this, Except_ThrowInt, wxT("Throw &int")), - 0, wxCENTRE | wxALL, 5); + 0, wxEXPAND | wxALL, 5); sizerTop->Add(new wxButton(this, Except_ThrowObject, wxT("Throw &object")), - 0, wxCENTRE | wxALL, 5); + 0, wxEXPAND | wxALL, 5); + sizerTop->Add(new wxButton(this, Except_ThrowUnhandled, wxT("Throw &unhandled")), + 0, wxEXPAND | wxALL, 5); sizerTop->Add(new wxButton(this, Except_Crash, wxT("&Crash")), - 0, wxCENTRE | wxALL, 5); + 0, wxEXPAND | wxALL, 5); sizerTop->Add(new wxButton(this, wxID_CANCEL, wxT("&Cancel")), - 0, wxCENTRE | wxALL, 5); + 0, wxEXPAND | wxALL, 5); SetSizerAndFit(sizerTop); } @@ -571,6 +637,11 @@ void MyDialog::OnThrowObject(wxCommandEvent& WXUNUSED(event)) throw MyException(wxT("Exception thrown from MyDialog")); } +void MyDialog::OnThrowUnhandled(wxCommandEvent& WXUNUSED(event)) +{ + throw UnhandledException(); +} + void MyDialog::OnCrash(wxCommandEvent& WXUNUSED(event)) { DoCrash(); diff --git a/src/common/appbase.cpp b/src/common/appbase.cpp index 7298740648..e3a0c06988 100644 --- a/src/common/appbase.cpp +++ b/src/common/appbase.cpp @@ -662,6 +662,11 @@ bool wxAppConsoleBase::OnExceptionInMainLoop() throw; } +bool wxAppConsoleBase::StoreCurrentException() +{ + return false; +} + #endif // wxUSE_EXCEPTIONS // ---------------------------------------------------------------------------- diff --git a/src/common/event.cpp b/src/common/event.cpp index ddd90d8f6d..a1c36c1e80 100644 --- a/src/common/event.cpp +++ b/src/common/event.cpp @@ -1624,24 +1624,59 @@ bool wxEvtHandler::SafelyProcessEvent(wxEvent& event) loop->Exit(); } //else: continue running current event loop - - return false; } catch ( ... ) { // OnExceptionInMainLoop() threw, possibly rethrowing the same - // exception again: very good, but we still need Exit() to - // be called, unless we're not called from the loop directly but - // from Yield(), in which case we shouldn't exit the loop but just - // unwind to the point where Yield() is called where the exception - // might be handled -- and if not, then it will unwind further and - // exit the loop when it is caught. + // exception again. We have to deal with it here because we can't + // allow the exception to escape from the handling code, this will + // result in a crash at best (e.g. when using wxGTK as C++ + // exceptions can't propagate through the C GTK+ code and corrupt + // the stack) and in something even more weird at worst (like + // exceptions completely disappearing into the void under some + // 64 bit versions of Windows). if ( loop && !loop->IsYielding() ) loop->Exit(); - throw; + + // Give the application one last possibility to store the exception + // for rethrowing it later, when we get back to our code. + bool stored = false; + try + { + if ( wxTheApp ) + stored = wxTheApp->StoreCurrentException(); + } + catch ( ... ) + { + // StoreCurrentException() really shouldn't throw, but if it + // did, take it as an indication that it didn't store it. + } + + // If it didn't take it, just abort, at least like this we behave + // consistently everywhere. + if ( !stored ) + { + try + { + if ( wxTheApp ) + wxTheApp->OnUnhandledException(); + } + catch ( ... ) + { + // And OnUnhandledException() absolutely shouldn't throw, + // but we still must account for the possibility that it + // did. At least show some information about the exception + // in this case. + wxTheApp->wxAppConsoleBase::OnUnhandledException(); + } + + wxAbort(); + } } } #endif // wxUSE_EXCEPTIONS + + return false; } bool wxEvtHandler::SearchEventTable(wxEventTable& table, wxEvent& event) diff --git a/src/common/evtloopcmn.cpp b/src/common/evtloopcmn.cpp index 827989f3e5..e22fccdd64 100644 --- a/src/common/evtloopcmn.cpp +++ b/src/common/evtloopcmn.cpp @@ -140,6 +140,14 @@ bool wxEventLoopBase::YieldFor(long eventsToProcess) DoYieldFor(eventsToProcess); + // If any handlers called from inside DoYieldFor() threw exceptions, they + // may have been stored for later rethrow as it's unsafe to let them escape + // from inside DoYieldFor() itself, as it calls native functions through + // which the exceptions can't propagate. But now that we're back to our own + // code, we may rethrow them. + if ( wxTheApp ) + wxTheApp->RethrowStoredException(); + return true; } @@ -207,7 +215,15 @@ bool wxEventLoopManual::ProcessEvents() if ( m_shouldExit ) return false; } - return Dispatch(); + + const bool res = Dispatch(); + + // Rethrow any exceptions which could have been produced by the handlers + // ran by Dispatch(). + if ( wxTheApp ) + wxTheApp->RethrowStoredException(); + + return res; } int wxEventLoopManual::DoRun() diff --git a/src/gtk/evtloop.cpp b/src/gtk/evtloop.cpp index 660aab0119..10731b30f9 100644 --- a/src/gtk/evtloop.cpp +++ b/src/gtk/evtloop.cpp @@ -78,6 +78,11 @@ int wxGUIEventLoop::DoRun() OnExit(); + // Rethrow any exceptions which could have been produced by the handlers + // ran by the event loop. + if ( wxTheApp ) + wxTheApp->RethrowStoredException(); + return m_exitcode; }