Adding SCons configure style checks to new example code.

Current Repository Revision: 32

Code highlight key
Grey: Code unchanged
Red: Code removed
Green: Code added
Blue: Code not shown

In the last post a new library (tictac) was added to the SCons test example code. Now it’s time to put some code into that library, and check that the SCons setup can handle the two libraries. The first configure style checks in the code  – checks for C++ headers – will also be added.

Firstly, a walk through the new code. The tictac library is to implement a set of classes and functions to support a game of Noughts and Crosses (or TicTacToe) for two players (one of which could be the computer, but, for the purposes of this test code, won’t be). All of the code added so far is in the mi_test_scons/src/tictac/core and its relevant tests directories.

We start by creating something to represent the contents of a square (or the Piece). This can be a nought, a cross or empty (undefined). The Piece is simply declared as an enumerated type as follows:

enum Piece
{
  NOUGHT=0,
  UNDEFINED_PIECE,
  CROSS
};

The enumerations are defined in what may seem an unusual order. This is so that bitwise operations can be performed to set/clear a defined/undefined bit, or to set the piece type. This enables a Square class to be created that acts as a kind of tri-state variable (actually it’s more of a quad-state variable as it could hold defined-cross, defined-nought, undefined-cross and undefined-nought) to represent each square on the Board.

The Square class is simply a collection of setters and getters for the contents of a single square. The content is stored as an unsigned char, which is manipulated inside the setters and getters to set it to any of the four states as mentioned above. An unsigned char is used as it is the smallest type, and only two bits are actually needed to store the Piece. The Piece type itself isn’t used because an enumerated type is (usually) the same size as an int (though I had a little bit of a mad moment when writing this and forgot why I hadn’t used the enumerated type – see revision 31, and it’s reverse merge (revision 32) in the example code ;-) ).

A bitfield could have been used with just two bits in it, but this would have to be aligned to the size of a char anyway, and its usually better to go for the simpler option. A further option would have been to simply use two bools, but this would be at least double the size of the unsigned char type. This may all seem like a small saving, but if a library client wanted to create a board with millions of Squares, or if someone wants to include it in a small embedded system, it’ll turn out to be a huge saving! These things should always be considered. One of the test systems only has 64MB RAM. This helps keep things memory efficient! :-)

The Square has two setter methods (set with a Piece which changes the content to defined with that Piece, and set without a piece that simply changes the content to defined), one clear method (to set the content as undefined) and a content method that returns the current Piece. These use bit logic to change the content of the square.

The content of the Square should be one of the following values:

Binary ValueMeaning
00UNDEFINED_PIECE/NOUGHT
01Defined/NOUGHT
10UNDEFINED_PIECE/CROSS
11Defined/CROSS

These values and the order we originally put the Piece enumerated type in makes the set with a Piece method easy to write:

void mi_ticac::Square::set(const Piece pce)
{
  content_m = static_cast<unsigned char>(pce)+1;
}

By adding one to the enumerated type, the content is set to the correct value:

PieceBinary valueOne added (binary)Final Meaning
NOUGHT0001Defined/NOUGHT
CROSS1011Defined/CROSS
UNDEFINED_PIECE0110UNDEFINED_PIECE/CROSS

Similarly, the set method without a Piece can be easily written. All that is needed in this method is ensure the defined/undefined (or least significant) bit is set to 1:

void mi_tictac::Square::set(void)
{
  content_m |= 1;
}

Using the OR operation, the least significant bit (LSB) is always set to 1 (? | 1 = 1) and the rest of the bits in the unsigned char are left as they are ( ? | 0 = ?).

Finally for the setters, we want to clear (set to 0) the defined/undefined bit. This is done in the clear method:

void mi_tictac::Square::clear(void)
{
  content_m &=2;
}

Using the AND operation with 2 (binary 10) will always clear the LSB (? & 0 = 0), and leave bit 1 (the nought/cross bit) as whatever it was (? & 1 = ?).

The last method in the Square is the getter – returning the Piece that the Square represents. This code isn’t just a case of returning the enumerated type, because it has to convert from the internal unsigned char type to the enumerated Piece type as an enumerated type cannot be set from another type. This makes sense because an enumerated type, by definition, is only permitted to hold certain values, and enabling it to be set from another type may break that rule – something which wouldn’t be known until runtime.

The code to convert is, again, pretty simple, and again it uses logical operators:

mi_tictac::Piece mi_tictac::Square::content(void) const
{
  Piece cont = UNDEFINED_PIECE;

  if (content_m & static_cast<unsigned char>(UNDEFINED_PIECE))
  {
    if (content_m & static_cast<unsigned char>(CROSS))
      cont = CROSS;
    else
      cont = NOUGHT;
  }
  return cont;
}

The logic is straightforward given what you already know from this post even if you didn’t know much about bitwise logic before! :-)

