![]() |
Pig! — Building a SkyOS Application in C++ |
|
|
Chapter 1 |
|
It's time to get serious and build a real application using C++ for SkyOS; by "real" I mean an application that embodies all the good things that high-quality software should include, such as a clean and easily understood design, effective error checking and diagnostic functions, and a graphically pleasing user interface. It the application also does something entertaining or useful, that would be nice too! Unfortunately, right now we have none of these things, so let's get our tools together and see what happens next.
All of the source code and a precompiled binary for this chapter can be downloaded here: PigChapter01.zip.
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.
The rules.mak file is derived from the file provided with the SkyOS SDK, but has been tuned up for clarity. We start out with a couple of macros that you will have to tailor to your own environment:
Next come static (LIB_*) and shared (SHLIB_*) libraries; this section will grow if future versions of Pig! require other libraries. The DEFAULT_LIBS macro is a shorthand list of the most commonly-used libraries for C and C++ applications. We also set some default values for the compiler flags (CFLAGS) and the linker flags (LDFLAGS) (only the -fno-rtti flag is optional, removing any of the others will cause the link to fail). Finally, there is a lengthy set of make rules for different file types, all of which was copied pretty much verbatim from the original; normally, you should not have to change any of these.
The makefile file defines the source file dependencies for Pig!, and includes the global rules.mak file. For this initial version of Pig!, we only have two source files and four header files, so the dependencies have been manually added to the make file. As Pig! gets more sophisticated, the number of files will increase and the dependencies will also become more complicated; eventually, it may be necessary to use a tool like the *NIX automake, which automatically creates make files by parsing the actual source files. But for now, we'll maintain makefile the hard way:
|
# |
Before we start slinging code around, let's look at the high level design first. Since we're using C++, it makes sense to take advantage of its object-oriented features, so we will construct Pig! using one or more classes.
We'll start with the top-level class, which represents the entire application. This class—rather unimaginatively named App—is declared in the header file App.h and is implemented in the source file App.cpp. Since we may need to access our App object from anywhere in the application, it is stored as the global variable theApp. When the application is first started up, a single App object is created and will exist for as long as the application is running; consequently, we'll implement the App::Init(), App::Run() and App::Kill() methods, which are pretty self-explanatory. This also makes our main() routine very simple, as it looks something like this:
|
App theApp; |
Our App class is not quite complete, though, because the application includes a top-level window. For this reason, we'll mix the Window template class with the App class to create an "application object window". This creates an instance of the window template class called Window<App>, but we'll typedef it as base_class to make the code more legible. This means that we'll have to add some Window methods to our App class, but since the application window is not a "normal" window, it will require some special logic. We'll see more of this later in the Source Code section.
In order to keep our design simple (and hopefully easier to understand) we'll create a master window class that provides all of the essential window functionality, and then create unique subclasses for each special type of window Pig! will use. Our master window class should handle simple things like checking to see if the window is valid, or managing a context...stuff that every window would do exactly the same way. That way, we don't have to keep re-writing the same logic over and over again, we just add the code that is unique to the window being created. Initially, this list of essentials looks like this:
Create() — creates the window
Destroy() — destroys the window
Draw() — draws the contents of the window
Show() — makes the window visible
GetHeight() / GetWidth() — returns the window's dimensions
GetHandle() / GetWindowPtr() — returns a handle (or pointer) to the window's internal s_window structure
IsValidWindow() — returns true if the GUI window exists
More importantly, the Window template
class also handles all of the message routing between the window procedure and the
different window objects. It accomplishes this by using the
windowData field in the
s_window structure to store a pointer
to the window object; the generic window procedure then extracts this pointer and
invokes the appropriate object method. (There is more about this in
my discussion of Message Handing in C++). The
typical lifetime of a window object (and it's associated window) goes something
like this:
The Window object is created, either on the stack, or by calling new.
The Create() method is then called to create the GUI window that the window object will manage; subclasses should always call the Window::Create() method so that the master Window class has a chance to 'do its thing'.
Messages and commands are automagically routed from the generic window procedure to the appropriate window object methods.
If the Destroy() method is invoked—or the window object goes out of scope or is deleted—the GUI window is also destroyed; just as with Create(), subclasses should always call the Window::Destroy() method so that the master Window class can clean up.
The only thing we have to worry about now is that the window object is the boss: if it dies, it automatically destroys the GUI window as well. However, if the GUI window is destroyed without the window object being informed, things could get unstable in a hurry because the m_pWindow pointer would point to an s_window structure that doesn't exist any more! We'll have to take extra precautions to ferret out all ways that a window can be destroyed and make sure that the appropriate message is handled by the Window template class.
In addition to these two classes, we will add a small handful of utility functions that will be used throughout the application. These are typically wrappers around SkyOS API calls, and are intended to make the source code cleaner and easier to use. Right now, there are two global utility functions in Pig!, both of which are declared in Pig.h and implemented in Main.cpp:
Panic() — a fatal error handler that displays an incomprehensible error message, then exits the application
PromptUser() — a wrapper around the GI_messagebox() API call to display a prompt.
Although it's not technically a function, the HAL.h header file is also chock-full of utility goodness: it is intended to make porting code easier by added a thin layer of predefined constants and primitive data types that can be quickly tailored to different environments. (HAL = Hardware Abstraction Layer) Rather than wondering whether an int variable is 16 bits or 32 bits wide, the HAL defines things like int16 and int32 (as well as uint16 and uint32 for unsigned versions) which makes the resulting code pretty obvious. The HAL also helps overcome some minor differences in function names (strcmpi vs. stricmp) by wrapping command run-time library calls. None of this makes the resulting application any faster or smaller, but it does make the code a bit more readable; it also makes it a lot easier to bring code into Pig! from other applications that already use the HAL header file. (Like everything I've written for the past 10 years...)
To make debugging our new application a little be easier (or a lot easier in some cases!) we'll throw in some debugging functions and capabilities. All of these are controlled by the __DEBUG__ symbol, which is defined in the main Pig.h header file. If __DEBUG__ is non-zero, we are building a debug version and all of the debugging code is included; conversely, setting it to zero will build a production version in which all the debugging code 'disappears'. Currently, the debugging features are as follows:
ASSERT() — tests an expression and invokes the fatal error handler if the test fails
ASSERT_PTR() — tests a pointer and invokes the fatal error handler if the pointer is NULL
DEBUG_LOG() — writes debug output to the /boot/home/pig-debug.txt log file
DEBUG_MSG() — writes out selected messages (s_gi_msg) to the log file in readable format
DEBUG_ONLY() — includes any valid code only in debug builds
Like the other global utility functions, the debug functions DebugLog() and DebugMsg() are declared in Pig.h and implemented in Main.cpp. You should never directly call these functions, or you will get link errors in production builds; use the DEBUG_LOG and DEBUG_MSG macros instead.
We've already discussed huge chunks of the code as part of the Application Design, but it's time to look at each source file in detail. The following narrative assumes that you have either opened the appropriate file in another window, or have printed it out. For those who care, the source code is formatted using tabs set to four (4) characters .
This is the main header file, and must be the first header file included in every source file for the Pig! application: if you make any changes to this file, you should recompile everything! This header file includes the following sections:
Global Declarations — symbols such as __DEBUG__ that are used throughout the application to control what and how code is compiled
Global Constants/Definitions — defines or declares global constants that are used throughout the application.
Error Codes — an enumerated list of all error codes unique to the Pig! application
DEBUG Macros — macros used in debugging
Function Prototypes — all globally available functions should be prototyped here
This is the Hardware Abstraction Layer, and like Pig.h, if you make any changes to this file, you will have to recompile everything! This header file includes the following sections:
OS Specific Headers — includes the standard SkyOS header files skygi.h and gc.h, as well as some other standard system headers
Global Data Primitives — defines a set of primitive data types to aid portability
Global Constants — defines a set of constants to aid portability
Global Macros — defines a set of macros to aid portability
Global Classes — a small set of "mix-in" classes
This source file includes the traditional C/C++ entry point (main()) and all global utility and debug functions; it also declares all global variables. The only interesting item is the constant kLogFile, which controls where the log file will be created; if you don't want it in /boot/home (or object to the file name) you can change it here.
This is the interface to the App class and mixes in the Window template class (because the application object manages the top-level window). Included in the interface is the 'message map' that routes messages and commands sent to the application window:
|
BEGIN_MSG_MAP("App") |
Notice that this message map invokes the onRedraw() and onWindowDestroy() methods, neither of which are explicitly defined within the App class. In this circumstance, the Window::onRedraw() and Window::onWindowDestroy() methods will be invoked. In contrast, the onDestroy() method is implemented in this header file, and is connected to the MSG_DESTROY message: this causes the application to 'quit' when the top-level application window is closed.
This is the implementation of the App class, and provides some of the mandatory Window methods:
Create() — creates the main menu bar and the application window; this is slightly different from the typical creation because it uses GI_create_app() instead of GI_create_window().
Destroy() — destroys the window by invoking the Window::Destroy() method.
Draw() — draws the contents of the window, which (for now) is a boring grey rectangle.
Show() — makes the window visible; normally we would use the Window::Show() method, but because this is the top-level window, we have to use GI_ShowApplicationWindow() instead of GI_show().
This source file also provides a window procedure (AppWindowProc()) that is unique to the App class. Since the top-level application window uses the windowData field, we can't use the generic window procedure, because it expects to find a pointer to the window object in the windowData field. What we do instead is use the global theApp variable, which limits us to one instance of the App class at a time, but that's not an issue because we should only have one instance of this class anyway. (You could create additional instances, but they wouldn't receive any messages...)
This is the interface (and implementation) of the Window template class, and also defines all of the message handling macros:
BEGIN_MSG_MAP(classname) — declared at the start of a 'message
map' and includes the class name for debugging purposes;END_MSG_MAP — declared at the end of a 'message map';MAP_CMD(id,fn) — connects a specific command (MSG_COMMAND)
to a class method;MAP_CMD_RANGE(idLo,idHi,fn) — connects a range of commands to
a class method;MAP_MSG(id,fn) — connects a specific message to a class method;MAP_MSG_RANGE(idLo,idHi,fn) — connects a range of messages to
a class method.These macros all combine to create a unique HandleMessage() method for each subclass that will detect the desired messages and route them to the appropriate methods. Because this is a "mix-in" template class (as opposed to a true base class), it is possible to invoke methods in either the subclass or the template class. In other words, it is possible for the template class to call methods in the subclass, and vice versa. (Yes...I know this is some serious C++ voodoo, but trust me...it works.)
The best thing for you to do now is to grab the Pig! source code and build your own version. For those too impatient to do that—or those who don't have access to a development environment—there is a precompiled binary that can be loaded and run 'as is'. Either way, you will be presented with a very boring grey window that doesn't do anything; in fact, the menus don't even work yet, so the only way to exit the application is to use the .close box on the window. But once the application exits, you'll be able to read the pig-debug.txt file and see how the program flow went, and what messages were routed where. Not exactly a Pulitzer-prize winning novel, but a first peek at what's going on under the covers of Pig!
I also recognize that there are still some pretty ugly bits to the code, like hard-coded directories and window sizes. As Pig! continues to develop (pun intended) we'll work on getting rid of these "Bad Things" by replacing them with a better implementation.
In Chapter 2, we'll hook up the menus so that they actually work, and put up a 'splash window' for everyone to admire. And, no, I'm not going to tell you why the application is called Pig!
Until the next chapter…Tschüß!
,,,^..^,,,
|
|
|
2004.08.20-14:13