DDJ Cover from June, 2000 Dr. Dobb's Journal June, 2000
by Mark Nelson and Ping Ni

Windows users are all familiar with the concept of DLL hell. This occurs when applications try to work with mismatched versions of DLLs, resulting in bugs, poor performance, and even system crashes. Although all operating systems are vulnerable to versioning troubles, Windows seems to be particularly prone to problems in this area.

Under certain circumstances, DLL hell can become magnified to an excruciating level. We found ourselves in position over the last several months while working on a large development project. This project involves geographically scattered teams of programmers, distributed object technology, and dozens of DLLs and executables. With interfaces and capabilities changing on a daily basis, we found that a good system of version management was crucial to our continued progress. When the system breaks down, mass confusion can idle dozens of highly paid programmers and Q/A personnel. This is not a good thing.

Where we need it

To get this problem under control, we first identified four critical places where we need to know the version of an executable or DLL.

1: When looking directly at a file

Working with developers when a project is changing rapidly can be very frustrating for Q/A people. It seems that no matter what sort of problem they report, the immediate response from development goes something like “You need to get the latest version, that bug has been fixed.”

Because of this sort of response, it is vital that end users be able to easily determine what version of a DLL or executable is installed on their system. To make this as easy as possible, Microsoft created a standardized method for setting product versions under Windows. One of the standard resource types that can be bound to a project is a Version resource. Our project is built using Visual C++, which stores the version resource in an RC file. Figure 1 shows a typical view of this version information from the IDE.



Figure 1
Version information in a Visual Studio project

Note that the resource shown in Figure 1 appears to have duplicates of the version information. The Key values named FILEVERSION and PRODUCTVERSION in the upper half of the display are in the fixed-info section of the resource. These two values are stored as DWORDs in the EXE or DLL file, and as such are useful for using in numerical tests of version properties. The values labeled FileVersion and ProductVersion are string parameters, and are useful for displaying to end users or printing in trace files. Keeping numbers in two places certainly makes us more vulnerable to mistakes, but sometimes we have to make compromises to do things the Microsoft way!

Once the version information is bound into the executable or DLL, an end user can view it by simply asking for the properties of the file from an Explorer view. Figure 2 shows the version information that Windows displays from this view. (Note that the O/S shows the string values, not the fixed-info numeric values.)



Figure 2
The Win32 file properties dialog

2: When reading trace files

One very common technique used to capture debugging information is the use of a trace or log file. This is usually a simple ASCII text file that can be easily viewed or printed out. Q/A testers know that if they can attach a trace file to a bug report the odds of a fix improve greatly. Unfortunately, developers will always want to know what version of the program was used to create the trace file.

A proactive way to deal with this question is to always print the component version number as one of the first lines in a trace file. That can be done with a line as simple as this:

CODE:
  1. Log <<"FOO.EXE version 1.2\n";

3: Checking versions at run time

A standard part of any Windows program is the generic About Box. Good programmers always take care to include version information in the About Box. Figure 3 shows a sample of this dialog from a typical Windows application.



Figure 3
The About Box from Microsoft Outlook Express

The About Box is just a simple Windows Dialog, which is usually defined in the RC file along with other resource information. The simplest (and probably most common) way to put the version information in the dialog is to create a static text resource that contains the ASCII text verbatim.

4: Checking out modules from source code control

Source code control is a key component of modern team-based development. Every source code control system in use today allows programming teams to label a package of files with a version number. That package can then be retrieved by name at any time in the future, which gives developers an easy way to follow the life history of bugs. We frequently find that Q/A is testing a product release that is a few versions behind what development is presently testing.

We use Visual Source Safe for our source code control system. Most of our development staff uses the GUI based front end for this program. When we want to identify a particular version of our package, we right click on a project, and then select the Label menu item. This brings up a dialog box as shown in Figure 4. Typing in the version number causes Visual Source Safe to create a checkpoint for every file in that project. At any time in the future we can ask VSS for copies of all the files for that particular version. (This can include executable and DLL versions as well.)



Figure 4
Labeling a project in Visual Source Safe

What’s wrong with this picture?

At this point it might seem that we’ve got our version control problem pretty well under control. But experienced programmers should see right away that we’ve got a real problem. We’ve got one piece of data, the version number, and it has to be stored in four different places. This situation should set off alarms any time you see it, because we’ve set up a system that only works if we have perfect adherence to a multi-step procedure. Updating version numbers means editing an RC file in at least two places, locating and editing trace file function calls, and correctly determining the projects to label in the source code control system.