The next class in the core library is the collection of Squares – the Board. The Board class simply holds an array of nine Squares. An array is used rather than, for example, an STL container as the number of Squares required is known (though this may change in a later version!), and the array is the simplest, most-efficient way to represent the board. Board has three methods – one to set a single Square, one to get the contents of a single Square, and one to clear all squares (set them back to UNDEFINED_PIECE).

The methods setSquare and contents take an unsigned short to identify the square whose contents are to be set/returned. This number should be in the range 1-9 (this is better for humans than 0-8, even if it means the code having to subtract one for the array index). If the number is outside the range, a SquareOutOfRange exception is thrown.

An exception is used here because the way the code is to be called by clients of the library is unknown. With an exception, they can deal with it at the level of the call, turn it into an error code, allow it to pass to another level, or simply ignore it and let the program crash. In other places a return code may be used, but when something can be called from outside the library it seems a good case for an exception. This is a choice that should be made based on the situation, and always using exceptions, or always using return codes means the programmer is not thinking about what is best in a particular situation. Never think that one-way is the only way. There is no programmer’s panacea! :-)

A test was added for each class (in src/titac/core/tests) and the code compiled and tested on Linux and Windows. As the initial coding was done under Linux, there were some problems when building with MSVS.

Before moving on to the MSVS problems, a change was made to the way tests could be specified at the SCons command line. A new command line option, testname was added to enable the user to specify the name (or part of a name or path) of the test to run. This required a change to the SConstruct file:

...
    ('smalllib',
'Set to the number of object files below which a library →
small'.
     0),
    ('testname',
"The name (or part of the path) of test(s) to run - sets →
'runtests=true'",
     ''),
    BoolOption('debug','Set to apply debug flags',false),
...

A change was also necessary to the mi_scons.py file in the MI_BuildTestsAndExecute function to check for the test name in the test being run. It makes use of the fact the the Python string function find matches everything if it is trying to match an empty string:

...
trgt = env.Program(nxttst)

