Pig! — Building a SkyOS Application in C++


Chapter 3

As promised at the end of Chapter 2, this chapter's main goal is persistence: being able to save something out to a file and read it back in again.  To meet this objective, almost all of the new code for Chapter 3 is found in the Humble Framework, and is based on three new classes: HFile, HSettings, and HTree.  The HSettings class provides a simple data repository that can be used to load "application settings" from a file, make changes to them as the application executes, and then finally write out the updated settings to the same file.  Of course, there's no point in creating a data repository unless there is data to be managed, so some application settings will be added the the Pig! application to test the new features.

All of the source code and a precompiled binary for this chapter can be downloaded here: PigChapter03.zip.

Humble Framework

The most complex new class in the Humble Framework is the HSettings class, which combines the HFile and HTree classes to provide a non-structured and persistent data repository.  The HList class has also appeared for the first time, even though it isn't actually used yet, it is still available for developers.

For details or to download the latest version of the source code, please refer to the Humble Framework on-line documentation.

High-Level Classes

Base Classes

Collection Classes New!

Diagnostic / Error Handling

File Classes New!

The HSettings Class

The data repository created by the HSettings class is persistent, in that it can be written out and read in from a file external to the application; it is also non-structured, in that every element within the data store consists of only a key and a value; the key is any unique string, while the value can be any string.  Since both a key and a value are required, a single element is also called a key/value pair.  To keep the framework flexible, naming conventions for keys are left the developer, and the only limitation is that the key not exceed 64 characters in length.  For example, we'll see later that the Pig! application uses the key PigApp:AutoSplash to store a boolean value (flag) that controls whether or not the splash window is displayed on startup.  The key could just as easily have been ShowSplashWindow, or even J9X83AA4: it need only be non-empty and unique within the current HSettings instance to be valid.  Values, on the other hand, do not have to be unique, and can be any non-empty string.  The HSettings class also allows for numeric (int) and flag (bool) settings, but these are simply converted to/from strings internally; a savvy developer could easily extend the HSettings class to save and load custom data types as well. 

One issue that must be dealt with is how to handle a setting that is missing; this is particularly crucial when an application is started for the first time, as the settings file may not exist at all!  The HSettings class deals with missing values by requiring all Get() operations to specify a default value; if the setting isn't already present, then it is automatically inserted with the default value.  Either way, the HSettings::Get() method is guaranteed to always return a value; HSettings does not, however, make any attempt to determine if the value is reasonable.  In a solid and robust design, the value of every application setting should be sanity-checked before actually being acted upon...just in case.

To manage the set of key/value pairs efficiently, the HSettings class uses a binary tree that is implemented in the HTree and HTreeNode template classes.  Each key/value pair is stored as a Pair structure—which is not visible outside the HSettings class—which sets a length limit of 64 characters for the key and 80 characters for the value.  The Pair structure also implements a Compare() method, which is required by the HTree class:

int Compare(Pair const & rhs) const
      { return strKey.Compare(rhs.strKey); }

This function compares the value of the Pair structure to another Pair, returning -1 if the current key is smaller, +1 if the current key is larger, or zero if the two keys are the same.  The HTree class calls this method to order nodes internally, and also to search for values within the tree.  Each node in the tree managed by the HTree class is an instance of the HTreeNode class, and includes (as the m_data element) a Pair structure.  Furthermore, since the key/value pairs are actually stored within the tree structure, the HSettings class doesn't worry about allocating or freeing memory for the key/value pairs at all: it's all handed automagically by the HTree and HTreeNode template classes.  Developers familiar with the concept will have already figured out by now that the HSettings class is little more than a glorified "string map" with the built-in ability to convert between the string and binary representations of basic data types.