A typical situation we run into at work might have a team of ten people working on the release of fifteen or twenty components. To make sure that we update all version information correctly might involve as many as fifty separate procedures. What are the chances that someone might forget to update the version number in a single About Box, or incorrectly label the VSS project in question? A single mistake such as this can cause immense confusion the next time a bug is reported for a incorrectly identified component.

While it would be possible to create a written procedure list and require people to verify each and every step, it’s infinitely better to set up a system that allows for one-step version setting system. By updating all version information in one step, we are no longer vulnerable to human frailty. If the process is set up properly one time, it should work correctly every time it is used in the future.

The Perl Solution

Physicists have been battling for years to unify the four fundamental forces of nature: gravity, electromagnetism, the weak force, and the strong force. While it may have been hubris on our part, we were determined to find a unified way to set the versions in the four different areas of our development process.

The key to the solution was finding a programmatic way to update the version numbers used everywhere in the program. The only thing we needed to do in order to make this happen was to modify the version resource in each component’s RC file. This meant doing a bit of simple parsing and text modification. And without a doubt, the tool most suitable to this task has to be Perl.

Using Perl, it was a quick matter to write a script that we now use to increment either the major or minor version of an entire batch of components. The resulting script, IncVersion.pl, is shown in Listing 1.

IncVersion.pl is invoked with an initial argument of major or minor, followed by a list of projects. It then works its way through each of the projects, modifying the RC file for each project. There are two lines in each RC file that need to be modified. The first looks something like this:

CODE:
  1. FILEVERSION       5,0,1,0

And the second looks like this:

CODE:
  1. VALUE "FileVersion",     "5.1\0"

The Perl script locates this line in each RC file, and updates the version number in one of two ways. If the initial argument is minor, the last digit in the version is incremented. For example, the version in the lines shown above will change from 5.1 to 5.2. If the initial argument is major, the first digit in the version is incremented. The version above would change from 5.1 to 6.0. (Note that this simple example uses version numbers with just two components.) The FILEVERSION string in the two examples would change to either 5,0,2,0 or 6,0,0,0.

Some programming details

The Perl script IncVersion.pl provides an automated way to change the version resource for a given DLL or EXE. By executing it with the correct list of projects, it's possible to automatically update all the components of a big project in an automated way. This is clearly going to cut down on procedural and typing mistakes. But it is still a long way from a unified version system. This only affects the first place where we need version information, which is in the Windows property view. The remaining three cases don't get any help from this at all.

As it turns out, two of the remaining cases are fixed with just a bit of code. In the strategy we were using previously, the version numbers sent to the trace file and shown in the About Box were both hard-coded. A better strategy is to extract the version information from the RC file. Listing 2 shows a C++ routine that can be used to get the version number from the RC file for a given module. We provide routines to read the version information from either the fixed-info or string version resources. (In practice you can arbitrarily choose one or the other.) Now my logging output should look something like this:

CODE:
  1. Log <<"FOO.EXE version " <<StringModuleVersion() <<"\n";

Likewise, the About Box code can be generated dynamically as well. If your About Box is a standard Windows Dialog, you only have to override the WM_INITDIALOG message and insert the appropriate text into a static text control. In an MFC app, the code might look something like this:

C++:
  1. BOOL CAboutDlg::OnInitDialog()
  2. {
  3.     stringstream s;
  4.     s <<"GuiFoo.exe version " <<ModuleVersion();
  5.     SetDlgItemText( IDC_VERSION_BOX, s.str().c_str() );
  6.     CDialog::OnInitDialog();i
  7.     return TRUE;
  8. }

BAT file considered helpful

By using these minor modifications, we now have automatic version number control in three of the four designated places. Unfortunately, the fourth target of our version control system is a little more difficult to deal with. Remember that each new version of our project needs to have a label associated with it under our version control system. It would be nice if Visual Source Safe could just read the contents of the version resource in each project's RC file, but at this time it doesn't know how to do that. Accordingly, we have to manually label each new version of our project after it has been checked in.

Although we usually use Visual Source Safe's GUI interface, it can also be invoked from the command line. We make use of this in our versioning system by having IncVersion.pl generate a BAT file that will apply the appropriate label to the currently selected project in Visual Source Safe. A typical LABEL.BAT file will look like this:

