Menu

Exceptions_and_OWLNext

Exceptions and OWLNext

Although OWL uses C++ exceptions, OWL and early parts of OWLNext were written in an era when C++ exception handling was poorly understood. Consequently, much of the code is written without concern for exceptions, leaving the potential for memory leaks and undefined behaviour.

Since then the concept of exception safety has been established, and we now have firm coding guidelines for writing exception-safe code (see Exception Safety at Wikipedia). Improving the OWLNext code in this regard is an ongoing effort. In client code, you should assume that any OWLNext function may throw exceptions, unless it is declared noexcept or throw().

This article looks closer at some areas of OWLNext where exception handling is of particular importance and provides some guidelines on how to write exception-safe OWLNext applications.



OWLNext now uses the std::exception base class

Originally, OWL used the old exception class xmsg as the base class for TXBase, which in turn is the base class for TXOwl and the derived exception classes, TXWindow etc. OWLNext now uses std::exception as the base class for TXBase.

The diagnostics macros, CHECK and PRECONDITION, originally threw exceptions check and precondition derived from xerror, which in turn was derived from xmsg. OWLNext replaces these exception classes by TCheckFailure and TPreconditionFailure derived from TDiagException, which in turn is derived from std::exception.


Exception transport across the Windows API boundary

The Windows API (formerly known as Win32) is provided in the C language. It is not C++. This means that the API, on a standard language level, does not support C++ exceptions, and OWLNext client code should be careful not to let C++ exceptions escape across the API boundary. In particular, this is an issue in any Windows API callback, such as message handlers.

For message handlers as defined in OWLNext, i.e. event handlers specified in OWL response tables, OWLNext provides an exception firewall in the private function TWindow::ReceiveMessage. So if an exception escapes from an event handler, e.g. TWindow::EvEndSession, it will be caught and suspended in ReceiveMessage, then rethrown in TApplication::MessageLoop. In effect, the exception is transported across the API boundary.

