|[Home] [Credit Search] [Category Browser] [Staff Roll Call]||The LINUX.COM Article Archive|
|Originally Published: Sunday, 24 September 2000||Author: Ryan Gordon|
|Published to: enhance_articles_games/General||Page: 1/1 - [Std View]|
From MFC to GTK: A Developer's Journey
At Loki Entertainment, we make a living out of porting Windows-based games to Linux. Building a business entirely from software ports is not a new concept, even in the video game industry. For example, I remember firing up Wolfenstein-3D on a Macintosh years ago, thanks to MacPlay's porting efforts.
There is a simple reason for this: the video game industry is very volatile. A company could spend millions of dollars over 18 months to produce a game, just to have it fail in the market. This is also why successful ideas duplicate; today, most cool games are either based on, or look like, Quake. Furthermore, since the originals were so popular, we can enjoy seven sequels to King's Quest and eight repeats of Ultima. To really understand the volatility of the market, just look at the creators of those two games; Sierra and Origin, once giants in the field, are now mere ghosts of themselves. It's a vicious trade. With financial volatility as the only constant in this business, game companies just can't justify the added expense of porting games to "niche markets" like BeOS or Linux. Those that make the effort (like id and Epic), never put out the money to port the extras. It simply costs too much for something that's nice to have, but not crucial to the gaming experience.
Some of you may be thinking that porting a map editor must be simple, and therefore cheap, compared to porting a whole game. Not so. Heroes of Might and Magic 3 is approximately 250,000 lines of C++ code. Its map editor is only 75,000 lines. The game took LESS time that the editor to reach final release. The same is true for "Fear" and "Loathing", the game editors that we shipped with Myth 2.
Why is that? First and foremost, we can blame the Microsoft Foundation Classes. This API is a brick wall of incompatibility. Secondly, we can blame Visual C++'s extensions to the language.
I can't put all the blame on Microsoft, however. GCC doesn't always function as expected. The Standard Template Library (STL) and the C++ runtime tend to lag behind. And while this isn't necessarily a fault, Linux and Windows have very different paradigms of use and development that need to be addressed by the programmer, not just in operation, but also in developmental habits.
The Microsoft Foundation Classes are going to be 90% of your porting headache. Games generally don't use the MFC, so usually most the bulk of that porting work is focused on generating Linux-based graphics and sound drivers to integrate into the codebase. This is why the editors take longer: you have to deal with an almost-certain code rewrite when you encounter MFC code.
You might be able to eliminate this headache almost completely with the aid of support libraries such as Twine or MainWin, but then you get new headaches to replace the old one. MFC 4.2, an ancient version, is the last code revision Microsoft legally permits you to compile for non-Windows platforms. The source to newer revisions of MFC comes with the Visual Studio product from Microsoft. Your conscience and your lawyers can decide if you should try your luck with these.
Legality aside, don't forget your end users; not only are win32 wrappers considered to be "cheating" by the Linux community, no one wants to run a native Linux application that looks like a native Windows application. After all, if we wanted to use Windows programs, we'd just run Windows in the first place and save all this hassle. Your users demand more from you. Do not cheat them out of it.
The GNU project has produced a very high quality C compiler. Unfortunately, their C++ offering just isn't as good. I have trouble maintaining my faith in software that often announces that it has become "confused by earlier errors." To be fair, if it is dealing with syntactically stable code, the compiler works very well, but we are rarely in control of the quality of code we are porting. If nothing else, GCC is going to offer you cryptic error messages when it encounters the inevitable uses of Microsoft extensions to the C++ language.
Here are some tips, tools, and tactics to make your porting efforts less painful:
Get the right tools for the job.
Chances are, your favorite Linux distribution comes with EGCS 1.1. You are going to want to upgrade immediately to at least GCC 2.95.2. The distributed revision of the Standard Template Library is almost certainly out of date, too. At the time of this writing, the latest from SGI (http://www.sgi.com/Technology/STL/) is version 3.2. Be sure to grab this, and explicitly specify it with the "-I" option to GCC during compiles. If, during the course of your porting efforts, you find compiler or library behavior that seems incorrect, it might legitimately be a bug in the GNU software. Don't be afraid to get the bleeding-edge CVS snapshot of the compiler, since these frequently fix odd problems.
Slackware 7 users, and possibly others, are going to want to upgrade their copies of GNU debugger to a version that understands multithreaded programs. As of this writing, gdb still gets confused by classes that make use of templates and multiple inheritance, so again, check the CVS snapshot if it gives you trouble.
Next, you will want a text editor that can syntax-highlight C++ code. There are several high-quality editors for Linux that do syntax highlighting. I prefer FTE (http://fte.sourceforge.net/). The reason for this is to give a visual hint as to what is a comment and what is actual code. The rest of the syntax highlighting is less important in this case. While porting, never delete a line of code. Comment it out and make your changes below it. If you need to, comment out whole functions. You are going to find that having an immediate reference to the original code is immensely helpful, but all that text can get very confusing. Syntax highlighting makes everything much easier.
Having a CVS repository for the project, even if it's just for one developer, is almost essential. The ability to quickly determine and back out changes to the code is an enormous help, especially if a porting tactic used in one subsystem ruins another. Commit your work regularly.
Keep Visual C++ and MSDN handy.
Your worst enemy is also your best resource. Either keep a Windows box within arm's reach, or install VMware. When the program you are porting misbehaves, nothing helps as much as being able to step through it under gdb and Visual Studio simultaneously.
MSDN, the ultimate documentation for Win32 programming, takes up two CD-ROMs. If you don't have it, or can't install it, it's freely available on the web at http://msdn.microsoft.com/. Always look up unfamiliar classes and functions, as many have unexpected and non-obvious side-effects.
Glib, GDK, and GTK+ references, which are also invaluable, even if they are a sometimes a little incomplete, can be found at http://www.gtk.org/. Keep the source to these libraries around to fill in gaps where the documentation is lacking.
Glade (http://glade.pn.org/) is a user interface builder for GTK+ and Gnome. It stores interfaces in an XML document, which may then be generated at runtime with the libglade library. Changes to the user interface can even be done with a text editor, and don't require a recompile. The real benefit, however, is that, as with Visual C++, you can build your UI very quickly.
While Glade itself can generate C and C++ code, you will do better to use libglade, and write a simple C to C++ glue layer for your signal callbacks. Remember that Glade and libglade are different projects with different authors, so be sure to send questions and bug reports to the correct recipients. And, like GCC, the Glade packages that came with your distribution are probably out of date, so be sure to upgrade.
Make liberal use of assertions.
GUI applications for any platform expect certain events to occur, and not occur, in an unspoken, but specific order. Otherwise, variables don't get set, chained events don't trigger, and the program mysteriously misbehaves or crashes. Go into each MFC-based class, find the event handlers (OnMouseMove, OnPaint, etc.), and fill in some obvious conditions. Look for the data that handler uses. For example, a class's OnSize method might want to allocate an off-screen buffer that matches the new size of the screen, for later use when painting. At the start of this method, it would be wise to assert that the window's new width and height are greater than zero. Be anal about this. Check the details that couldn't possibly go wrong, because it you don't, they will, undetected.
Bitmaps are not the same in X.
In Windows, there is the concept of the "bitmap", which is a block of memory that holds a graphic. The Win32 API provides drawing primitives, such as lines and rectangles, that can be applied to bitmaps, but you may also manipulate them on the byte level. In practice, this works well for a system that has no facilities for remote display.
X and GDK separate their graphics into "drawables" and "images". Drawables are for high-level primitives, and images are for low-level byte manipulation. You may draw an image's contents to a drawable, or dump a drawable's contents to an image. With this ability to convert between formats, you might think this will be a convenient means to emulate win32 bitmaps. Wrong.
Drawables are stored in the X server. Images are stored in the client. To swap between formats, the entirety of the data must be transferred. Depending on the size of your image, this can be a noticable delay, especially over remote and ssh connections.
The solution is to choose one or the other; if you can use just drawables, then do so. If you need low-level byte manipulation, use only images, and rewrite the primitives. Functions like gdk_draw_rectangle() are actually very simple to rewrite for a GdkImage.
Implement the tiny things, scrap the big things.
There is a subconscious design to almost every program: the most frequently used routines are the simplest to reimplement. The goal is to sucessfully find the balance between what you can sucessfully fill in (CPoint, CRect), and what is best replaced in the source (CDC objects for GDK calls). The same is true for the C-callable win32 API; functions like SetTimer and GetClientRect are simple enough to rewrite in a matter of minutes with GTK+ equivalents, and tend to be used all over the place.
The balance is important, though: if you want to reimplement all the pieces of the MFC that your program uses, then expect the effort to take weeks, not days, longer.
This is only some basic advice to get you started. The only truth in porting MFC code to GTK+ is this: no matter how prepared you are, the design of the average MFC program is going to give you something you aren't expecting. Just remember to allow at least twice as much time for the project as you would initially expect to be reasonable.