CODE:
  1. ss label -C -L"PROJECT 2.1"

The Perl script can be modified to generate the project label you want, the important thing is that the version number in the label is identical to that placed in the RC files.

Given the existence of LABEL.BAT, tying all the pieces of the puzzle together is done via one master BAT file. Ours is called UPDATE.BAT, and it looks something like this:

CODE:
  1. call checkout.bat
  2. IncVersion.pl minor FOO_A FOO_B FOO_C FOO_D
  3. all makeall.bat
  4. call checkin.bat
  5. ss CP $/Projects/FOO
  6. call label.bat

The BAT files called to check out the projects, build the components, then check them back in are all very specific to our projects, and will need to be tailored to each projects needs. CHECKIN.BAT and CHECKOUT.BAT just make calls to the command line interface of Visual Source Safe. MAKEALL.BAT invokes the command line NMAKE tool supplied with Visual Source Safe.

Conclusion

Managing version information in a large project doesn't have a nice tidy management solution, at least not with our Win32 development tools. However, by making a few modifications to our source code, and resorting to some old-fashioned command line tools, we have developed a system that does a great job of protecting us from minor version snafus. It does this by doing all version updates in one fell swoop, which makes sure that the process occurs across all components of the system at once. It isn't perfect, and may not even be ideal, but it's working for us.

Listings

Listing 1
IncVersion.pl:

PERL:
  1. #####################################################################################
  2. # This program will change the FileVersion and FILEVERSION accordingly and set them
  3. # consistently across all the directories.
  4. #
  5. # To use this program, run as
  6. #         IncVersion.pl major project-1 project-2 ...
  7. #  or
  8. #         IncVersion.pl minor project-1 project-2 ...
  9. #
  10. # The version info will be used to generate label.bat that can be call to label files
  11. #       in SourceSafe.
  12. #####################################################################################
  13.  
  14.   $maxMajor = 0;
  15.   $maxMinor = 0;
  16.   $count = 1;
  17.  
  18. # find the max of major and minor version number
  19.   while ($count <= $#ARGV) {
  20.     $file1 = $ARGV[$count]."\\".$ARGV[$count].".rc";
  21.     eval { SetMax() };
  22.     print $@;
  23.     $count++;
  24.   }
  25.  
  26.   if ($ARGV[0] eq "major") {
  27.     $maxMajor++;
  28.     $maxMinor = 0;
  29.   } else {
  30.     $maxMinor++;
  31.   }
  32.  
  33. # write a batch file to be called later to set labels in SourceSafe
  34.  
  35.   $labelFile = "label.bat";
  36.   open(OutLabel, ">$labelFile") || die "Failed to open $labelFile to write\n\t$!\n";
  37.   print OutLabel "ss label -C- -L\"PROJECT $maxMajor\.$maxMinor\"\n";
  38.   close(OutLabel);
  39.  
  40.   $count = 1;
  41.   while ($count <= $#ARGV) {
  42.     $file1 = $ARGV[$count]."\\".$ARGV[$count].".rc";
  43.     $file2 = $ARGV[$count]."\\".$ARGV[$count].".tmp";
  44.     eval { ProcessFile()};
  45.     if ($@) {
  46.       print $@;
  47.     } else {
  48.       eval { CopyFile()};
  49.       print $@;
  50.     }
  51.     $count++;
  52.   }
  53.  
  54. sub SetMax {
  55.   print "Find the max in $file1\n";
  56.   open(InRC, "$file1") || die "Failed to open $file1 to read\n\t$!\n";
  57.   while ($_ = <InRC>)  {
  58.     if ($_ =~/FILEVERSION/) {
  59.       @currentLine = split(' ', $_);
  60.       @oldVersion  = split(',', $currentLine[1]);
  61.       if ($oldVersion[0]> $maxMajor) {
  62.         $maxMajor = $oldVersion[0];
  63.       }
  64.       if ($oldVersion[2]> $maxMinor) {
  65.         $maxMinor = $oldVersion[2];
  66.       }
  67.     } elsif ($_  =~/FileVersion/) {
  68.       @currentLine = split(' ', $_);
  69.       @item        = split('"', @currentLine[2]);
  70.       @vItem       = split(/\\/, @item[1]);
  71.       @version     = split('\.', $vItem[0]);
  72.       if ($version[0]> $maxMajor) {
  73.         $maxMajor = $version[0];
  74.       }
  75.       if ($version[1]> $maxMinor) {
  76.         $maxMinor = $version[1];
  77.       }
  78.     }
  79.   }
  80.   close (InRC);
  81. }
  82.  
  83. sub ProcessFile {
  84.   print "Converting $file1 to $file2\n";
  85.   open(InRC, "$file1") || die "Failed to open $file1 to read\n\t$!\n";
  86.   open(OutRC, ">$file2") || die "Failed to open $file2 to write\n\t$!\n";
  87.   while ($_ = <InRC>)  {
  88.     if ($_ =~/FILEVERSION/)
  89.       {
  90.       print OutRC " FILEVERSION $maxMajor,0,$maxMinor,0\n";
  91.       }
  92.     elsif ($_  =~/FileVersion/)
  93.       {
  94.       print OutRC "\t    VALUE \"FileVersion\", \"$maxMajor.$maxMinor\\0\"\n";
  95.       }
  96.     else
  97.       {
  98.       print OutRC "$_";
  99.       }
  100.   }
  101.   close (InRC);
  102.   close (outRC);
  103. }
  104.  
  105. sub CopyFile {
  106.   print "Copying $file2 to $file1\n";
  107.   unlink($file1);
  108.   open(InRC, "$file2") || die "Failed to open $file2 to read\n\t$!\n";
  109.   open(OutRC, ">$file1") || die "Failed to open $file1 to write\n\t$!\n";
  110.   while ($_ = <InRC>)  {
  111.     print OutRC "$_";
  112.     }
  113.   close (InRC);
  114.   close (outRC);
  115. }

