Porting an Application to KDE 3
KDE 3.0 is out, which turned my experimental port into work that other people may actually try to compile. The application was modest, but it touched enough of KDE 2 and Qt 2 to make a straight rebuild unrealistic. I expected a grand rewrite. The successful approach was much duller: establish a clean baseline, move one boundary at a time, and keep the program runnable.
I started with a separate source copy and a fresh build directory. Building outside the source tree made stale generated files much easier to spot.
make -f Makefile.cvs
mkdir build-kde3
cd build-kde3
../configure --prefix="$KDEDIR" --enable-debug
make
The first pass was entirely mechanical. I fixed renamed headers, methods whose signatures changed, and code that depended on implicit conversions no longer accepted by Qt 3. I did not improve the design while doing this. Combining a port with a cleanup makes each failure ambiguous, and ambiguity is already supplied free of charge by C++ error messages.
Qt’s collection classes deserved special attention. Code that stored pointers in a QList-style class or relied on automatic deletion needed its ownership checked rather than merely adjusted until it compiled. Qt 3’s classes are familiar, but familiar is not identical. I wrote down who owned each object, especially widgets, list items, and objects parented to a QObject. Parent-child deletion is useful only when the parent is the intended owner.
Signals and slots were the next boundary. Every class using them needed Q_OBJECT, and every connection needed matching argument types. The string notation hides mistakes from the compiler:
connect(action, SIGNAL(activated()),
this, SLOT(openDocument()));
I ran the application from a terminal and treated every QObject::connect warning as a real defect. It is tempting to ignore one because the window still appears. That tends to last until the ignored connection is the Save action.
The KDE layer was easier once Qt was quiet. I checked command-line handling through KCmdLineArgs, application setup through KApplication, actions, standard shortcuts, and XMLGUI resource files. An old .rc file can leave menus looking plausible while actions are missing or duplicated. Removing installed test copies before checking the new one prevented Konqueror and the application from finding an obsolete resource in another prefix.
Configuration code also needed a deliberate pass. I kept reads and writes together and checked defaults rather than assuming the old configuration file contained every key:
KConfig *config = KGlobal::config();
config->setGroup("View");
bool showStatus = config->readBoolEntry("ShowStatusBar", true);
For document handling, I resisted replacing everything at once. The application already separated its document widget reasonably well, so I first made that compile, then considered whether it should become a KPart. A port is a poor time to discover that the document, window, and network code have secretly been one object wearing three hats.
The most useful testing sequence was deliberately boring:
- Start with an empty configuration directory.
- Open a local file from the command line.
- Open it through the file dialog.
- Modify, save, and reopen it.
- Exercise every action and shortcut.
- Close with unsaved changes.
- Repeat with an existing KDE 2 configuration.
I also built with debugging enabled and ran under gdb. A port often exposes old lifetime errors because widget destruction order or an event sequence changes slightly. The new library did not necessarily create the dangling pointer; it merely stopped arranging the furniture around it.
Binary compatibility was not a goal for local plug-ins compiled against KDE 2. They had to be rebuilt against the KDE 3 and Qt 3 headers and libraries. Trying to preserve old C++ objects across that boundary would save a compilation and purchase a much stranger crash.
Before calling the port complete, I also built a source archive and compiled that archive in an empty directory. A working source tree can quietly depend on generated files, unlisted headers, or resources left by yesterday’s build. make distcheck is useful when the project supports it, but even the plain exercise of unpacking the archive elsewhere catches embarrassing omissions. I then installed into a temporary prefix and started a KDE session with that prefix in its search path. This tests what was shipped rather than what happens to be within reach of the compiler.
Warnings received their own pass after behavior was correct. I enabled the warnings supported by the older compiler and looked especially for suspicious casts, hidden overloads, and variables whose type changed under Qt 3. I did not turn every warning cleanup into part of the port, but I did investigate each one. A warning newly exposed by a library transition often points directly at an assumption that deserves daylight.
My preference after this exercise is to make the smallest source change that produces correct KDE 3 behavior, then do cleanup separately. The port remains reviewable, regressions have fewer hiding places, and users get a working application sooner. It is not heroic engineering, but heroic engineering has an alarming habit of requiring heroic debugging.