# Now execute
if [env['runtests'] or env['testname'] != '':
  cmnd = env['currbdir']+env['pathsep']+str(trgt[0])
  dmname = 'dummy_'+str(trgt[0])
  if cmnd.find(env['testname']) != -1:
  ··env.Command(dmname,trgt,cmnd)
...

So, for example, you can make the following calls to SCons to run specific tests:

# Run all tests
scons -Q runtests=true

# Run any test with 'board' in its name
scons -Q testname="board"

# Run all tests in the 'core' directory or anything else with
# 'core' in it's name
scons -Q testname="core"

Now to the MSVS problems. Firstly, there was a warning about structured exceptions (C4571). This is a warning that, when compiling with /EHsc structured exceptions will not be caught by catch(…). This warning has now been disabled in the SConstruct file:

#  /wd4571 - Disable this warning about not catching structured
             exceptions in catch(...) block due to many warnings
             for every catch(...) block.
#  /FC     - Display the full path of source files containing
...
genf = ' /Wall /EHsc /wd4820 /wd4710 /wd4571 /FC'
unff = ' /Za'

Another set of warnings, which have been appearing since this blog began and had begun to get too many to ignore, regarded argc and argv being unreferenced variables when compiling with MSVS. All argc and argv parameters were removed from main functions to correct this warning.

NOTE: The above paragraph may suggest that compiler warnings are occasionally ignored. You should never ignore compiler warnings as they often point to real problems. The next MSVS warning is a prime example!

MSVS also warned that the virtual function TictacException::what did not override a base class virtual member function (warning C4263). The reason for this was a missing const on the end of the function. This is an example of where a warning can point to a potential error, and also where using multiple compilers can help catch more problems than a single compiler (gcc gave no warning about this).

The final few warnings from MSVS were about type conversions. In the tests, unsigned int had been used for loop variables which call into the content function which takes an unsigned short. All loops were changed to unsigned short. A further conversion warning was given for the line that sets the square content by adding one to the Piece (Square::set). As the +1 was outside the cast, there was a warning that an unsigned char was being set from an int. This was easily solved:

void mi_tictac::Square::set(const Piece pce)
{
  // One is added to the value so that the value is set to →
defined (or in the case
  // of undefined, it will clear the defined bit).
  content_m = static_cast<unsigned char>(pce)+1;
  content_m = static_cast<unsigned char>(pce+1);
}

Once the MSVS warnings were fixed the code was committed, and rebuilt under gcc. This added a new warning about the exception specification being different between the derived TictacException::what() and the base std::exception::what(). This is simply resolved by adding throw() to the end of the what() method. Exception specifications won’t be used in the code normally, but are added here simply to clear the warning. Normally a list of exceptions thrown are put in the Doxygen documentation.

Now the code compiled cleanly with both gcc and MSVS, and we can move on to adding the first configure style checks to the code.

As the SConscript files at each level are functionally the same for each library, it would be better not to have the configure style checks just in the SConscript files that needed them, and missing from the others. To avoid this inconsistency, a new type of file was created – SConfig. A SConfig file is created only in parts of the source tree where the configure style checks need to be carried out, while other parts of the tree don’t contain one (or could contain an empty one, but that’s not necessary). The SConfig file can then be checked for in each directory’s SConscript file, and included if necessary. This only requires a small change to all SConscript files in the source tree:

# Set up the environment for this directory
loc_env.MI_SetDirectories()
srcf = loc_env.MI_GetSources()

# Make any configure style checks
if (os.path.isfile("%s/SConfig" % loc_env['currsdir'])):
  SConscript("SConfig", exports='loc_env')

# Store results
objs=[]

This change (or something similar) is included in all Library Level, Subdirectory Level and Test Builder SConscript files. A small change was also made to the SConstruct file to export the os for use in the SConscript files:

if not glob_env['forgive']:
  glob_env.Append(CCFLAGS = unff)

# Allow others to use the environment and mi_scons module
Export('glob_env', 'mi_scons', 'os')

# Pass on to the next stage
BuildDir(glob_env['bdir'],glob_env['sdir'],duplicate=0)
SConscript('%s/Sconscript' % glob_env['bdir'])

Finally the SConfig files can be added containing the configure style checks. To start with, some simple header file checks are made. The first SConfig file is placed in the src/tictac/core directory, and contains checks for the string, exception, and sstream headers used by files in that library:

# Copyright message cut out
...

# Import the environment
Import('loc_env')

# Set up the configure environment
conf = Configure(loc_env)

# Check for standard library headers
if not conf.CheckCXXHeader('string'):
  print "ERROR: String header not present."
  print "ERROR: Please ensure the Standard Library is installed →
and it"
  print "ERROR: contains <string>."
  Exit(1)

if not conf.CheckCXXHeader('exception'):
  print "ERROR: Exception header not present."
  print "ERROR: Please ensure the Standard Library is installed →
and it"
  print "ERROR: contains <exception>."
  Exit(1)

if not conf.CheckCXXHeader('sstream'):
  print "ERROR: String stream header not present."
  print "ERROR: Please ensure the Standard Library is installed →
and it"
  print "ERROR: contains <sstream>."
  Exit(1)

# Finish up the configure environment
loc_env = conf.Finish()

The tests in src/tictac/core/tests also use the iostream header, so a SConfig file is also included in that directory:

# Copyright message cut out
...

# Import the environment
Import('loc_env')

# Set up the configure environment
conf = Configure(loc_env)

# Check for standard library headers
if not conf.CheckCXXHeader('iostream'):
  print "ERROR: Input/Output stream header not present."
  print "ERROR: Please ensure the Standard Library is installed →
and it"
  print "ERROR: contains <iostream>."
  Exit(1)
# Finish up the configure environment
loc_env = conf.Finish()

Now running SCons without the -Q will show the configure checks (running with the -Q will only show any errors, not the list of checks):

[mi_test_scons]$ scons
scons: Reading SConscript files ...
Checking for C++ header file string... yes
Checking for C++ header file exception... yes
Checking for C++ header file sstream... yes
Checking for C++ header file iostream... yes
scons: done reading SConscript files.
scons: Building targets ...
...

The changes detailed in this post have been added to the example code repository. The current revision is 32.

Revision 23 added the initial tictac code, and also the testname variable to the SConstruct file and mi_scons.py.

Revision 24 corrected the Doxygen tag exceptions to exception (though the Doxygen manual claims that exceptions is a synonym for exception, Doxygen reported it as an unrecognised tag).

Revision 25 Simply added a link to the PDF version of the Doxygen documentation from the introductory page.

Revision 26 Fixed some errors in comments in exceptions.h.

Revision 27 Fixed the Windows problems outlined in this post.

Revision 28 Added the throw() exception specifier to the what() function in the TictacException to stop gcc warning.

Revision 29 Added the SConfig file in src/tictac/core and updated all the necessary SConscript files to check for and include and SConfig file.

Revision 30 Added the SConfig file to src/tictac/core/tests.

Revision 31 Was a mistake (mentioned in this post) where I forgot why I’d used unsigned char for the board rather than Piece.

Revision 32 Reverted to revision 30 to remove revision 31.

The next post will add to the tictac library, adding new checks for libraries and functions, and including replacement functions if necessary ones don’t exist. It was also noticed that the glob_env/loc_env change made in a previous post didn’t seem to work, as the foobar library wasn’t included when linking the tictac library. This will need further investigation in a later post.

When writing the code for tictac it seemed that a style guide may be useful in explaining why certain decision are made in the code. This guide will be created shortly, and will be organic – growing as thing are thought of which need to go in there. The guide will contain both coding and Doxygen stylings for the Mere Idea code base. :-)

Leave a Reply

You must be logged in to post a comment.