[ < ] [ > ]   [ << ] [ Up ] [ >> ]         [Top] [Contents] [Index] [ ? ]

D. Conditional Compilation

It is often necessary to arrange for a single source program to serve multiple purposes, where it is compiled in different ways to achieve these different goals. Some examples of the need for this feature are

In C, or C++, the typical approach would be to use the preprocessor that is defined as part of the language. The Ada language does not contain such a feature. This is not an oversight, but rather a very deliberate design decision, based on the experience that overuse of the preprocessing features in C and C++ can result in programs that are extremely difficult to maintain. For example, if we have ten switches that can be on or off, this means that there are a thousand separate programs, any one of which might not even be syntactically correct, and even if syntactically correct, the resulting program might not work correctly. Testing all combinations can quickly become impossible.

Nevertheless, the need to tailor programs certainly exists, and in this Appendix we will discuss how this can be achieved using Ada in general, and GNAT in particular.

D.1 Use of Boolean Constants  
D.2 Debugging - A Special Case  
D.3 Conditionalizing Declarations  
D.4 Use of Alternative Implementations  
D.5 Preprocessing  


[ < ] [ > ]   [ << ] [ Up ] [ >> ]         [Top] [Contents] [Index] [ ? ]

D.1 Use of Boolean Constants

In the case where the difference is simply which code sequence is executed, the cleanest solution is to use Boolean constants to control which code is executed.

 
FP_Initialize_Required : constant Boolean := True;
...
if FP_Initialize_Required then
...
end if;

Not only will the code inside the if statement not be executed if the constant Boolean is False, but it will also be completely deleted from the program. However, the code is only deleted after the if statement has been checked for syntactic and semantic correctness. (In contrast, with preprocessors the code is deleted before the compiler ever gets to see it, so it is not checked until the switch is turned on.)

Typically the Boolean constants will be in a separate package, something like:

 
package Config is
   FP_Initialize_Required : constant Boolean := True;
   Reset_Available        : constant Boolean := False;
   ...
end Config;

The Config package exists in multiple forms for the various targets, with an appropriate script selecting the version of Config needed. Then any other unit requiring conditional compilation can do a with of Config to make the constants visible.


[ < ] [ > ]   [ << ] [ Up ] [ >> ]         [Top] [Contents] [Index] [ ? ]

D.2 Debugging - A Special Case

A common use of conditional code is to execute statements (for example dynamic checks, or output of intermediate results) under control of a debug switch, so that the debugging behavior can be turned on and off. This can be done using a Boolean constant to control whether the code is active:

 
if Debugging then
   Put_Line ("got to the first stage!");
end if;

or

 
if Debugging and then Temperature > 999.0 then
   raise Temperature_Crazy;
end if;

Since this is a common case, there are special features to deal with this in a convenient manner. For the case of tests, Ada 2005 has added a pragma Assert that can be used for such tests. This pragma is modeled on the Assert pragma that has always been available in GNAT, so this feature may be used with GNAT even if you are not using Ada 2005 features. The use of pragma Assert is described in section `Pragma Assert' in GNAT Reference Manual, but as an example, the last test could be written:

 
pragma Assert (Temperature <= 999.0, "Temperature Crazy");

or simply

 
pragma Assert (Temperature <= 999.0);

In both cases, if assertions are active and the temperature is excessive, the exception Assert_Failure will be raised, with the given string in the first case or a string indicating the location of the pragma in the second case used as the exception message.

You can turn assertions on and off by using the Assertion_Policy pragma. This is an Ada 2005 pragma which is implemented in all modes by GNAT, but only in the latest versions of GNAT which include Ada 2005 capability. Alternatively, you can use the `-gnata' switch to enable assertions from the command line (this is recognized by all versions of GNAT).

For the example above with the Put_Line, the GNAT-specific pragma Debug can be used:

 
pragma Debug (Put_Line ("got to the first stage!"));

If debug pragmas are enabled, the argument, which must be of the form of a procedure call, is executed (in this case, Put_Line will be called). Only one call can be present, but of course a special debugging procedure containing any code you like can be included in the program and then called in a pragma Debug argument as needed.

One advantage of pragma Debug over the if Debugging then construct is that pragma Debug can appear in declarative contexts, such as at the very beginning of a procedure, before local declarations have been elaborated.

Debug pragmas are enabled using either the `-gnata' switch that also controls assertions, or with a separate Debug_Policy pragma. The latter pragma is new in the Ada 2005 versions of GNAT (but it can be used in Ada 95 and Ada 83 programs as well), and is analogous to pragma Assertion_Policy to control assertions.

Assertion_Policy and Debug_Policy are configuration pragmas, and thus they can appear in `gnat.adc' if you are not using a project file, or in the file designated to contain configuration pragmas in a project file. They then apply to all subsequent compilations. In practice the use of the `-gnata' switch is often the most convenient method of controlling the status of these pragmas.

Note that a pragma is not a statement, so in contexts where a statement sequence is required, you can't just write a pragma on its own. You have to add a null statement.

 
if ... then
   ... -- some statements
else
   pragma Assert (Num_Cases < 10);
   null;
end if;


[ < ] [ > ]   [ << ] [ Up ] [ >> ]         [Top] [Contents] [Index] [ ? ]

D.3 Conditionalizing Declarations

In some cases, it may be necessary to conditionalize declarations to meet different requirements. For example we might want a bit string whose length is set to meet some hardware message requirement.

In some cases, it may be possible to do this using declare blocks controlled by conditional constants:

 
if Small_Machine then
   declare
      X : Bit_String (1 .. 10);
   begin
      ...
   end;
else
   declare
      X : Large_Bit_String (1 .. 1000);
   begin
      ...
   end;
