Dr. Dobb's is part of the Informa Tech Division of Informa PLC

This site is operated by a business or businesses owned by Informa PLC and all copyright resides with them. Informa PLC's registered office is 5 Howick Place, London SW1P 1WG. Registered in England and Wales. Number 8860726.


Channels ▼
RSS

C/C++

Win32 Version Control


Jun00: Win32 Version Control

Mark works on IP Telephony projects at Cisco Systems. He is a frequent contributor to DDJ, and just published the second edition of his Serial Communications Developer's Guide. You can reach him at markn@ ieee.org or http://www.dogma.net/markn/. Ping programs for IP Telephony projects at Cisco Systems. She received her Ph.D. in computer science from North Carolina State University. You can reach Ping at [email protected].


Windows users are 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 these problems.

Under certain circumstances, DLL hell can become magnified to an excruciating level. We found ourselves in this position while working on a large development project that involved geographically scattered teams of programmers, distributed object technology, and dozens of DLLs and executables. With interfaces and capabilities changing on a daily basis, a good system of version management was crucial.

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

1. Looking directly at a file. Working with developers when a project is changing rapidly can be frustrating for Quality Assurance (QA) people. No matter what sort of problem QA reports, the immediate response from development goes something like this: "You need to get the latest version. That bug has been fixed."

Because of this sort of response, it is vital that end users easily determine what version of a DLL or executable is installed on their system. To facilitate this, 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 called a "version resource." Our project is built using Visual C++, which stores the version resource in an RC file. Figure 1 is a typical view of this version information from the IDE.

The resource 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 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. (Although keeping numbers in two places makes us more vulnerable to mistakes, sometimes we have to compromise to do things the Microsoft way.)

Once the version information is bound into the executable or DLL, end users 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. (The operating system shows the string values, not the fixed-info numeric values.)

2. Reading trace files. One common technique used to capture debugging information is the use of a trace or log file -- usually an ASCII text file that can be easily viewed or printed out. QA testers know that if they can attach a trace file to a bug report, the odds of a fix improve greatly. Unfortunately, developers 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. This can be done with a line like this:

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 is a sample of this dialog from a typical Windows application.

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 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 lets programming teams 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 use Visual Source Safe (VSS) 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, then select the Label menu item. This brings up a dialog box like Figure 4. Typing in the version number causes VSS 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.)

What's Wrong with this Picture?

At this point, it might seem that we've got our version control problem under control. But experienced programmers should see 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 should set off alarms, because we've set up a system that only works if we have perfect adherence to a multistep 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 10 people working on the release of 15 or 20 components. Making sure that we update all version information correctly might involve as many as 50 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 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 one-step version setting system. By updating all version information in one fell swoop, 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 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 the tool most suitable to this task is Perl.

With 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. Listing One (IncVersion.pl) is the resulting script. IncVersion.pl is invoked with an initial argument of major or minor, followed by a list of projects. It then works its way through the projects, modifying the RC file for each. There are two lines in each RC file that need to be modified. The first looks something like this:

FILEVERSION 5,0,1,0

