Pig! — Building a SkyOS Application in C++


Chapter 5
A Pig with STyLe!
 

Last chapter's promise of eye candy has turned out to be something of a fib, but the big news for this release is that Pig! is now based on the C++ Standard Library.  To be a bit more accurate, portions of the Humble Framework now use C++ Standard Library template classes to accomplish as much of the internal data management as practical.  For those of you who are not familiar with the C++ Standard Library—or it's precursor, the Standard Template Library (STL)—remain calm: the public interface of the Humble Framework is essentially unchanged from the previous version.  The STL template classes may be some of the most brilliant C++ code over made public, but they are also wickedly complex and will torture test both the programmer and the compiler.  Because of this, the Humble Framework tries to hide as much of the voodoo code as possible in the implementation, exposing only friendly, easy-to-read class interfaces to the developer.  (If you are interested in delving deeper into the C++ Standard Library, I would recommend The C++ Standard Library: A Tutorial and Reference by Nicolai M. Josuttis.) 

As usual, all of the source code and a precompiled binary for this chapter can be downloaded here: PigChapter05.zip.  If you would like to report a bug—and I'm sure there's more than one in there for you to find—or would like to make a request, suggestion, or comment, you can reach me or through the SkyOS forums.  I would also like to humbly thank both Robert and the other members of the SkyOS Development Team for their time and patience in helping me decipher cryptic error messages and track down fatal configuration problems.

Development Tools

The C++ Standard Library is completely unbiased, in that it hates all compilers equally.  In order to compile Pig! (or any application built on top of the Humble Framework) I strongly recommend that you use the i386-skyos-pe cross-compiler distributed as part of the SkyOS developer's kit (SDK).  This is a port of GCC 3.4.0 that has been specifically tailored to SkyOS by Robert, and is intended to be used within a Cygwin shell under Windows.  Installing and configuring this beast can be very frustrating, but here are a couple of lessons learned to help keep the hair-pulling to a minimum:

1. Don't let Cygwin install any flavor of gcc: it's not required and only confuses matters; you will, however, need to install both the binutils and make packages.
2. If you have installed the SDK from SkyOS 5 Beta 8.2, delete the /skyos/include/stl and /skyos/include/c++/c++ folders, which contain obsolete STLPort or redundant RTL header files.
3. Once you've unpacked the cross compiler, make sure that both the build/i386-skyos-pe/bin and build/libexec/gcc/i386-skyos-pe/3.4.0 folders appear in the PATH environment variable of the Cygwin bash shell.
4. Make sure that the line
    #define _GLIBCXX_HAVE_INT64_T 1
is commented out in the /skyos/include/c++/c++config.h header file.
5. To verify that you are using the correct cross compiler, type gcc -v from the Cygwin bash shell command line.  You should see a response similar to this one:

$ gcc -v
Using built-in specs.
Configured with: ../gcc-3.4.0/configure --target=i386-skyos-pe --prefix=c:/skyos /apps/gcc/build

(Additional flags omitted for brevity)
Thread model: single
gcc version 3.4.0

Humble Framework

As previously discussed, the Humble Framework is divided into high-level modules:

For details or to download the latest version of the source code, please refer to the Humble Framework on-line documentation.  New classes added to this version are highlighted in the list below, and classes that have been significantly expanded are emphasized; classes that are marked with () and should be used with caution, as they are still being tested and are likely to change in future releases.

ADT Classes

Base Classes

Debugging Classes

GUI Classes

SYS Classes

New in this Release

Humble.cpp

After struggling mightily to keep the Humble Framework limited to only header (*.h) files, I finally threw in the towel and created a single source file (Humble.cpp) that contains global utility functions and variable declarations.  Most of these functions where previously implemented in Humble.h, which has shrunk considerable in this version.  Consequently, the DECLARE_HUMBLE_VARS() macro is no longer needed, but any application built with the framework must compile and link the Humble.cpp source file into the application.  This can be easily done in the makefile by adding $(HUMBLE_OBJ) to the list of object files:

  TARGET = Pig.app
  OBJFILES = Pig.o WSplash.o $(HUMBLE_OBJ)

HObj & HObjNoCopy