end if;

Note that in this approach, both declarations are analyzed by the compiler so this can only be used where both declarations are legal, even though one of them will not be used.

Another approach is to define integer constants, e.g. Bits_Per_Word, or Boolean constants, e.g. Little_Endian, and then write declarations that are parameterized by these constants. For example

 
for Rec use
  Field1 at 0 range Boolean'Pos (Little_Endian) * 10 .. Bits_Per_Word;
end record;

If Bits_Per_Word is set to 32, this generates either

 
for Rec use
  Field1 at 0 range 0 .. 32;
end record;

for the big endian case, or

 
for Rec use record
  Field1 at 0 range 10 .. 32;
end record;

for the little endian case. Since a powerful subset of Ada expression notation is usable for creating static constants, clever use of this feature can often solve quite difficult problems in conditionalizing compilation (note incidentally that in Ada 95, the little endian constant was introduced as System.Default_Bit_Order, so you do not need to define this one yourself).


[ < ] [ > ]   [ << ] [ Up ] [ >> ]         [Top] [Contents] [Index] [ ? ]

D.4 Use of Alternative Implementations

In some cases, none of the approaches described above are adequate. This can occur for example if the set of declarations required is radically different for two different configurations.

In this situation, the official Ada way of dealing with conditionalizing such code is to write separate units for the different cases. As long as this does not result in excessive duplication of code, this can be done without creating maintenance problems. The approach is to share common code as far as possible, and then isolate the code and declarations that are different. Subunits are often a convenient method for breaking out a piece of a unit that is to be conditionalized, with separate files for different versions of the subunit for different targets, where the build script selects the right one to give to the compiler.

As an example, consider a situation where a new feature in Ada 2005 allows something to be done in a really nice way. But your code must be able to compile with an Ada 95 compiler. Conceptually you want to say:

 
if Ada_2005 then
   ... neat Ada 2005 code
else
   ... not quite as neat Ada 95 code
end if;

where Ada_2005 is a Boolean constant.

But this won't work when Ada_2005 is set to False, since the then clause will be illegal for an Ada 95 compiler. (Recall that although such unreachable code would eventually be deleted by the compiler, it still needs to be legal. If it uses features introduced in Ada 2005, it will be illegal in Ada 95.)

So instead we write

 
procedure Insert is separate;

Then we have two files for the subunit Insert, with the two sets of code. If the package containing this is called File_Queries, then we might have two files

and the build script renames the appropriate file to

 
file_queries-insert.adb

and then carries out the compilation.

This can also be done with project files' naming schemes. For example:

 
For Body ("File_Queries.Insert") use "file_queries-insert-2005.ada";

Note also that with project files it is desirable to use a different extension than `ads' / `adb' for alternative versions. Otherwise a naming conflict may arise through another commonly used feature: to declare as part of the project a set of directories containing all the sources obeying the default naming scheme.

The use of alternative units is certainly feasible in all situations, and for example the Ada part of the GNAT run-time is conditionalized based on the target architecture using this approach. As a specific example, consider the implementation of the AST feature in VMS. There is one spec:

 
s-asthan.ads

which is the same for all architectures, and three bodies:

`s-asthan.adb'
used for all non-VMS operating systems
`s-asthan-vms-alpha.adb'
used for VMS on the Alpha
`s-asthan-vms-ia64.adb'
used for VMS on the ia64

The dummy version `s-asthan.adb' simply raises exceptions noting that this operating system feature is not available, and the two remaining versions interface with the corresponding versions of VMS to provide VMS-compatible AST handling. The GNAT build script knows the architecture and operating system, and automatically selects the right version, renaming it if necessary to `s-asthan.adb' before the run-time build.

Another style for arranging alternative implementations is through Ada's access-to-subprogram facility. In case some functionality is to be conditionally included, you can declare an access-to-procedure variable Ref that is initialized to designate a "do nothing" procedure, and then invoke Ref.all when appropriate. In some library package, set Ref to Proc'Access for some procedure Proc that performs the relevant processing. The initialization only occurs if the library package is included in the program. The same idea can also be implemented using tagged types and dispatching calls.


[ < ] [ > ]   [ << ] [ Up ] [ >> ]         [Top] [Contents] [Index] [ ? ]

D.5 Preprocessing

Although it is quite possible to conditionalize code without the use of C-style preprocessing, as described earlier in this section, it is nevertheless convenient in some cases to use the C approach. Moreover, older Ada compilers have often provided some preprocessing capability, so legacy code may depend on this approach, even though it is not standard.

To accommodate such use, GNAT provides a preprocessor (modeled to a large extent on the various preprocessors that have been used with legacy code on other compilers, to enable easier transition).

The preprocessor may be used in two separate modes. It can be used quite separately from the compiler, to generate a separate output source file that is then fed to the compiler as a separate step. This is the gnatprep utility, whose use is fully described in 17. Preprocessing Using gnatprep.

The preprocessing language allows such constructs as

 
#if DEBUG or PRIORITY > 4 then
   bunch of declarations
#else
   completely different bunch of declarations
#end if;

The values of the symbols DEBUG and PRIORITY can be defined either on the command line or in a separate file.

The other way of running the preprocessor is even closer to the C style and often more convenient. In this approach the preprocessing is integrated into the compilation process. The compiler is fed the preprocessor input which includes #if lines etc, and then the compiler carries out the preprocessing internally and processes the resulting output. For more details on this approach, see 3.2.17 Integrated Preprocessing.


[ << ] [ >> ]           [Top] [Contents] [Index] [ ? ]

This document was generated by GNAT Mailserver on May, 10 2012 using texi2html