[ < ] | [ > ] | [ << ] | [ Up ] | [ >> ] | [Top] | [Contents] | [Index] | [ ? ] |
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] | [ ? ] |
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] | [ ? ] |
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] | [ ? ] |
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] | [ ? ] |
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:
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] | [ ? ] |
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] | [ ? ] |