To write out the key/value pairs to a file, the HSettings class is derived from the new HFile class, which wraps the UNIX standard file I/O routines and provides a little better error handing.  The key/value pairs are written out to file using the format "key=value", which makes them easy to read and edit; this is also the format used by decades of ".ini" files.  Future versions of the HSettings class will be able to categorize key/value pairs by using the [section] label, but for now they are written out to the file in alphabetical order.

Observant readers may also note that the HDebugLog class does not use the HFile class for file I/O; diagnostic code does not rely on the rest of the framework to minimize the chances of the debugging code affecting the code being debugged.  (Research the theory of Schrödinger's Cat for more on why this is important.)

Virtually all of the new classes added to the Humble Framework for this chapter will be completely invisible to the user.  It seems somewhat unjust that end-users rarely see or appreciate much of the complex design that developers must implement.  Just once I'd like to have a pop-up message that says "Hey!  I spent a lot of time testing the AVL tree code to make sure that it stays balanced, even after millions of insertions and deletions!"  Then again, coding must be kind of like parenting: as long as the end result behaves itself in public, nobody cares how it got there.

Development Tools

The single most important tool for creating our SkyOS application is the C++ compiler (gcc) which is available for a variety of platforms and operating systems from the GCC Home Page.  It doesn't matter if you are developing under Windows, Linux, or SkyOS itself, but what does matter is that you  must be using version 2.95.3-10 of gcc and it's accompanying libraries.  Newer versions will compile the code, but you will not be able to successfully link them, because the C++ runtime libraries for SkyOS have not been updated yet.  (Robert, are you listening?)  The hardest part of building the first C++ application is getting the make files properly set up so that all of the necessary libraries and startup code are linked.  We will be using two separate make files: rules.mak to define a global set of dependencies and makefile to define the dependencies unique to Pig!.  The following example is for building Pig! on a Windows system, using the Cygwin version of GCC; if you are using a different development environment, you will have to tailor the make files somewhat.  Your mileage may vary.

rules.mak

There are no changes to the rules.mak make file since the previous chapter.

makefile

The only significant change to the makefile is the inclusion of the new Humble Framework header files:.

HUMBLE = $(DIR_HUMBLE)/Humble.h \
    $(DIR_HUMBLE)/HAL.h  \
    $(DIR_HUMBLE)/HApp.h \
    $(DIR_HUMBLE)/HBase.h \
    $(DIR_HUMBLE)/HDebug.h \
    $(DIR_HUMBLE)/HError.h \
    $(DIR_HUMBLE)/HFile.h \
    $(DIR_HUMBLE)/HList.h \
    $(DIR_HUMBLE)/HSettings.h \
    $(DIR_HUMBLE)/HString.h \
    $(DIR_HUMBLE)/HTree.h \
    $(DIR_HUMBLE)/HWindow.h

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.  To take advantage of the new application settings, some minor changes have been made to this class:

The application settings added to this version of Pig! are as follows

The Init() method has been modified to make the 'Splash Window' optional, based on the PigApp:AutoSplash application setting.  If this flag is true (the default value) then the splash window is displayed during application startup; if the flag is false, then the splash window is not displayed.  There is no way within the Pig! application to change this setting (yet) but feel free to manually edit the settings file to test this feature.  The code to actually read the setting value is pretty straightforward, and looks like this:

 //
 //  Pop up the splash window, but only if the AutoSplash setting is true
 //
 if (m_settings.GetFlag("PigApp:AutoSplash", true)) //  defaults to true
     SendCommand(CMD_HELP_ABOUT);

The settings are actually loaded from the file in the new ParseCmdLine() method, which is called from HApp::Init() during the application initialization. As made clear in the method's name, ParseCmdLine() is intended to let the application read command line arguments and respond to them.  This version of Pig! ignores the command line arguments, but does load the persistent settings file, hard-coded for now as kSettingsFile (/boot/home/Pig.ini).  The ParseCmdLine() method is an ideal place to read the settings file, because it is called before the main window is created; this way, the size and position of the main window can be based on settings, instead of hardcoded values.

 HRESULT PigApp::ParseCmdLine(int32 argc, TextPtr argv[])
 {
 HRESULT     hr;

 if ((hr = base_class::ParseCmdLine(argc, argv)) != S_OK)
     return hr;
 //
 //  Try to load our settings file...it's OK if this doesn't work, because it
 //  may be the first time the app is executed.
 //
 (void) m_settings.Load(kSettingsFile);

 return S_OK;
 }

The 'new & improved' startup and shutdown sequence now looks something like this for the Pig! application: 

  main()
    PigApp::Init()
      
base_class::Init()
        PigApp::ParseCmdLine()

          base_class::ParseCmdLine()
        PigApp::Create()
          base_class::manageWindow()

    base_class::Run() (main event loop)

    PigApp::Kill()
      
base_class::Kill()

A key part of this whole process is that every time an application class overrides the Init() or Kill() methods, they must invoke the base class methods to give the HApp class a chance to perform it's own startup and shutdown processing.  This is demonstrated in the PigApp::Kill() method, which has been added to save the settings, but only if everything is shutting down gracefully.  If the application is being shutdown due to an error, then the settings file is left alone; the theory here is to avoid writing out invalid or unwanted settings caused by the error.

 int PigApp::Kill(ErrCode ec)
 {
 if (ec == NO_ERROR) // true if this is a normal shutdown
     m_settings.Save(kSettingsFile);

 return base_class::Kill(ec);
 }

Also new to the PigApp class is the onPreDestroy() message handler, which handles any MSG_PRE_DESTROY messages sent to the main window.  Since our window is about to be nuked when this message is received, this is a convenient place to capture the window's current dimensions and position, and save them for next time:

 bool PigApp::onPreDestroy(s_gi_msg const & msg, HRESULT & hr)
 {
 //
 //  Capture our window size/location and save them in the global settings.
 //
 int         nXPos,
             nYPos;

 if (GI_compute_screen_coordinates(m_pWindow, &nXPos, &nYPos) == S_OK)
     {
     m_settings.Set("PigApp:Window:XDim", GetWidth() + 1);
     m_settings.Set("PigApp:Window:YDim", GetHeight() + 1);
     m_settings.Set("PigApp:Window:XPos", nXPos - GetXPos());
     m_settings.Set("PigApp:Window:YPos", nYPos - GetYPos());
     }

 return true;
 }

In addition to all of these changes, the onAppExit() and onHelpAbout() methods were moved from the Pig.h header file into the Pig.cpp source file.

The WSplash Class

The 'splash window' hasn't been touched, so the WSplash.h header file and WSplash.cpp source file are unchanged from the last chapter.  If you're working with Pig! for the first time, don't forget to copy the Splash JPEG to /boot/home/Splash.jpg!

Conclusion

If you have downloaded and compiled the latest version of Pig!—or just tried the precompiled binary—you've probably noticed something odd: the settings for the main window size and location aren't updated properly!  You can edit the values in the Pig.ini file manually, and they'll work fine (assuming you put in reasonable values) but a quick peek in the  pig-debug.txt debug log uncovers the culprit: the settings are being updated in PigApp::onPreDestroy() after the settings file is written out in PigApp::Kill().  This means that a MSG_PRE_DESTROY message is being sent to the main window after the main event loop in HApp::Run() has already exited; which doesn't make sense.  Since an ounce of empirical evidence outweighs a pound of theory, I'm still trying to figure out why this is happening; rather than find an alternate solution, I've also intentionally left it 'broken' to encourage others to investigate the problem.  Feel free to contact me and point out any errors:  nothing makes me happier than being proved wrong by someone smarter than I am...that's called learning.

Coming Soon...

Chapter 4 will continue to focus on persistence, with a method for bundling multiple application resources together in a single external file to make it easier to install the Pig! application.

Until the next chapter…Tschüß!

,,,^..^,,,

2004.10.08-15:24