and the second like this:

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 just shown 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. (This 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

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, you can automatically update all the components of a big project in an automated way. It 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 -- 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 Two is a C++ routine that gets 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 the following:

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

Likewise, the About Box code can also be generated dynamically. 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:

BOOL CAboutDlg::OnInitDialog()

{

stringstream s;

s << "GuiFoo.exe version "

<< ModuleVersion();

SetDlgItemText( IDC_VERSION_BOX,

s.str().c_str() );

CDialog::OnInitDialog();

return TRUE; }

BAT File Considered Helpful

With these 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 conquer. 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 VSS 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 VSS'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 VSS. A typical LABEL.BAT file will look like the following:

ss label -C -L"PROJECT 2.1"

The Perl script can be modified to generate the project label you want, but the version number in the label must be identical to the number 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 the following:

call checkout.bat

IncVersion.pl minor FOO_A FOO_B FOO_C FOO_D

call makeall.bat

call checkin.bat

ss CP $/Projects/FOO

call label.bat

The BAT files called to check out the projects, build the components, and check them back in are all specific to a particular project, and will need to be tailored to each new project's needs. CHECKIN.BAT and CHECKOUT.BAT just make calls to the command-line interface of VSS. MAKEALL.BAT invokes the command-line NMAKE tool supplied with VSS.

Conclusion

Managing version information in a large project doesn't have a nice tidy 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 ensures 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.

DDJ

Listing One

############################################################################
# This program will change the FileVersion and FILEVERSION accordingly and 
#  set them consistently across all the directories.
# To use this program, run as
#     IncVersion.pl major project-1 project-2 ...
#  or
#     IncVersion.pl minor project-1 project-2 ...
# The version info will be used to generate label.bat that can be call to 
#       label files in SourceSafe.
#############################################################################
  $maxMajor = 0;
  $maxMinor = 0;
  $count = 1;
# find the max of major and minor version number
  while ($count <= $#ARGV) {
    $file1 = $ARGV[$count]."\\".$ARGV[$count].".rc";
    eval { SetMax() };
    print $@;
    $count++;
  }
  if ($ARGV[0] eq "major") {
    $maxMajor++;
    $maxMinor = 0;
  } else {
    $maxMinor++;
  }
# write a batch file to be called later to set labels in SourceSafe
  $labelFile = "label.bat";
  open(OutLabel, ">$labelFile") || die 
                              "Failed to open $labelFile to write\n\t$!\n";
  print OutLabel "ss label -C- -L\"PROJECT $maxMajor\.$maxMinor\"\n";
  close(OutLabel);

  $count = 1;
  while ($count <= $#ARGV) {
    $file1 = $ARGV[$count]."\\".$ARGV[$count].".rc";
    $file2 = $ARGV[$count]."\\".$ARGV[$count].".tmp";
    eval { ProcessFile()};
    if ($@) { 
      print $@;
    } else {
      eval { CopyFile()};
      print $@;
    }
    $count++;
  }
sub SetMax {
  print "Find the max in $file1\n";
  open(InRC, "$file1") || die "Failed to open $file1 to read\n\t$!\n";
  while ($_ = <InRC>)   {
    if ($_ =~/FILEVERSION/) {
      @currentLine = split(' ', $_);
      @oldVersion  = split(',', $currentLine[1]);
      if ($oldVersion[0] > $maxMajor) {
        $maxMajor = $oldVersion[0];
      }
      if ($oldVersion[2] > $maxMinor) {
        $maxMinor = $oldVersion[2];
      }
    } elsif ($_  =~/FileVersion/) {
      @currentLine = split(' ', $_);
      @item        = split('"', @currentLine[2]);
      @vItem       = split(/\\/, @item[1]);
      @version     = split('\.', $vItem[0]);
      if ($version[0] > $maxMajor) {
        $maxMajor = $version[0];
      }
      if ($version[1] > $maxMinor) {
        $maxMinor = $version[1];
      }
    }
  }
  close (InRC);
}
sub ProcessFile {
  print "Converting $file1 to $file2\n";
  open(InRC, "$file1") || die "Failed to open $file1 to read\n\t$!\n";
  open(OutRC, ">$file2") || die "Failed to open $file2 to write\n\t$!\n";
  while ($_ = <InRC>)   {
    if ($_ =~/FILEVERSION/)
      {
      print OutRC " FILEVERSION $maxMajor,0,$maxMinor,0\n";
      }
    elsif ($_  =~/FileVersion/)
      {
      print OutRC "\t    VALUE \"FileVersion\", \"$maxMajor.$maxMinor\\0\"\n";
      }
    else
      {
      print OutRC "$_";
      }
  }
  close (InRC);
  close (outRC);
}
sub CopyFile {
  print "Copying $file2 to $file1\n";
  unlink($file1);
  open(InRC, "$file2") || die "Failed to open $file2 to read\n\t$!\n";
  open(OutRC, ">$file1") || die "Failed to open $file1 to write\n\t$!\n";
  while ($_ = <InRC>)   {
    print OutRC "$_";
    }
  close (InRC);
  close (outRC);
}

Back to Article

Listing Two

#include <string>
#include <vector>
using namespace std;
// Extracts the fixed-info version information from the version resource in 
// the RC file for the current module. The four integers that make up the 
// fixed-info version information are formatted into a string and returned 
// to the caller.
string FixedModuleVersion()
{
    char file_name[ MAX_PATH ];
    GetModuleFileName( ::GetModuleHandle( NULL ), file_name, MAX_PATH );
    DWORD dwDummyHandle; 
    DWORD len = GetFileVersionInfoSize( file_name, &dwDummyHandle );
    vector<BYTE> buf( len );
    ::GetFileVersionInfo( file_name, 0, len, buf.begin() );
    unsigned int ver_length;
    LPVOID lpvi;
    ::VerQueryValue( buf.begin(), "\\", &lpvi, &ver_length );
    VS_FIXEDFILEINFO fileInfo;
    fileInfo = *(VS_FIXEDFILEINFO*)lpvi;
    stringstream s;
    s << HIWORD(fileInfo.dwFileVersionMS) << "."
      << LOWORD(fileInfo.dwFileVersionMS) << "."
      << HIWORD(fileInfo.dwFileVersionLS) << "."
      << LOWORD(fileInfo.dwFileVersionLS);
    return s.str();
}
// This routine will extract the version string from the string version 
// resource in the RC file for the current module. You must add version.lib 
// to your project to link to the Win32 versioning API calls. The actual call
// VerQueryValue() uses a value of 040904B0 for the language and character set.
// This value is equivalent to English language text encoded using Unicode.
//
string StringModuleVersion()
{
    char file_name[ MAX_PATH ];
    GetModuleFileName( ::GetModuleHandle( NULL ), file_name, MAX_PATH );
    DWORD dwDummyHandle; 
    DWORD len = GetFileVersionInfoSize( file_name, &dwDummyHandle );
    vector<BYTE> buf( len );
    ::GetFileVersionInfo( file_name, 0, len, buf.begin() );
    char *version;
    unsigned int ver_length;
    ::VerQueryValue( buf.begin(), "\\StringFileInfo\\040904B0\\FileVersion",
                                          (void **) &version, &ver_length );
    return string( version, ver_length );
}









Back to Article


Related Reading


More Insights






Currently we allow the following HTML tags in comments:

Single tags

These tags can be used alone and don't need an ending tag.

<br> Defines a single line break

<hr> Defines a horizontal line

Matching tags

These require an ending tag - e.g. <i>italic text</i>

<a> Defines an anchor

<b> Defines bold text

<big> Defines big text

<blockquote> Defines a long quotation

<caption> Defines a table caption

<cite> Defines a citation

<code> Defines computer code text

<em> Defines emphasized text

<fieldset> Defines a border around elements in a form

<h1> This is heading 1

<h2> This is heading 2

<h3> This is heading 3

<h4> This is heading 4

<h5> This is heading 5

<h6> This is heading 6

<i> Defines italic text

<p> Defines a paragraph

<pre> Defines preformatted text

<q> Defines a short quotation

<samp> Defines sample computer code text

<small> Defines small text

<span> Defines a section in a document

<s> Defines strikethrough text

<strike> Defines strikethrough text

<strong> Defines strong text

<sub> Defines subscripted text

<sup> Defines superscripted text

<u> Defines underlined text

Dr. Dobb's encourages readers to engage in spirited, healthy debate, including taking us to task. However, Dr. Dobb's moderates all comments posted to our site, and reserves the right to modify or remove any content that it determines to be derogatory, offensive, inflammatory, vulgar, irrelevant/off-topic, racist or obvious marketing or spam. Dr. Dobb's further reserves the right to disable the profile of any commenter participating in said activities.

 
Disqus Tips To upload an avatar photo, first complete your Disqus profile. | View the list of supported HTML tags you can use to style comments. | Please read our commenting policy.