Listing 2
Getting VersionInfo in C++:

C++:
  1. #include <string>
  2. #include <vector>
  3. using namespace std;
  4.  
  5. //
  6. // This routine will extract the fixed-info version
  7. // information from the version resource in the RC
  8. // file for the current module. The four integers
  9. // that make up the fixed-info version information are
  10. // formatted into a string and returned to the caller.
  11. //
  12. string FixedModuleVersion()
  13. {
  14.     char file_name[ MAX_PATH ];
  15.     GetModuleFileName( ::GetModuleHandle( NULL ), file_name, MAX_PATH );
  16.     DWORD dwDummyHandle;
  17.     DWORD len = GetFileVersionInfoSize( file_name, &dwDummyHandle );
  18.     vector<BYTE> buf( len );
  19.     ::GetFileVersionInfo( file_name, 0, len, buf.begin() );
  20.     unsigned int ver_length;
  21.     LPVOID lpvi;
  22.     ::VerQueryValue( buf.begin(),
  23.                      "\\",
  24.                      &lpvi,
  25.                      &ver_length );
  26.     VS_FIXEDFILEINFO fileInfo;
  27.     fileInfo = *(VS_FIXEDFILEINFO*)lpvi;
  28.     stringstream s;
  29.     s <<HIWORD(fileInfo.dwFileVersionMS) <<"."
  30.       <<LOWORD(fileInfo.dwFileVersionMS) <<"."
  31.       <<HIWORD(fileInfo.dwFileVersionLS) <<"."
  32.       <<LOWORD(fileInfo.dwFileVersionLS);
  33.     return s.str();
  34. }
  35.  
  36. //
  37. // This routine will extract the version string from the
  38. // string version resource in the RC file for the current module.
  39. // Note that you must add version.lib to your project to
  40. // link to the Win32 versioning API calls. The actual call
  41. // VerQueryValue() uses a value of 040904B0 for the language
  42. // and character set. This value is equivalent to English
  43. // language text encoded using Unicode.
  44. //
  45. string StringModuleVersion()
  46. {
  47.     char file_name[ MAX_PATH ];
  48.     GetModuleFileName( ::GetModuleHandle( NULL ), file_name, MAX_PATH );
  49.     DWORD dwDummyHandle;
  50.     DWORD len = GetFileVersionInfoSize( file_name, &dwDummyHandle );
  51.     vector<BYTE> buf( len );
  52.     ::GetFileVersionInfo( file_name, 0, len, buf.begin() );
  53.     char *version;
  54.     unsigned int ver_length;
  55.  
  56.     ::VerQueryValue( buf.begin(),
  57.                      "\\StringFileInfo\\040904B0\\FileVersion",
  58.                      (void **) &version,
  59.                      &ver_length );
  60.     return string( version, ver_length );
  61. }