Also new to this version is that all classes are now derived from either HObj or HObjNoCopy.  Classes that can be safely assigned using the default copy constructor—or define their own—should be derived from HObj, while any class that keeps a pointer to dynamically allocated memory or a system resource should be derived from HObjNoCopy.  (For an excellent explanation of why, see Item #11 in Scott Meyer's book Effective C++.)  Astute readers will quickly recognize that it is nothing more than the previous noncopyable class renamed, mostly to conform to the emerging naming conventions within the framework.  Neither class adds very much functionality at this point—just a static ClassName() method to return the class name—but I am currently experimenting with implementations of a HObj::Load() and HObj::Save() to provide XML-based object persistence.

For now, HObj and HObjNoCopy are peers, i.e., neither is derived from the other.  A cleaner and more intuitive design would be for HObjNoCopy to be a subclass of HObj, so expect that to happen soon...

Widgets & Windows

The most significant change to this release of the Humble Framework is the concept of a 'widget', which is the new name for a generic GUI element formerly called a window. The HWidget class now contains a lot of the code that previously was internal to the HWindow<T> template class, while the HWindow<T> template class has been demoted to being nothing more than a special purpose subclass of HWidget. In other words, while a window is also a widget, a widget isn't necessarily a window.  So what's the difference?  Think of it this way: a widget is any GUI element that takes up space on the screen, whether it's a button, an icon, a text box, or even a scrolling list; widgets can contain other widgets or windows (their children) and also respond to messages from the SkyOS kernel.  Windows behave in exactly the same way, but with one major exception: widgets are drawn by the SkyGI subsystem, while windows are responsible for drawing themselves

The reworked class hierarchy can be illustrated as follows:

A good example is the new HPane class, which defines a rectangular area that is filled with a given color; since the HPane::Draw() method is implemented, i.e., the HPane draws itself, by definition it is a window. A menu, on the other hand, doesn't require a Draw() method because it is automagically drawn by the SkyGI subsystem; this makes it a widget.  If you are creating user-defined GUI element and aren't sure whether to derive your class from HWidget or HWindow<T>, as soon as you find yourself writing code in the Draw() method...it's a window.  This is further exemplified by the fact that only windows maintain a Graphic Context (m_pGC) for drawing; since widgets are drawn by the SkyGI subsystem, they do not require a GC.  (You may notice that the HWidget::Draw() method is actually implemented, but all it does is return an ERR_NOT_IMPLEMENTED error.)

The HWidget class is now a convenient place to put a ton of generic methods that (in most cases) apply to all widgets.  It is possible to get the current size (HWidget::GetHeight() / HWidget::GetWidth()), the current position in client or screen coordinates (HWidget::GetBounds() / HWidget::GetScreenBounds()), or mark parts of the widget dirty (HWidget::Smudge()).  Every HWidget object also maintains a handle to it's SkyGI counterpart in m_hMe; the HWidget object is considered to be the "boss" in this relationship: if the C++ object is destroyed, it automatically destroys the GUI object as well. To try and prevent the situation where the GUI object is destroyed without the widget object being informed, the MSG_WINDOW_DESTROY messages is trapped by the HWidget class, which give the widget C++ object the chance to clean up, which is vital because things would get unstable in a hurry if the handle referenced a window that didn't exist any more!  

To complicate matters, though, lots of widgets are destroyed automatically when their owning window is destroyed, and it isn't necessary—and may even be harmful—to explicitly destroy the SkyGI object.  These have been dubbed 'auto-destruct' widgets, which means that calling the HWidget::Destroy() method doesn't actually destroy the SkyGI object.  All it does is set the SkyGI HANDLE (m_hMe) to NULL and unhook the widget object from the message map, letting the owner clean up the SkyGI object at a later time. Since most widgets are  created as "children" of a parent window, they will fall into this category so the default value for HWidget::m_bAutoDestruct is true.  Setting m_bAutoDestruct to false by calling  HWidget::SetAutoDestruct(false) will cause the HWidget::Destroy() method to now destroy the underlying SkyGI object as well.  The m_bAutoDestruct flag can be set and reset any number of times without affecting the widget in any way, it is only significant when the HWidget::Destroy() method is called.

The way in which messages are routed to both widgets and windows is also handled a little bit differently now: a pointer to the C++ object is no longer stored in the windowData field of the s_window structure.  Instead, SkyGI messages are now routed through a single window procedure (HWidget::WidgetProc) that uses the HANDLE associated with the message to 'look up' the HWidget object associated it.  The HANDLE and HWidget pointer pairs are maintained by the new HWidgetMap class, which uses an internal std::map<> and a simple 'one deep' cache.  If a matching widget object is found, the message is routed to it via the unchanged MSG_MAP() macros, otherwise, the message is passed to the generic SkyGI window procedure.  Widgets (and windows) can also dynamically turn message routing on or off via the enableMessages() and disableMessages() methods, although this is normally handled by the HWidget and HWindow<T> classes.  Since the same window procedure can be used for all widgets and windows (which are just subclassed widgets) created by the Humble Framework, the 'special' application window procedure seen in previous versions is no longer needed. 

To summarize the new 'widgets & windows' model:

Improved Error Handling

As the Humble Framework gets more complicated, it became necessary to both beef up and standardize the way errors are handled.  Errors are still passed back to the caller as an ErrCode value (ERR_*), but the HError::GetLastError() method will yield much more information, such as the exact location in the source code where the error was raised.  Ideally, a function should call HError::NoError() to reset the error state, then set the error state by calling HError::Set() when an error is detected; this way, the error information can always be retrieved by calling HError::GetLastError()

An HError can also be thrown by calling HError::Throw(); since HError is intentionally derived from the std::logic_error exception subclass, it can be caught with a very generic catch(const std::exception &)  statement. This will catch HError exceptions, as well as any standard exceptions thrown by the C++ Standard Library.  Once a std::exception is caught, the simplest way to distinguish between a framework error and a RTL error is to test HError::GetLastError(): if it evaluates to NO_ERROR, then an RTL exception was caught.  Alternately, the  what() method—which returns a readable string describing what happened—could be queried instead, and should be valid for all exceptions caught by the generic handler. 

In choosing between HError::Set() and HError::Throw(), keep in mind that exception handling can incur a significant amount of processing overhead, especially if it is necessary to 'unwind' several stack frames.  Moreover, once an exception is thrown, it is virtually impossible to return to the point where the error occurred, making recovery difficult.  For this reason, the Humble Framework tries to avoid propagating exceptions beyond the current method; almost every method within the framework that throws exceptions also includes an appropriate catch block to handle them.  The HApp::Main() method includes a top-level try/catch block that catches any uncaught exceptions; unfortunately, this exception handler only displays an "I'm dying now." nastygram before exiting the application.  A typical way in which errors are handled within the Humble Framework is as follows:

ErrCode
MyFunction(void)
  {
  ErrCode     ec = HError::NoError();

  try
      {
      //
      // A bunch of impressive and really complicated code
      //
      if (/* it worked */)
          {
          //
          // More impressive and really complicated code
          //
          if (/* something else failed */)
              HError::Throw(ERR_WHATEVER, __FILE__, __LINE__);
          //
          // Even more impressive and really complicated code
          //
          if (MyOtherFunction() != NO_ERROR)
              throw HError::GetLastError();
          }
      else
          ec = HError::Set(ERR_OOPS, __FILE__, __LINE__);
      }
  catch (const std::exception & e)
      {
      //
      // This will catch both the HError::Throw() and the generic throw statement above
      // as well as any std::exceptions thrown by the C++ RTL.
      //
      if ((ec = ErrCode( HError::GetLastError() )) == NO_ERROR)
          ec = HError::Set(ERR_STL_EXCEPTION, __FILE__, __LINE__);
      }

  return ec;
  }

Another useful exception class is the HAssertion, which is almost identical to the HError class, except that instead of storing an ErrCode, the HAssertion stores a text string describing the problem.  Although the HAssertion class was created to implement the DEBUG_ASSERT() macro, it is available in both debug and production builds; an application can easily throw an HAssertion with a single line of code:

  HAssertion::Throw( "Something horrible happened!", __FILE__, __LINE__ );

Since both HError and HAssertion are derived from the same C++ Standard Library std::exception class, they can both be caught with the same generic catch block described above.  Also like the HError class, the HAssertion class implements the what() method that returns the string and file location passed to the Throw() method; in the code snippet above, the what() method would return "MyFile.cpp(754): Something horrible happened!"

Odds & Ends

In addition to the major changes described above, there is a multitude of minor changes, bug fixes, and enhancements.  Among the more significant are:

I have tried to remain 'backwards compatible' wherever possible, but I still consider the Humble Framework to be a 'work in progress', so be prepared for changes.  One change I can guarantee for next time is a better ChangeLog!

Application Classes

The Pig! application still consists of two unique classes, each of which has an interface (header) file and an implementation (source) file.  The classes are as follows:

The PigApp Class

The top-level class PigApp class—which represents the entire application—is declared in the header file Pig.h and is implemented in the source file Pig.cpp.  This version of the PigApp class maintains two HPane windows to do the actual drawing: one draws the fixed-width pane on the left of the main window, while the other fills the remaining window area; pointers to these two widgets are store in m_pwLeft and m_pwRight, accordingly.  These objects are created near the bottom of the PigApp::Create() method, and will persist for the duration of the application.  This also demonstrates the concept of delayed positioning: they are initially created with a height and width of zero, and then later resized to the correct position and dimensions by the call to PigApp::updateLayout() just before the main window is made visible.  This is the same method that is used to reposition the widgets when the main application window is resized by the user, which conveniently keeps all of the widget sizing and positioning code in one place.

Missing from the  PigApp class is the old AppWindowProc() function, which has been replaced by the new and improved HWidget::WidgetProc() method.  Also, the MSG_DESTROY message is now handed (instead of MSG_PRE_DESTROY) when the application is exiting.

The WSplash Class

The WSplash.cpp source file has been essentially unchanged, except that better error handling has been added to the WSplash::Create() method, and the WSplash::Draw() method has been moved from WSplash.cpp into the WSplash.h header file.

Conclusion

There are several classes in the Humble Framework—HBlowfish, HButton, HChecksum, HIcon, HMD5, HSplitter, and HTextLabel, just name a few—that are still being tested and should appear in the next release.  I was also delighted to receive the first user-contributed classes to the Humble Framework from Ville Pirhonen (aka w5) which are also waiting in the wings.  The Humble Framework is making slow but satisfactory progress, and as the 'widget & window' model grows and becomes more stable, it should get easier to fulfill the promise of some eye-candy made in the last chapter. 

Until the next chapter…Tschüß!

,,,^..^,,,

 

2004.12.10-16:02