Originally, OWL used a similar solution. However, in version 5, the explicit exception transport was removed, and exception transport was made to rely on the low-level interaction between Windows Structured Exception Handling (SEH) and the implementation of C++ exceptions in the 32-bit Borland C++ compiler. However, this piggy-backing on SEH proved to be unreliable, and explicit exception transport was reintroduced in OWLNext version 6.34 (see [bugs:#230]).

The reintroduction of explicit exception transport came with a few changes to how exceptions are handled in the message loop. Previously, escaped exceptions were handled by displaying a message box to the user, with an option to ignore the exception and resume the application (which most often was not successful, since the application probably already was in a corrupt state). Now, this option is no longer available. Any exception that has been allowed to escape across the API boundary will cause the application to shut down. Exceptions are no longer handled in the message loop. They are simply rethrown, ultimately to be handled by the OWLNext startup code, which simply displays an error message and quits (see "main.cpp").

Exception transport is only provided for event handlers. For any other Windows API callback, you have to ensure that C++ exceptions do not escape your code, or risk undefined behaviour.


Exceptions in event handlers

As described above, exception transport is provided for event handlers defined in OWLNext response tables, as well as virtual functions on the call chain between ReceiveMessage and the handler (see TWindow::WindowProc). However, you must still handle exceptions in your event handlers, unless it is OK for the application to shut down when an exception occurs (such as for diagnostics exceptions).

Dealing with rethrown exceptions and WM_QUIT

If you do try to handle exceptions in your event handlers, you need to pay attention to exceptions rethrown by the exception transport machinery. To ensure that unhandled exceptions reach the main message loop, OWLNext calls PostQuitMessage when an unhandled exception is caught and suspended (this change was introduced with the reintroduction of exception transport in version 6.34, see bugs:#230). The call to PostQuitMessage ensures that all message loops are terminated, that the exception is rethrown and that it is ultimately handled and reported by the OWLNext startup code. However, this may lead to surprises and program bugs if you try to handle a rethrown exception in your code. In particular, since WM_QUIT is pending in this case, any call to MessageBox will not display, and the application will terminate, despite your exception handling, unless you remove WM_QUIT from the message queue.

For example, consider this simplified example from an actual OWLNext application that ran into this issue:

void TMyApp::CmSelectWindowLayout()
{
  try
  {
    TWindowLayout layout;
    TWindowLayoutSelectionDlg dlg(GetMainWindow(), layout);
    if (dlg.Execute() == IDOK)
      RestoreWindowLayout(layout);
  }
  catch (const std::runtime_exception&)
  {
    GetMainWindow()->MessageBox("Something went wrong. Let's ignore it and hope for the best.");
  }
}

Now, if there is an unhandled exception in any of the event handlers in TWindowLayoutSelectionDlg, it will be suspended and PostQuitMessage will be called. This breaks the dialog message loop, and the exception is rethrown (by a call to ResumeThrow in in TDialog::Execute). However, since PostQuitMessage has been called, WM_QUIT is now pending in the message queue, so the MessageBox in our catch block does not show up. Our function ends, WM_QUIT is handled by the main message loop, and the application shuts down without any warning. Oops.

The recommended way to deal with this issue is simply not to attempt to handle exceptions eminating from dialog box execution, or any other OWLNext call that invokes message loops and callbacks (event handlers). Just let those exceptions through, so that they are reported and the application exits gracefully. Only handle exceptions in your own code, or in OWLNext code you can prepare for. For example:

void TMyApp::CmSelectWindowLayout()
{
  TWindowLayout layout;
  TWindowLayoutSelectionDlg dlg(GetMainWindow(), layout);
  if (dlg.Execute() == IDOK)
    try
    {
      RestoreWindowLayout(layout);
    }
    catch (const std::runtime_exception&)
    {
      GetMainWindow()->MessageBox("Unable to restore this window layout. Try another.");
    }
}

However, if you really need to handle any exception in cases like this, you need to remove WM_QUIT from the message queue. A brute-force way to do that is to call TMsgThread::FlushQueue. In our example, we also need to ensure that the main window is re-enabled, since a failure in the modal dialog box may have left it disabled.

void TMyApp::CmSelectWindowLayout()
{
  try
  {
    TWindowLayout layout;
    TWindowLayoutSelectionDlg dlg(GetMainWindow(), layout);
    if (dlg.Execute() == IDOK)
      RestoreWindowLayout(layout);
  }
  catch (const std::runtime_exception&)
  {
    FlushQueue(); // Remove any pending WM_QUIT message.
    GetMainWindow()->EnableWindow(true);
    GetMainWindow()->MessageBox("Something went wrong. Let's ignore it and hope for the best.");
  }
}

Note that calling FlushQueue may cause your application to miss important messages. A more robust solution is to manually remove only WM_QUIT:

  catch (const std::runtime_exception&)
  {
    const auto cancelQuit = []
    {
      auto m = MSG{};
      return ::PeekMessage(&m, nullptr, WM_QUIT, WM_QUIT, PM_REMOVE) == TRUE;
    };
    [[maybe_unused]] const auto didCancelQuit = cancelQuit();
    WARN(didCancelQuit, "Cancelled WM_QUIT message caused by unhandled exception.");    
    GetMainWindow()->EnableWindow(true);
    GetMainWindow()->MessageBox("Something went wrong. Let's ignore it and hope for the best.");
  }

But now things are starting to get complicated. Perhaps it is better to just let unhandled exceptions through. If you really need handling like this, then consider encapsulating it in an error handling function.


Exceptions in overriding virtual functions

Unfortunately, the OWLNext source code is generally not exception safe. In particular, the code often assumes that virtual functions do not throw exceptions.

For example, see the implementation of TDocManager::CreateDoc. It calls InitDoc, which in turn calls virtual functions TDocument::SetDocPath, InitDoc and Open without any exception handling. InitDoc also calls CreateView, which in turn calls virtual function TDocTemplate::ConstructView. If any of these functions throws an exception, then CreateDoc will leak the TDocument it failed to create [bugs:#344].

Hence, the advice is to never let an exception escape an overriding virtual function, unless you are confident it will be handled properly by OWLNext. Many of the virtual functions are designed to return an error code on failure (such as nullptr or false). So if possible, translate all exceptions, except diagnostics exceptions, to the appropriate error code in your overriding function. If you do let exceptions escape, inspect the OWLNext source code to make sure it handles it in a satisfactory manner, and test well (with actual exceptions!).


Exceptions in constructors

As for virtual functions, the advice is to never let an exception escape the constructor of a class derived from OWL, unless you are confident it will be handled properly by OWLNext.

For example, an exception in a constructor of a class derived from TView may lead to a dangling or double-deleted TDocument depending on the particular situation, the setting of the flag dtAutoDelete, and the version of OWLNext. See [bugs:#543].


Do not swallow diagnostics exceptions

In general, you should be careful not to swallow diagnostics exceptions in your code. The whole point of the diagnostics exceptions, e.g. generated by CHECK and PRECONDITION, is to alert you to a problem in the code.

Normally, you don't have to do anything. Diagnostics exceptions will just automatically pass through your code. However, sometimes you may need to catch std::exception, on which TDiagException is based, or you may need to catch everything. In such case, to let diagnostics exceptions pass through your exception handling code, you can filter them out by catching TDiagException and simply rethrowing it. Put the catch block for TDiagException before any other block. For example:

try
{
  //...some code...
}
catch (const owl::TDiagException&)
{
  throw; // Let diagnostics through.
}
catch (...)
{
  //...handle any other exceptions...
}

If your catch block is located inside a Windows API callback, from which you cannot let any exception escape, you can call TApplication::SuspendThrow rather than rethrowing the TDiagException. If your code instigated the callback, then insert a call to TApplication::ResumeThrow after the instigating call. If using SuspendThrow and ResumeThrow is not feasible, then report or log the error and immediately shut down the application.


Expect an exception — write exception-safe code

Unlike Windows API C code, OWLNext is C++ code, and you should expect that any function and constructor may throw an exception, unless it is explicitly stated in the API that it does not (i.e. the function signature has noexcept applied, or the now deprecated throw() specification). Unfortunately, as OWLNext is a thin wrapper for the Windows API, many programmers copy-and-paste code from the Windows documentation into their programs — code that is not written for exception safety. In particular, this becomes a big problem with functions that allocate resources, such as memory, GDI objects and file handles. Since an exception may occur, resources may leak, unless you carefully write your program to clean up, either by explicit exception handlers, or better yet, by using C++ code constructs, such as smart pointers, that exploit RAII to deallocate resources automatically.

As an example, let us consider copying the contents of a List Box onto the clipboard. If you rely on the Windows API clipboard example code, you may end up with something looking as follows:

  auto os = tostringstream{};
  const auto n = ListBox.GetCount();
  for (auto i = 0; i != n; ++i)
    os << ListBox.GetString(i) << _T('\n');
  const auto s = os.str();

  const auto bufSize = (s.size() + 1) * sizeof(tchar);
  auto h = GlobalAlloc(GMEM_MOVEABLE, bufSize);
  if (!h) throw std::runtime_error{"GlobalAlloc failed"};

  const auto buf = GlobalLock(h); CHECK(buf);
  memcpy_s(buf, bufSize, s.c_str(), bufSize);
  GlobalUnlock(h);

  const auto f = sizeof(tchar) > 1 ? CF_UNICODETEXT : CF_TEXT;
  TClipboard c{GetHandle(), true};
  c.EmptyClipboard();
  c.SetClipboardData(f, h); // SetClipboardData takes ownership of the handle.
  c.CloseClipboard();

Unfortunately, this code will leak the global memory allocated by GlobalAlloc, if any following function call throws an exception before the SetCliboardData call takes ownership of the handle. It is also brittle with regard to the lock it acquires by calling GlobalLock. Although there is no intervening code that can throw exceptions between the acquisition and return of the lock (memcpy_s does not throw exceptions), this may change over time as the code is maintained. For example, a reckless programmer (probably yourself) may later add a C++ function call, say SaveToLog, that may throw exceptions.

  const auto buf = GlobalLock(h); CHECK(buf);
  memcpy_s(buf, bufSize, s.c_str(), bufSize);
  SaveToLog(_T("Copied to clipboard: "), static_cast<LPCTSTR>(buf)); // *** BIG PROBLEM! MAY THROW! ***
  GlobalUnlock(h);

Fortunately, modern C++ has tools to help us write exception-safe code with little effort. In particular, the smart pointer std::unique_ptr is immensly helpful, since it can be configured to take a custom deleter. By exploiting this, we can rewrite the code to be exception-safe.

  const auto bufSize = (s.size() + 1) * sizeof(tchar);
  using THandle = std::unique_ptr<void, decltype(&GlobalFree)>;
  auto h = THandle{GlobalAlloc(GMEM_MOVEABLE, bufSize), &GlobalFree};
  if (!h) throw std::runtime_error{"GlobalAlloc failed"};

  const auto buf = GlobalLock(h.get()); CHECK(buf);
  using TLock = std::unique_ptr<void, decltype(&GlobalUnlock)>;
  auto lock = TLock{h.get(), &GlobalUnlock};
  memcpy_s(buf, bufSize, s.c_str(), bufSize);
  lock.reset(); // Calls GlobalUnlock.

  const auto f = sizeof(tchar) > 1 ? CF_UNICODETEXT : CF_TEXT;
  TClipboard c{GetHandle(), true};
  c.EmptyClipboard();
  c.SetClipboardData(f, h.get()); 
  h.release(); // SetClipboardData took ownership of the handle.
  c.CloseClipboard();

Now, if there is an exception in this code, the unique_ptr destructor will call the specified deleter to clean up the allocated resource it refers to. And all is well, even if you later add that SaveToLog call.



Further reading



Related

Bugs: #230
Bugs: #344
Bugs: #543
Bugs: #571
Discussion: Running out of (menu) handles
Discussion: Using smart pointers in OWLNext 7
Discussion: TListBox copy / paste feature
Discussion: Help on incompatible changes in 7 series
Discussion: Exceptions and OWLNext
News: 2018/03/expect-an-exception--write-exception-safe-code
Wiki: Coding_Standards
Wiki: Frequently_Asked_Questions
Wiki: Knowledge_Base
Wiki: Upgrading_from_OWL

Want the latest updates on software, tech news, and AI?
Get latest updates about software, tech news, and AI from SourceForge directly in your inbox once a month.