168 Ada 95 QUALITY AND STYLE
PORTABILITY 143
CHAPTER 7
Portability
Discussions concerning portability usually concentrate on the
differences in computer systems, but the development and run-time
environment may also change:
portability (software). The ease with which software can be
transferred from one computer system or environment to
another (IEEE Dictionary 1984).
Most portability problems are not pure language issues.
Portability involves hardware (byte order, device I/O) and
software (utility libraries, operating systems, run-time
libraries). This chapter will not address these challenging
design issues.
This chapter does identify the more common portability problems
that are specific to Ada when moving from one platform or
compiler to another. It also suggests ways that nonportable code
can be isolated. By using the implementation hiding features of
Ada, the cost of porting can be significantly reduced.
In fact, many language portability issues are solved by the
strict definition of the Ada language itself. In most programming
languages, different dialects are prevalent as vendors extend or
dilute a language for various reasons: conformance to a
programming environment or features for a particular application
domain. The Ada Compiler Validation Capability (ACVC) was
developed by the U.S. Department of Defense at the Ada Validation
Facility, ASD/SIDL, Wright-Patterson Air Force Base, to ensure
that implementors strictly adhered to the Ada standard.
As part of the strict definition of Ada, certain constructs are
defined to be erroneous, and the effect of executing an erroneous
construct is unpredictable. Therefore, erroneous constructs are
obviously not portable. Erroneous constructs and bounded errors
are discussed in Guideline 5.9.1 through 5.9.10 and are not
repeated in this chapter.
Most programmers new to the language expect Ada to eliminate all
portability problems; it definitely does not. Certain areas of
Ada are not yet covered by validation. The definition of Ada
leaves certain details to the implementor. The compiler
implementor’s choices, with respect to these details, affect
portability.
The revisions to the Ada language approved in the 1995 standard
generate a new area of portability concerns. Some programs are
intended to have a long life and may start in Ada 83 (Ada
Reference Manual 1983) but transition to Ada 95 (Ada Reference
Manual 1995). Although this style guide focuses on the current
Ada standard and does not address transition issues, there are
portability issues relating to using certain features of the
language. These issues revolve around the language features
designated as obsolescent in Annex J of the Ada Reference Manual
(1995).
The constructs of the language have been developed to satisfy a
series of needs. These constructs can legitimately be used even
though they may impact portability. There are some general
principles to enhancing portability that are exemplified by many
of the guidelines in this chapter. They are:
• Recognize those Ada constructs that may adversely affect
portability on the relevant implementations or platforms.
• Rely on those Ada constructs that depend on characteristics
shared by all relevant implementations. Avoid the use of
those constructs whose implementation characteristics vary
on the relevant platforms.
• Localize and encapsulate nonportable features of a program
if their use is essential.
• Highlight the use of constructs that may cause portability
problems.
These guidelines cannot be applied thoughtlessly. Many of them
involve a detailed understanding of the Ada model and its
implementation. In many cases, you will have to make carefully
considered tradeoffs between efficiency and portability. Reading
this chapter should improve your insight into the tradeoffs
involved.
The material in this chapter was largely acquired from three
sources: the Ada Run-Time Environments Working Group (ARTEWG)
Catalogue of Ada Runtime Implementation Dependencies (ARTEWG
1986); the Nissen and Wallis book on Portability and Style in Ada
(Nissen and Wallis 1984); and a paper written for the U.S. Air
Force by SofTech on Ada Portability Guidelines (Pappas 1985). The
last of these sources (Pappas 1985) encompasses the other two and
provides an in-depth explanation of the issues, numerous
examples, and techniques for minimizing portability problems.
Conti (1987) is a valuable reference for understanding the
latitude allowed for implementors of Ada and the criteria often
used to make decisions.
This chapter’s purpose is to provide a summary of portability
issues in the guideline format of this book. The chapter does not
include all issues identified in the references but only the most
significant. For an in-depth presentation, see Pappas (1985). A
few additional guidelines are presented here and others are
elaborated upon where applicable. For further reading on Ada I/O
portability issues, see Matthews (1987), Griest (1989), and CECOM
(1989).
Some of the guidelines in this chapter cross reference and place
stricter constraints on other guidelines in this book. These
constraints apply when portability is being emphasized.
Guidelines in this chapter are frequently worded “consider . . .”
because hard and fast rules cannot apply in all situations. The
specific choice you make in a given situation involves design
tradeoffs. The rationale for these guidelines is intended to
give you insight into some of these tradeoffs.
7.1 FUNDAMENTALS
This section introduces some generally applicable principles of
writing portable Ada programs. It includes guidelines about the
assumptions you should make with respect to a number of Ada
features and their implementations and guidelines about the use
of other Ada features to ensure maximum portability.
7.1.1 Obsolescent Features
guideline
• In programs or components intended to have a long life,
avoid using the features of Ada declared as “obsolescent” by
Annex J of the Ada Reference Manual (1995), unless the use
of the feature is needed for backward compatibility with Ada
83 (Ada Reference Manual 1983).
• Document the use of any obsolescent features.
• Avoid using the following features:
- The short renamings of the packages in the predefined
environment (e.g., Text_IO as opposed to Ada.Text_IO)
- The character replacements of ! for |, : for #, and % for
quotation marks
- Reduced accuracy subtypes of floating-point types
- The 'Constrained attribute as applied to private types
- The predefined package ASCII
- The exception Numeric_Error
- Various representation specifications, including at
clauses, mod clauses, interrupt entries, and the
Storage_Size attribute
rationale
Ten years of reflection on the use of Ada 83 led to the
conclusion that some features of the original language are not
as useful as originally intended. These features have been
replaced with others in the Ada 95 revision. It would have
been desirable to remove the obsolescent features completely,
but that would have prevented the upward compatible transition
of programs from Ada 83 to Ada 95. Thus, the obsolescent
features remain in the language and are explicitly labeled as
such in Annex J of the Ada Reference Manual (1995). The
features listed in Annex J are candidates for removal from the
language during its next revision.
If a program’s lifetime may extend beyond the next language
revision, it should avoid the obsolescent language features
unless backward compatibility with Ada 83 forces their use.
exceptions
When you instantiate Ada.Text_IO.Float_IO, the values of the
Default_Fore and Default_Aft fields are set from the values of
the 'Fore and 'Aft attributes of the actual floating-point
type used in the instantiation. If you declare a reduced
accuracy floating-point type that you then use to instantiate
Ada.Text_IO.Float_IO, the output field widths are determined
from the reduced accuracy type, although the implementation
accuracy is unchanged (Rationale 1995, §3.3).
7.1.2 Global Assumptions
guideline
• Make informed assumptions about the support provided for the
following on potential target platforms:
- Number of bits available for type Integer (range
constraints)
- Number of decimal digits of precision available for
floating-point types
- Number of bits available for fixed-point types (delta and
range constraints)
- Number of characters per line of source text
- Number of bits for Root_Integer expressions
- Number of seconds for the range of Duration
- Number of milliseconds for Duration'Small
- Minimum and maximum scale for decimal types
• Avoid assumptions about the values and the number of values
included in the type Character.
instantiation
These are minimum values (or minimum precision in the case of
Duration'Small) that a project or application might assume
that an implementation provides. There is no guarantee that a
given implementation provides more than the minimum, so these
would be treated by the project or application as maximum
values also.
- 16 bits available for type Integer (-2**15 .. 2**15 - 1)
- 6 decimal digits of precision available for floating-
point types
- 24 bits available for fixed-point types
- 200 characters per line of source text
- 16 bits for expressions
- -86_400 .. 86_400 seconds (1 day) for the range of
Duration (as specified in Ada Reference Manual [1995,
§9.6])
- 20 milliseconds for Duration'Small (as specified in Ada
Reference Manual [1995, §9.6])
rationale
Some assumptions must be made with respect to certain
implementation-specific values. The exact values assumed
should cover the majority of the target equipment of interest.
Choosing the lowest common denominator for values improves
portability.
Implementations may supply an alternate character set specific
to a locale or environment. For instance, the implementation
on an IBM-compatible PC may support that machine’s native
character set rather than
Latin 1. As a result, some character values may or may not be
supported, for example, the smiley face.
notes
Of the microcomputers currently available for incorporation
within embedded systems, 16-bit and 32-bit processors are
prevalent. Using current representation schemes, 6 decimal
digits of floating point accuracy imply a representation
mantissa at least 21 bits wide, leaving 11 bits for exponent
and sign within a 32-bit representation. This correlates with
the data widths of floating point hardware currently available
for the embedded systems market. A 32-bit minimum on fixed-
point numbers correlates with the accuracy and storage
requirements of floating point numbers. The 16-bit example for
Root_Integer expressions matches that for Integer storage.
(The 32-bit integers can be assumed if the application will
only be considered for 32-bit processors with a corresponding
32-bit operating system and supporting compiler.)
The values for the range and accuracy of values of the
predefined type Duration are the limits expressed in the Ada
Reference Manual (1995, §9.6). You should not expect an
implementation to provide a wider range or a finer
granularity.
A standard-mode Ada character set of Latin 1 can be assumed in
most cases for the contents and internal behavior of type
Character and packages Character.Latin_1,
Character.Handling, and Strings.Maps. However, this does not
mean that the target hardware platform is capable of
displaying the entire character set. You should not use a
nonstandard Ada character set unless intentionally producing a
nonportable user interface with a specific purpose.
7.1.3 Comments
guideline
• Use highlighting comments for each package, subprogram, and
task where any nonportable features are present.
• For each nonportable feature employed, describe the
expectations for that feature.
example
------------------------------------------------------------
------------
package Memory_Mapped_IO is
-- WARNING - This package is implementation specific.
-- It uses absolute memory addresses to interface with
the I/O
-- system. It assumes a particular printer's line length.
-- Change memory mapping and printer details when
porting.
Printer_Line_Length : constant := 132;
type Data is array (1 .. Printer_Line_Length) of
Character;
procedure Write_Line (Line : in Data);
end Memory_Mapped_IO;
------------------------------------------------------------
------------
with System;
with System.Storage_Elements;
package body Memory_Mapped_IO is
-- WARNING: Implementation specific memory address
Buffer_Address : constant System.Address
:= System.Storage_Elements.To_Address(16#200#);
---------------------------------------------------------
------------
procedure Write_Line (Line : in Data) is
Buffer : Data;
for Buffer'Address use Buffer_Address;
begin -- Write_Line
-- perform output operation through specific memory
locations.
...
end Write_Line;
---------------------------------------------------------
------------
end Memory_Mapped_IO;
------------------------------------------------------------
------------
rationale
Explicitly commenting each breach of portability will raise
its visibility and aid in the porting process. A description
of the nonportable feature’s expectations covers the common
case where vendor documentation of the original implementation
is not available to the person performing the porting process.
7.1.4 Main Subprogram
guideline
• Consider using only a parameterless procedure as the main
subprogram.
• Consider using Ada.Command_Line for accessing values from
the environment, but recognize that this package’s behavior
and even its specification are nonportable (see Guideline
7.1.6).
• Encapsulate and document all uses of package
Ada.Command_Line.
example
The following example encapsulates the arguments for a
hypothetical “execution mode” argument passed from the
environment. It encapsulates both the expected position and
the expected values of the argument, as well as provides a
default in cases where the environment was unable to provide
the information:
package Environment is
type Execution_Mode is (Unspecified, Interactive, Batch);
function Execution_Argument return Execution_Mode;
...
end Environment;
------------------------------------------------------------
----------
with Ada.Command_Line; use Ada.Command_Line;
with Ada.Strings.Unbounded; use Ada.Strings.Unbounded;
package body Environment is
function Execution_Argument return Execution_Mode is
Execution_Argument_Number : constant := 1;
Interactive_Mode_String : constant String := "-i";
Batch_Mode_String : constant String := "-b";
begin
if Argument_Count < Execution_Argument_Number then
return Unspecified;
elsif To_Unbounded_String (Argument
(Execution_Argument_Number)) =
Interactive_Mode_String then
return Interactive;
elsif To_Unbounded_String (Argument
(Execution_Argument_Number)) =
Batch_Mode_String then
return Batch;
else
return Unspecified;
end if;
end Execution_Argument;
end Environment;
rationale
The predefined language environment declares the package
Ada.Command_Line, providing a standardized way for a program
to obtain the values of a command line. Because all Ada
compilers must implement the packages in the predefined
language environment, you can create a program that is more
portable, maintainable, and readable by using this package.
You should, however, be aware that even though the language
defines the objects and type profiles of this package, it does
not force a relationship between the function results and any
other entity or operation, and thus, allows the possibility of
a nonportable behavior and specification.
The value returned by the function
Ada.Command_Line.Argument_Count is implementation-dependent.
Different operating systems follow different conventions
regarding the parsing and meaning of command line parameters.
To enhance your program’s portability, assume the simplest
case: that the external execution environment does not support
passing arguments to a program.
Some operating systems are capable of acquiring and
interpreting returned integer values near 0 from a function,
but many others cannot. Further, many real-time, embedded
systems will not be designed to terminate, so a function or a
procedure having parameters with modes out or in out will be
inappropriate to such applications.
This leaves procedures with in parameters. Although some
operating systems can pass parameters into a program as it
starts, others are not. Also, an implementation may not be
able to perform type checking on such parameters even if the
surrounding environment is capable of providing them.
notes
Real-time, embedded applications may not have an “operator”
initiating the program to supply the parameters, in which case
it would be more appropriate for the program to have been
compiled with a package containing the appropriate constant
values or for the program to read the necessary values from
switch settings or a downloaded auxiliary file. In any case,
the variation in surrounding initiating environments is far
too great to depend upon the kind of last-minute (program)
parameterization implied by (subprogram) parameters to the
main subprogram.
POSIX 5 provides a standard operating system command line
interface that might be a more appropriate alternative to the
Ada command line facility depending on the implementation
family of an application.
7.1.5 Encapsulating Implementation Dependencies
guideline
• Create packages specifically designed to isolate hardware
and implementation dependencies and designed so that their
specification will not change when porting.
• Clearly indicate the objectives if machine or solution
efficiency is the reason for hardware or implementation-
dependent code.
• For the packages that hide implementation dependencies,
maintain different package bodies for different target
environments.
• Isolate interrupt receiving tasks into implementation-
dependent packages.
• Refer to Annex M of the Ada Reference Manual (1995) for a
list of implementation-dependent features.
example
See Guideline 7.1.3.
rationale
Encapsulating hardware and implementation dependencies in a
package allows the remainder of the code to ignore them and,
thus, to be fully portable. It also localizes the
dependencies, making it clear exactly which parts of the code
may need to change when porting the program.
Some implementation-dependent features may be used to achieve
particular performance or efficiency objectives. Commenting
these objectives ensures that the programmer can find an
appropriate way to achieve them when porting to a different
implementation or explicitly recognize that they cannot be
achieved.
Interrupt entries are implementation-dependent features that
may not be supported (e.g., VAX Ada uses pragmas to assign
system traps to “normal” rendezvous). However, interrupt
entries cannot be avoided in most embedded, real-time systems,
and it is reasonable to assume that they are supported by an
Ada implementation. The value for an interrupt is
implementation-defined. Isolate it.
notes
You can use Ada to write machine-dependent programs that take
advantage of an implementation in a manner consistent with the
Ada model but that make particular choices where Ada allows
implementation freedom. These machine dependencies should be
treated in the same way as any other implementation-dependent
features of the code.
7.1.6 Implementation-Added Features
guideline
• Avoid the use of vendor-supplied packages.
• Avoid the use of features added to the predefined packages
that are not specified in the Ada language definition or
Specialized Needs Annexes.
rationale
Vendor-added features are not likely to be provided by other
implementations. Even if a majority of vendors eventually
provide similar additional features, they are unlikely to have
identical formulations. Indeed, different vendors may use the
same formulation for (semantically) entirely different
features. See
Guideline 7.5.2 for further information on vendor-supplied
exceptions.
Ada has introduced a number of new pragmas and attributes that
were not present in Ada 83 (Ada Reference Manual 1983). These
new pragmas and attributes may clash with implementation-
defined pragmas and attributes.
exceptions
There are many kinds of applications that require the use of
these features. Examples include multilingual systems that
standardize on a vendor’s file system, applications that are
closely integrated with vendor products (i.e., user
interfaces), and embedded systems for performance reasons.
Isolate the use of these features into packages.
If a vendor-supplied package is provided in compilable source
code form, use of the package does not make a program
nonportable provided that the package does not contain any
nonportable code and can be lawfully included in your program.
7.1.7 Specialized Needs Annexes
guideline
• Use features defined in the Specialized Needs Annexes rather
than vendor-defined features.
• Document clearly the use of any features from the
Specialized Needs Annexes (systems programming, real-time
systems, distributed systems, information systems, numerics,
and safety and security).
rationale
The Specialized Needs Annexes define standards for specific
application areas without extending the syntax of the
language. You can port a program with specific domain needs
(e.g., distributed systems, information systems) across vendor
implementations more easily if they support the features
standardized in an annex rather than rely on specific vendor
extensions. The purpose of the annexes is to provide a
consistent and uniform way to address issues faced in several
application areas where Ada is expected to be used. Because
different compilers will support different sets of annexes if
any, you may have portability problems if you rely on the
features defined in any given annex.
The Specialized Needs Annexes provide special capabilities
that go beyond the core language definition. Because compilers
are not required to support the special-purpose annexes, you
should localize your use of these features where possible. By
documenting their usage, you are leaving a record of potential
porting difficulties for future programmers.
7.1.8 Dependence on Parameter Passing Mechanism
guideline
• Do not write code whose correct execution depends on the
particular parameter passing mechanism used by an
implementation (Ada Reference Manual 1995, §6.2; Cohen
1986).
• If a subprogram has more than one formal parameter of a
given subtype, at least one of which is [in] out, make sure
that the subprogram can properly handle the case when both
formal parameters denote the same actual object.
example
The output of this program depends on the particular parameter
passing mechanism that was used:
------------------------------------------------------------
------------
with Ada.Integer_Text_IO;
procedure Outer is
type Coordinates is
record
X : Integer := 0;
Y : Integer := 0;
end record;
Outer_Point : Coordinates;
---------------------------------------------------------
------------
procedure Inner (Inner_Point : in out Coordinates) is
begin
Inner_Point.X := 5;
-- The following line causes the output of the program
to
-- depend on the parameter passing mechanism.
Ada.Integer_Text_IO.Put(Outer_Point.X);
end Inner;
---------------------------------------------------------
------------
begin -- Outer
Ada.Integer_Text_IO.Put(Outer_Point.X);
Inner(Outer_Point);
Ada.Integer_Text_IO.Put(Outer_Point.X);
end Outer;
------------------------------------------------------------
------------
If the parameter passing mechanism is by copy, the results on
the standard output file are:
0 0 5
If the parameter passing mechanism is by reference, the
results are:
0 5 5
The following code fragment shows where there is a potential
for bounded error when a procedure is called with actual
parameters denoting the same object:
procedure Test_Bounded_Error (Parm_1 : in out Integer;
Parm_2 : in out Integer) is
procedure Inner (Parm : in out Integer) is
begin
Parm := Parm * 10;
end Inner;
begin
Parm_2 := 5;
Inner (Parm_1);
end Test_Bounded_Error;
In executing the procedure Test_Bounded_Error, both Parm_1 and
Parm_2 denote the object Actual_Parm. After executing the
first statement, the object Actual_Parm has the value 5. When
the procedure Inner is called, its formal parameter Parm
denotes Actual_Parm. It cannot be determined whether it
denotes the old value of Parm_1, in this case 1, or the new
value, in this case 5.
Actual_Parm : Integer := 1;
. . .
Test_Bounded_Error (Actual_Parm, Actual_Parm); -- potential
bounded error
rationale
Certain composite types (untagged records and arrays) can be
passed either by copy or by reference. If there are two or
more formal parameters of the same type, one or more of which
is writable, then you should document whether you assume that
these formal parameters do not denote the same actual object.
Similarly, if a subprogram that has a formal parameter of a
given subtype also makes an up-level reference to an object of
this same type, you should document whether you assume that
the formal parameter denotes a different object from the
object named in the up-level reference. In these situations
where an object can be accessed through distinct formal
parameter paths, the exception Program_Error may be raised,
the new value may be read, or the old value of the object may
be used (Ada Reference Manual 1995, §6.2).
See also Guideline 8.2.7.
exceptions
Frequently, when interfacing Ada to foreign code, dependence
on parameter-passing mechanisms used by a particular
implementation is unavoidable. In this case, isolate the calls
to the foreign code in an interface package that exports
operations that do not depend on the parameter-passing
mechanism.
7.1.9 Arbitrary Order Dependencies
guideline
• Avoid depending on the order in which certain constructs in
Ada are evaluated.
example
The output of this program depends upon the order of
evaluation of subprogram parameters, but the Ada Reference
Manual (1995, §6.4) specifies that these evaluations are done
in an arbitrary order:
package Utilities is
function Unique_ID return Integer;
end Utilities;
package body Utilities is
ID : Integer := 0;
function Unique_ID return Integer is
begin
ID := ID + 1;
return ID;
end Unique_ID;
end Utilities;
------------------------------------------------------------
--------------------
with Ada.Text_IO;
with Utilities; use Utilities;
procedure P is
begin
Ada.Text_IO.Put_Line (Integer'Image(Unique_ID) &
Integer'Image(Unique_ID));
end P;
If the parameters to the “&” function are evaluated in textual
order, the output is:
1 2
If the parameters are evaluated in the reverse order, the
output is:
2 1
rationale
The Ada language defines certain evaluations to occur in
arbitrary order (e.g., subprogram parameters). While a
dependency on the order of evaluation may not adversely affect
the program on a certain implementation, the code might not
execute correctly when it is ported. For example, if two
actual parameters of a subprogram call have side effects, the
effect of the program could depend on the order of evaluation
(Ada Reference Manual 1995, §1.1.4). Avoid arbitrary order
dependencies, but also recognize that even an unintentional
error of this kind could prohibit portability.
7.2 NUMERIC TYPES AND EXPRESSIONS
A great deal of care was taken with the design of the Ada
features related to numeric computations to ensure that the
language could be used in embedded systems and mathematical
applications where precision was important. As far as possible,
these features were made portable. However, there is an
inevitable tradeoff between maximally exploiting the available
precision of numeric computation on a particular machine and
maximizing the portability of Ada numeric constructs. This means
that these Ada features, particularly numeric types and
expressions, must be used with great care if full portability of
the resulting program is to be guaranteed.
7.2.1 Predefined Numeric Types
guideline
• Avoid using the predefined numeric types in package
Standard. Use range and digits declarations and let the
implementation pick the appropriate representation.
• For programs that require greater accuracy than that
provided by the global assumptions, define a package that
declares a private type and operations as needed; see Pappas
(1985) for a full explanation and examples.
• Consider using predefined numeric types (Integer, Natural,
Positive) for:
- Indexes into arrays where the index type is not
significant, such as type String
- “Pure” numbers, that is, numbers with no associated
physical unit (e.g., exponents)
- Values whose purpose is to control a repeat or iteration
count
example
The second and third examples below are not representable as
subranges of Integer on a machine with a
16-bit word. The first example below allows a compiler to
choose a multiword representation, if necessary.
Use:
type Second_Of_Day is range 0 .. 86_400;
rather than:
type Second_Of_Day is new Integer range 1 .. 86_400;
or:
subtype Second_Of_Day is Integer range 1 .. 86_400;
rationale
An implementor is free to define the range of the predefined
numeric types. Porting code from an implementation with
greater accuracy to one of lesser accuracy is a time consuming
and error-prone process. Many of the errors are not reported
until run-time.
This applies to more than just numerical computation. An easy-
to-overlook instance of this problem occurs if you neglect to
use explicitly declared types for integer discrete ranges
(array sizes, loop ranges, etc.) (see Guidelines 5.5.1 and
5.5.2). If you do not provide an explicit type when specifying
index constraints and other discrete ranges, a predefined
integer type is assumed.
The predefined numeric types are useful when you use them
wisely. You should not use them to avoid declaring numeric
types—then you lose the benefits of strong typing. When your
application deals with different kinds of quantities and
units, you should definitely separate them through the use of
distinct numeric types. However, if you are simply counting
the number of iterations in an iterative approximation
algorithm, declaring a special integer type is probably
overkill. The predefined exponentiation operators ** require
an integer as the type of its right operand.
You should use the predefined types Natural and Positive for
manipulating certain kinds of values in the predefined
language environment. The types String and Wide_String use an
index of type Positive. If your code indexes into a string
using an incompatible integer type, you will be forced to do
type conversion, reducing its readability. If you are
performing operations like slices and concatenation, the
subtype of your numeric array index is probably insignificant
and you are better off using a predefined subtype. On the
other hand, if your array represents a table (e.g., a hash
table), then your index subtype is significant, and you should
declare a distinct index type.
notes
There is an alternative that this guideline permits. As
Guideline 7.1.5 suggests, implementation dependencies can be
encapsulated in packages intended for that purpose. This could
include the definition of a 32-bit integer type. It would then
be possible to derive additional types from that 32-bit type.
7.2.2 Accuracy Model
guideline
• Use an implementation that supports the Numerics Annex (Ada
Reference Manual 1995, Annex G) when performance and
accuracy are overriding concerns.
rationale
The Numerics Annex defines the accuracy and performance
requirements for floating- and fixed-point arithmetic. The
Annex provides a “strict” mode in which the compiler must
support these requirements. To guarantee that your program’s
numerical performance is portable, you should compile and link
in the strict mode. If your program relies upon the numeric
properties of the strict mode, then it will only be portable
to other environments that support the strict numerics mode.
The accuracy of floating-point numbers is based on what
machine numbers can be represented exactly in storage. A
computational result in a register can fall between two
machine numbers when the register contains more bits than
storage. You can step through the machine numbers using the
attributes 'Pred and 'Succ. Other attributes return values of
the mantissa, exponent, radix, and other characteristics of
floating- and
fixed-point numbers.
7.2.3 Accuracy Analysis
guideline
• Carefully analyze what accuracy and precision you really
need.
rationale
Floating-point calculations are done with the equivalent of
the implementation’s predefined floating-point types. The
effect of extra “guard” digits in internal computations can
sometimes lower the number of digits that must be specified in
an Ada declaration. This may not be consistent over
implementations where the program is intended to be run. It
may also lead to the false conclusion that the declared types
are sufficient for the accuracy required.
You should choose the numeric type declarations to satisfy the
lowest precision (smallest number of digits) that will provide
the required accuracy. Careful analysis will be necessary to
show that the declarations are adequate. When you move to a
machine with less precision, you probably can use the same
type declaration.
7.2.4 Accuracy Constraints
guideline
• Do not press the accuracy limits of the machine(s).
rationale
Just because two different machines use the same number of
digits in the mantissa of a floating-point number does not
imply they will have the same arithmetic properties. Some Ada
implementations may give slightly better accuracy than
required by Ada because they make efficient use of the
machine. Do not write programs that depend on this.
7.2.5 Comments
guideline
• Comment the analysis and derivation of the numerical aspects
of a program.
rationale
Decisions and background about why certain precisions are
required in a program are important to program revision or
porting. The underlying numerical analysis leading to the
program should be commented.
7.2.6 Subexpression Evaluation
guideline
• Anticipate the range of values of subexpressions to avoid
exceeding the underlying range of their base type. Use
derived types, subtypes, factoring, and range constraints on
numeric types (see Guidelines 3.4.1, 5.3.1, and 5.5.3).
example
This example is adapted from the Rationale (1995, §3.3):
with Ada.Text_IO;
with Ada.Integer_Text_IO;
procedure Demo_Overflow is
-- assume the predefined type Integer has a 16-bit range
X : Integer := 24_000;
Y : Integer;
begin -- Demo_Overflow
y := (3 * X) / 4; -- raises Constraint_Error if the
machine registers used are 16-bit
-- mathematically correct intermediate result if 32-bit
registers
Ada.Text_IO.Put ("(");
Ada.Integer_Text_IO.Put (X);
Ada.Text_IO.Put (" * 3 ) / 4 = ");
Ada.Integer_Text_IO.Put (Y);
exception
when Constraint_Error =>
Ada.Text_IO.Put_Line ("3 * X too big for register!");
end Demo_Overflow;
rationale
The Ada language does not require that an implementation
perform range checks on subexpressions within an expression.
Ada does require that overflow checks be performed. Thus,
depending on the order of evaluation and the size of the
registers, a subexpression will either overflow or produce the
mathematically correct result. In the event of an overflow,
you will get the exception Constraint_Error. Even if the
implementation on your program’s current target does not
result in an overflow on a subexpression evaluation, your
program might be ported to an implementation that does.
7.2.7 Relational Tests
guideline
• Consider using <= and >= to do relational tests on real
valued arguments, avoiding the , =, and /= operations.
• Use values of type attributes in comparisons and checking
for small values.
example
The following examples test for (1) absolute “equality” in
storage, (2) absolute “equality” in computation, (3) relative
“equality” in storage, and (4) relative “equality” in
computation:
abs (X - Y) <= Float_Type'Model_Small -- (1)
abs (X - Y) <= Float_Type'Base'Model_Small -- (2)
abs (X - Y) <= abs X * Float_Type'Model_Epsilon -- (3)
abs (X - Y) <= abs X * Float_Type'Base'Model_Epsilon -- (4)
And, specifically, for “equality” to 0:
abs X <= Float_Type'Model_Small -- (1)
abs X <= Float_Type'Base'Model_Small -- (2)
abs X <= abs X * Float_Type'Model_Epsilon -- (3)
abs X <= abs X * Float_Type'Base'Model_Epsilon -- (4)
rationale
Strict relational comparisons ( , =, /= ) are a general
problem with computations involving real numbers. Because of
the way comparisons are defined in terms of model intervals,
it is possible for the values of the comparisons to depend on
the implementation. Within a model interval, the result of
comparing two values is nondeterministic if the values are not
model numbers. In general, you should test for proximity
rather than equality as shown in the examples. See also
Rationale (1995, §§G.4.1 and G.4.2.).
Type attributes are the primary means of symbolically
accessing the implementation of the Ada numeric model. When
the characteristics of the model numbers are accessed by type
attributes, the source code is portable. The appropriate model
numbers of any implementation will then be used by the
generated code.
Although 0 is technically not a special case, it is often
overlooked because it looks like the simplest and, therefore,
safest case. But in reality, each time comparisons involve
small values, you should evaluate the situation to determine
which technique is appropriate.
notes
Regardless of language, real-valued computations have
inaccuracy. That the corresponding mathematical operations
have algebraic properties usually introduces some confusion.
This guideline explains how Ada deals with the problem that
most languages face.
7.2.8 Decimal Types and the Information Systems Annex
guideline
• In information systems, declare different numeric decimal
types to correspond to different scales (Brosgol, Eachus,
and Emery 1994).
• Create objects of different decimal types to reflect
different units of measure (Brosgol, Eachus, and Emery
1994).
• Declare subtypes of the appropriately scaled decimal type to
provide appropriate range constraints for application-
specific types.
• Encapsulate each measure category in a package (Brosgol,
Eachus, and Emery 1994).
• Declare as few decimal types as possible for unitless data
(Brosgol, Eachus, and Emery 1994).
• For decimal calculations, determine whether the result
should be truncated toward 0 or rounded.
• Avoid decimal types and arithmetic on compilers that do not
support the Information Systems Annex (Ada Reference Manual
1995, Annex F) in full.
example
-- The salary cap today is $500,000; however this can be
expanded to $99,999,999.99.
type Executive_Salary is delta 0.01 digits 10 range 0 ..
500_000.00;
------------------------------------------------------------
------------------
package Currency is
type Dollars is delta 0.01 digits 12;
type Marks is delta 0.01 digits 12;
type Yen is delta 0.01 digits 12;
function To_Dollars (M : Marks) return Dollars;
function To_Dollars (Y : Yen) return Dollars;
function To_Marks (D : Dollars) return Marks;
function To_Marks (Y : Yen) return Marks;
function To_Yen (D : Dollars) return Yen;
function To_Yen (M : Marks) return Yen;
end Currency;
rationale
The Ada language does not provide any predefined decimal
types. Therefore, you need to declare decimal types for the
different scales you will need to use. Differences in scale
and precision must be considered in deciding whether or not a
common type will suffice (Brosgol, Eachus, and Emery 1994).
You need different types for objects measured in different
units. This allows the compiler to detect mismatched values in
expressions. If you declare all decimal objects to be of a
single type, you forego the benefits of strong typing. For
example, in an application that involves several currencies,
each currency should be declared as a separate type. You
should provide appropriate conversions between different
currencies.
You should map data with no particular unit of measure to a
small set of types or a single type to avoid the explosion of
conversions between numeric types.
Separate the range requirement on a decimal type from its
precision, i.e., the number of significant digits required.
From the point of view of planning for change and ease of
maintenance, you can use the digit’s value to accommodate
future growth in the values to be stored in objects of the
type. For example, you may want to anticipate growth for
database values and report formats. You can constrain the
values of the type through a range constraint that matches
current needs. It is easier to modify the range and avoid
redefining databases and reports.
Ada automatically truncates toward 0. If your requirements are
to round the decimal result, you must explicitly do so using
the 'Round attribute.
The core language defines the basic syntax of and operations
on decimal types. It does not specify, however, the minimum
number of significant digits that must be supported. Nor does
the core language require the compiler to support values of
Small other than powers of 2, thus enabling the compiler
effectively to reject a decimal declaration (Ada Reference
Manual 1995, §3.5.9). The Information Systems Annex provides
additional support for decimal types. It requires a minimum of
18 significant digits. It also specifies a Text_IO.Editing
package that provides support analogous to the COBOL picture
approach.
7.3 STORAGE CONTROL
The management of dynamic storage can vary between Ada
environments. In fact, some environments do not provide any
deallocation. The following Ada storage control mechanisms are
implementation-dependent and should be used with care in writing
portable programs.
7.3.1 Representation Clause
guideline
• Do not use a representation clause to specify number of
storage units.
rationale
The meaning of the 'Storage_Size attribute is ambiguous;
specifying a particular value will not improve portability. It
may or may not include space allocated for parameters, data,
etc. Save the use of this feature for designs that must depend
on a particular vendor’s implementation.
notes
During a porting activity, it can be assumed that any
occurrence of storage specification indicates an
implementation dependency that must be redesigned.
7.3.2 Access-to-Subprogram Values
guideline
• Do not compare access-to-subprogram values.
rationale
The Ada Reference Manual (1995, §3.10.2) explains that an
“implementation may consider two
access-to-subprogram values to be unequal, even though they
designate the same subprogram. This might be because one
points directly to the subprogram, while the other points to a
special prologue that performs an Elaboration_Check and then
jumps to the subprogram.” The Ada Reference Manual (1995,
'4.5.2) states that it is “unspecified whether two access
values that designate the same subprogram but are the result
of distinct evaluations of Access attribute references are
equal or unequal.”
See also Guideline 5.3.4.
exceptions
If you must compare an access-to-subprogram value, you should
define a constant using the
access-to-subprogram value and make all future comparisons
against the constant. However, if you attempt to compare
access-to-subprogram values with different levels of
indirection, the values might still be unequal, even if
designating the same subprogram.
7.3.3 Storage Pool Mechanisms
guideline
• Consider using explicitly defined storage pool mechanisms.
example
See the Rationale (1995, §13.4) for an example of the use of
storage pools.
rationale
There are several alternatives to consider when deciding what
storage management technique to use. You should choose as
simple a technique as possible that still satisfies your
application requirements.
You can use allocators and unchecked deallocation as is. Note
that the degree to which explicitly deallocated storage is
reclaimed might vary across implementations (Ada Reference
Manual 1995, §13.11.2).
You use allocators as before. Instead of using unchecked
deallocation, you maintain your own free lists of objects that
are no longer in use and available for reuse.
You use allocators and possibly unchecked deallocation;
however, you implement a storage pool and associate it with
the access type(s) via a Storage_Pool clause. You can use this
technique to implement a mark/release storage management
paradigm, which might be significantly faster than an
allocate/deallocate paradigm. Some vendors may provide a
mark/release package as part of their Ada environment.
You do not use allocators, but instead use unchecked
conversion from the address and do all your own default
initialization, etc. It is unlikely you would use this last
option because you lose automatic default initialization.
7.4 TASKING
The definition of tasking in the Ada language leaves many
characteristics of the tasking model up to the implementor. This
allows a vendor to make appropriate tradeoffs for the intended
application domain, but it also diminishes the portability of
designs and code employing the tasking features. In some
respects, this diminished portability is an inherent
characteristic of concurrency approaches (see Nissen and Wallis
1984, 37).
A discussion of Ada tasking dependencies when employed in a
distributed target environment is beyond the scope of this book.
For example, multiprocessor task scheduling, interprocessor
rendezvous, and the distributed sense of time through package
Calendar are all subject to differences between implementations.
For more information, Nissen and Wallis (1984) and ARTEWG (1986)
touch on these issues, and Volz et al. (1985) is one of many
research articles available.
If the Real-Time Systems Annex is supported, then many
concurrency aspects are fully defined and, therefore, a program
can rely on these features while still being portable to other
implementations that conform to the
Real-Time Systems Annex. The following sections provide
guidelines based on the absence of this annex.
7.4.1 Task Activation Order
guideline
• Do not depend on the order in which task objects are
activated when declared in the same declarative list.
rationale
The order in which task objects are activated is left
undefined in the Ada Reference Manual (1995, §9.2). See also
Guideline 6.1.5.
7.4.2 Delay Statements
guideline
• Do not depend on a particular delay being achievable (Nissen
and Wallis 1984).
• Never use knowledge of the execution pattern of tasks to
achieve timing requirements.
rationale
The rationale for this appears in Guideline 6.1.7. In
addition, the treatment of delay statements varies from
implementation to implementation, thereby hindering
portability.
Using knowledge of the execution pattern of tasks to achieve
timing requirements is nonportable. Ada does not specify the
underlying scheduling algorithm, and there is no guarantee
that system clock ticks will be consistently precise between
different systems. Thus, when you change system clocks, your
delay behavior also changes.
7.4.3 Package Calendar, Type Duration, and System.Tick
guideline
• Do not assume a correlation between System.Tick and type
Duration (see Guidelines 6.1.7 and 7.4.2).
rationale
Such a correlation is not required, although it may exist in
some implementations.
7.4.4 Select Statement Evaluation Order
guideline
• Do not depend on the order in which guard conditions are
evaluated or on the algorithm for choosing among several
open select alternatives.
rationale
The language does not define the order of these conditions, so
assume that they are arbitrary.
7.4.5 Task Scheduling Algorithm
guideline
• Do not assume that tasks execute uninterrupted until they
reach a synchronization point.
• Use pragma Priority to distinguish general levels of
importance only (see Guideline 6.1.6).
rationale
The Ada tasking model requires that tasks be synchronized only
through the explicit means provided in the language (i.e.,
rendezvous, task dependence, pragma Atomic). The scheduling
algorithm is not defined by the language and may vary from
time sliced to preemptive priority. Some implementations
provide several choices that a user may select for the
application.
notes
The number of priorities may vary between implementations. In
addition, the manner in which tasks of the same priority are
handled may vary between implementations even if the
implementations use the same general scheduling algorithm.
exceptions
In real-time systems, it is often necessary to tightly control
the tasking algorithm to obtain the required performance. For
example, avionics systems are frequently driven by cyclic
events with limited asynchronous interruptions. A
nonpreemptive tasking model is traditionally used to obtain
the greatest performance in these applications. Cyclic
executives can be programmed in Ada, as can a progression of
scheduling schemes from cyclic through multiple-frame-rate to
full asynchrony (MacLaren 1980), although an external clock is
usually required.
7.4.6 Abort
guideline
• Avoid using the abort statement.
rationale
The rationale for this appears in Guideline 6.3.3. In
addition, treatment of the abort statement varies from
implementation to implementation, thereby hindering
portability.
7.4.7 Unprotected Shared Variables and Pragmas Atomic and
Volatile
guideline
• Do not use unprotected shared variables.
• Consider using protected types to provide data
synchronization.
• Have tasks communicate through the rendezvous mechanism.
• Do not use unprotected shared variables as a task
synchronization device.
• Consider using protected objects to encapsulate shared data.
• Use pragma Atomic or Volatile only when you are forced to by
run-time system deficiencies.
example
See Guidelines 6.1.1 and 6.1.2.
rationale
The rationale for this appears in Guidelines 6.1.1 and 6.2.4.
In addition, the treatment of unprotected shared variables
varies from implementation to implementation, thereby
hindering portability.
7.5 EXCEPTIONS
You should exercise care when using predefined exceptions because
aspects of their treatment may vary between implementations.
Implementation-specific exceptions must, of course, be avoided.
See Guidelines 4.3 and 5.8 for further information on exceptions.
See Guideline 7.1.6 for further information on vendor-supplied
features.
7.5.1 Predefined and User-Defined Exceptions
guideline
• Do not depend on the exact locations at which predefined
exceptions are raised.
• Do not rely on the behavior of Ada.Exceptions beyond the
minimum defined in the language.
rationale
The Ada Reference Manual (1995, §11) states that, among
implementations, a predefined exception for the same cause may
be raised from different locations. You will not be able to
discriminate between the exceptions. Further, each of the
predefined exceptions is associated with a variety of
conditions. Any exception handler written for a predefined
exception must be prepared to deal with any of these
conditions.
Guideline 5.6.9 discusses the use of blocks to define local
exception handlers that can catch exceptions close to their
point of origin.
7.5.2 Implementation-Specific Exceptions
guideline
• Do not raise implementation-specific exceptions.
• Convert implementation-specific exceptions within interface
packages to visible user-defined exceptions.
rationale
No exception defined specifically by an implementation can be
guaranteed to be portable to other implementations whether or
not they are from the same vendor. Not only may the names be
different, but the range of conditions triggering the
exceptions may be different also.
If you create interface packages for the implementation-
specific portions of your program, those packages can catch or
recognize implementation-specific exceptions and convert them
into user-defined exceptions that have been declared in the
specification. Do not allow yourself to be forced to find and
change the name of every handler you have written for these
exceptions when the program is ported.
7.6 REPRESENTATION CLAUSES AND IMPLEMENTATION-DEPENDENT FEATURES
Ada provides many implementation-dependent features that permit
greater control over and interaction with the underlying hardware
architecture than is normally provided by a high-order language.
These mechanisms are intended to assist in systems programming
and real-time programming to obtain greater efficiency (e.g.,
specific size and layout of variables through representation
clauses) and direct hardware interaction (e.g., interrupt
entries) without having to resort to assembly level programming.
Given the objectives for these features, it is not surprising
that you must usually pay a significant price in portability to
use them. In general, where portability is the main objective, do
not use these features. When you must use these features,
encapsulate them in packages that are well-commented as
interfacing to the particular target environment. This section
identifies the various features and their recommended use with
respect to portability.
7.6.1 Representation Clauses
guideline
• Use algorithms that do not depend on the representation of
the data and, therefore, do not need representation clauses.
• Consider using representation clauses when accessing or
defining interface data or when a specific representation is
needed to implement a design.
• Do not assume that sharing source files between programs
guarantees the same representation of data types in those
files.
rationale
In many cases, it is easy to use representation clauses to
implement an algorithm, even when it is not necessary. There
is also a tendency to document the original programmer’s
assumptions about the representation for future reference. But
there is no guarantee that another implementation will support
the representation chosen. Unnecessary representation clauses
also confuse porting or maintenance efforts, which must assume
that the programmer depends on the documented representation.
Interfaces to external systems and devices are the most
common situations where a representation clause is needed.
Uses of pragma Import and address clauses should be evaluated
during design and porting to determine whether a
representation clause is needed.
Without representation clauses, the language does not require
two compilations of an unchanged file to result in the same
data representation. Things that can change the representation
between compilations include:
- A change in a file earlier in the compilation order
- A change in the optimization strategy or level
- A change in versions of the compiler
- A change in actual compilers
- A change in the availability of system resources
Therefore, two independently linked programs or partitions
should only share data that has their representations
explicitly controlled.
notes
During a porting effort, all representation clauses can be
evaluated as either design artifacts or specifications for
accessing interface data that might change with a new
implementation.
7.6.2 Package System
guideline
• Avoid using package System constants except in attempting to
generalize other machine-dependent constructs.
rationale
Because the values in this package are implementation-
provided, unexpected effects can result from their use.
notes
If you must guarantee that physical record layouts will remain
the same between implementations, you can express record
fields by their first and last bit positions as shown in the
Ada Reference Manual (1995, §13.5.1). Static expressions and
named numbers should be used to let the compiler compute the
endpoints of each range in terms of earlier fields. In this
case, greater portability can be achieved by using
System.Storage_Unit to let the compiler compute the value of
the named number. However, this method might not work for all
values of System.Storage_Unit.
exceptions
Do use package System constants to parameterize other
implementation-dependent features (see Pappas (1985, §13.7.1).
7.6.3 Machine Code Inserts
guideline
• Avoid machine code inserts.
rationale
The Ada Reference Manual (1995, Annex C) suggests that the
package that implements machine code inserts is optional.
Additionally, it is not standardized so that machine code
inserts are most likely not portable. In fact, it is possible
that two different vendors’ syntax will differ for an
identical target, and differences in lower-level details, such
as register conventions, will hinder portability.
exceptions
If machine code inserts must be used to meet another project
requirement, recognize and document the portability decreasing
effects.
In the declarative region of the body of the routine where
machine code inserts are being used, insert comments
explaining what functions inserts provide and (especially) why
the inserts are necessary. Comment the necessity of using
machine code inserts by delineating what went wrong with
attempts to use other higher level constructs.
7.6.4 Interfacing to Foreign Languages
guideline
• Use the package Interfaces and its language-defined child
packages rather than implementation-specific mechanisms.
• Consider using pragma Import rather than access-to-
subprogram types for interfacing to subprograms in other
languages.
• Isolate all subprograms employing pragmas Import, Export,
and Convention to implementation-specific (interface)
package bodies.
example
This example shows how to interface with the following cube
root function written in C:
double cbrt (double x);
------------------------------------------------------------
------------------
package Math_Utilities is
Argument_Error : exception;
function Cube_Root (X : Float) return Float;
...
end Math_Utilities;
------------------------------------------------------------
------------------
with Interfaces.C;
package body Math_Utilities is
function Cube_Root (X : Float) return Float is
function C_Cbrt (X : Interfaces.C.Double) return
Interfaces.C.Double;
pragma Import (Convention => C,
Entity => C_Cbrt,
External_Name => "cbrt");
begin
if X < 0.0 then
raise Argument_Error;
else
return Float (C_Cbrt (Interfaces.C.Double (X)));
end if;
end Cube_Root;
...
end Math_Utilities;
rationale
For static interfacing to subprograms in other languages, the
pragma Import provides a better solution than access to
subprograms because no indirection is required. The pragma
Interface (Ada Reference Manual 1983) has been replaced by
pragmas Import, Export, and Convention. Annex B of the
Rationale (1995) discusses how to use these pragmas in
conjunction with the access-to-subprogram types in interfacing
to other languages.
Access to subprogram types is useful for implementing
callbacks in a separate subsystem, such as the X Window
system.
The problems with interfacing to foreign languages are
complex. These problems include pragma syntax differences,
conventions for linking/binding Ada to other languages, and
mapping Ada variables to foreign language variables. By hiding
these dependencies within interface packages, the amount of
code modification can be reduced.
exceptions
It is often necessary to interact with other languages, if
only an assembly language, to reach certain hardware features.
In these cases, clearly comment the requirements and
limitations of the interface and pragma Import, Export, and
Conventions usage.
7.6.5 Implementation-Specific Pragmas and Attributes
guideline
• Avoid pragmas and attributes added by the compiler
implementor.
rationale
The Ada Reference Manual (1995) permits an implementor to add
pragmas and attributes to exploit a particular hardware
architecture or software environment. These are obviously even
more implementation-specific and therefore less portable than
an implementor’s interpretations of the predefined pragmas and
attributes. However, the Ada Reference Manual (1995) defines a
set of annexes that have a uniform and consistent approach to
certain specialized needs, namely, real-time systems,
distributed systems, information systems, numerics,
interfacing to foreign languages, and safety and security. You
should always prefer the facilities defined in the annexes to
any vendor-defined pragmas and attributes.
7.6.6 Unchecked Deallocation
guideline
• Avoid dependence on Ada.Unchecked_Deallocation (see
Guideline 5.9.2).
rationale
The unchecked storage deallocation mechanism is one method for
overriding the default time at which allocated storage is
reclaimed. The earliest default time is when an object is no
longer accessible, for example, when control leaves the scope
where an access type was declared (the exact point after this
time is implementation-dependent). Any unchecked deallocation
of storage performed prior to this may result in an erroneous
Ada program if an attempt is made to access the object.
This guideline is stronger than Guideline 5.9.2 because of the
extreme dependence on the implementation of
Ada.Unchecked_Deallocation. Using it could cause considerable
difficulty with portability.
notes
Ada.Unchecked_Deallocation is a supported feature in all Ada
implementations. The portability issue arises in that
unchecked storage deallocations might cause varying results in
different implementations.
exceptions
Using unchecked deallocation of storage can be beneficial in
local control of highly iterative or recursive algorithms
where available storage may be exceeded.
7.6.7 Unchecked Access
guideline
• Avoid dependence on the attribute Unchecked_Access (see
Guideline 5.9.2).
rationale
Access values are subject to accessibility restrictions. Using
the attribute Unchecked_Access prevents these rules from being
checked, and the programmer runs the risk of having dangling
references.
7.6.8 Unchecked Conversion
guideline
• Avoid dependence on Ada.Unchecked_Conversion (see Guideline
5.9.1).
rationale
The unchecked type conversion mechanism is, in effect, a means
of bypassing the strong typing facilities in Ada. An
implementation is free to limit the types that may be matched
and the results that occur when object sizes differ.
exceptions
Unchecked type conversion is useful in implementation-
dependent parts of Ada programs where lack of portability is
isolated and where low-level programming and foreign language
interfacing are the objectives.
If an enumeration representation clause is used, unchecked
type conversion is the only language-provided way to retrieve
the internal integer code of an enumeration value.
7.6.9 Run-Time Dependencies
guideline
• Avoid the direct invocation of or implicit dependence upon
an underlying host operating system or Ada run-time support
system, except where the interface is explicitly defined in
the language (e.g., Annex C or D of the Ada Reference Manual
[1995]).
• Use standard bindings and the package Ada.Command_Line when
you need to invoke the underlying
run-time support system.
• Use features defined in the Annexes rather than vendor-
defined features.
rationale
Features of an implementation not specified in the Ada
Reference Manual (1995) will usually differ between
implementations. Specific implementation-dependent features
are not likely to be provided in other implementations. In
addition to the mandatory predefined language environment, the
annexes define various packages, attributes, and pragmas to
standardize implementation-dependent features for several
specialized domains. You enhance portability when you use the
features declared in the packages in the Annexes because you
can port your program to other vendor environments that
implement the same Annexes you have used. Even if a majority
of vendors eventually provide similar features, they are
unlikely to have identical formulations. Indeed, different
vendors may use the same formulation for (semantically)
entirely different features.
When coding, try to avoid depending on the underlying
operating system. Consider the consequences of including
system calls in a program on a host development system. If
these calls are not flagged for removal and replacement, the
program could go through development and testing only to be
unusable when moved to a target environment that lacks the
facilities provided by those system calls on the host.
Guideline 7.1.5 discusses the use of the package
Ada.Command_Line. If an Ada environment implements a standard
binding to operating system services, such as POSIX/Ada, and
you write POSIX-compliant calls, your program should be
portable across more systems.
exceptions
In real-time, embedded systems, making calls to low-level
support system facilities may often be unavoidable. Isolating
the uses of these facilities may be too difficult. Comment
them as you would machine code inserts (see Guideline 7.6.3);
they are, in a sense, instructions for the virtual machine
provided by the support system. When isolating the uses of
these features, provide an interface for the rest of your
program to use, which can be ported through replacement of the
interface’s implementation.
7.7 INPUT/OUTPUT
I/O facilities in Ada are not a part of the syntactic definition
of the language. The constructs in the language have been used to
define a set of packages for this purpose. These packages are not
expected to meet all the I/O needs of all applications, in
particular, embedded systems. They serve as a core subset that
may be used on straightforward data and that can be used as
examples of building I/O facilities upon the low-level constructs
provided by the language. Providing an I/O definition that could
meet the requirements of all applications and integrate with the
many existing operating systems would result in unacceptable
implementation dependencies.
The types of portability problems encountered with I/O tend to be
different for applications running with a host operating system
versus embedded targets where the Ada run-time is self-
sufficient. Interacting with a host operating system offers the
added complexity of coexisting with the host file system
structures (e.g., hierarchical directories), access methods
(e.g., indexed sequential access method [ISAM]), and naming
conventions (e.g., logical names and aliases based on the current
directory). The section on Input/Output in ARTEWG (1986) provides
some examples of this kind of dependency. Embedded applications
have different dependencies that often tie them to the low-level
details of their hardware devices.
The major defense against these inherent implementation
dependencies in I/O is to try to isolate their functionality in
any given application. The majority of the following guidelines
are focused in this direction.
7.7.1 Name and Form Parameters
guideline
• Use constants and variables as symbolic actuals for the Name
and Form parameters on the predefined I/O packages. Declare
and initialize them in an implementation dependency package.
rationale
The format and allowable values of these parameters on the
predefined I/O packages can vary greatly between
implementations. Isolation of these values facilitates
portability. Not specifying a Form string or using a null
value does not guarantee portability because the
implementation is free to specify defaults.
notes
It may be desirable to further abstract the I/O facilities by
defining additional Create and Open procedures that hide the
visibility of the Form parameter entirely (see Pappas 1985, 54-
55).
7.7.2 File Closing
guideline
• Close all files explicitly.
rationale
The Ada Reference Manual (1995, §A.7) does not define what
happens to external files after completion of the main
subprogram (in particular, if corresponding files have not
been closed).
The disposition of a closed temporary file may vary, perhaps
affecting performance and space availability (ARTEWG 1986).
7.7.3 Input/Output on Access Types
guideline
• Avoid performing I/O on access types.
rationale
The Ada Reference Manual (1995, §A.7) does not specify the
effects of I/O on access types. When such a value is written,
it is placed out of reach of the implementation. Thus, it is
out of reach of the
reliability-enhancing controls of strong type checking.
Consider the meaning of this operation. One possible
implementation of the values of access types is virtual
addresses. If you write such a value, how can you expect
another program to read that value and make any sensible use
of it? The value cannot be construed to refer to any
meaningful location within the reader’s address space, nor can
a reader infer any information about the writer’s address
space from the value read. The latter is the same problem that
the writer would have trying to interpret or use the value if
it is read back in. To wit, a garbage collection and/or heap
compaction scheme may have moved the item formerly accessed by
that value, leaving that value “pointing” at space that is now
being put to indeterminable uses by the underlying
implementation.
7.7.4 Package Ada.Streams.Stream_IO
guideline
• Consider using Sequential_IO or Direct_IO instead of
Stream_IO unless you need the low-level, heterogeneous I/O
features provided by Stream_IO.
rationale
Sequential_IO and Direct_IO are still well suited for
processing homogeneous files. Additionally, in cases where the
intent is to process homogeneous files, the use of
Sequential_IO or Direct_IO has the advantage of enforcing this
intent at compile time.
Stream_IO should be reserved for processing heterogeneous
files. In this case, a file is not a sequence of objects of
all the same type but rather a sequence of objects of varying
types. To read a heterogeneous sequence of objects in the
correct order requires some application-specific knowledge.
7.7.5 Current Error Files
guideline
• Consider using Current_Error and Set_Error for run-time
error messages.
example
with Ada.Text_IO;
...
begin
Ada.Text_IO.Open (File => Configuration_File,
Mode => Ada.Text_IO.In_File,
Name => Configuration_File_Name);
exception
when Ada.Text_IO.Name_Error =>
Ada.Text_IO.Put_Line (File =>
Ada.Text_IO.Standard_Error,
Item => "Can't open
configuration file.");
...
end;
rationale
The package Text_IO includes the concept of a current error
file. You should report errors to the user through the
associated subprograms Current_Error and Set_Error instead of
the standard output facilities. In interactive applications,
using the Text_IO error facilities increases the portability
of your user interface.
notes
In a program with multiple tasks for I/O, you need to be
careful of two or more tasks trying to set Current_Input,
Current_Output, or Current_Error. The potential problem lies
in unprotected updates to the “shared” state associated with a
package, in this case, the package Text_IO. Guidelines 6.1.1
and 6.2.4 discuss the related issues of unprotected shared
variables.
7.8 SUMMARY
fundamentals
• In programs or components intended to have a long life,
avoid using the features of Ada declared as “obsolescent” by
Annex J of the Ada Reference Manual (1995), unless the use
of the feature is needed for backward compatibility with Ada
83 (Ada Reference Manual 1983).
• Document the use of any obsolescent features.
• Avoid using the following features:
- The short renamings of the packages in the predefined
environment (e.g., Text_IO as opposed to Ada.Text_IO)
- The character replacements of ! for |, : for #, and % for
quotation marks
- Reduced accuracy subtypes of floating-point types
- The 'Constrained attribute as applied to private types
- The predefined package ASCII
- The exception Numeric_Error
- Various representation specifications, including at
clauses, mod clauses, interrupt entries, and the
Storage_Size attribute
• Make informed assumptions about the support provided for the
following on potential target platforms:
- Number of bits available for type Integer (range
constraints)
- Number of decimal digits of precision available for
floating-point types
- Number of bits available for fixed-point types (delta and
range constraints)
- Number of characters per line of source text
- Number of bits for Root_Integer expressions
- Number of seconds for the range of Duration
- Number of milliseconds for Duration'Small
- Minimum and maximum scale for decimal types
• Avoid assumptions about the values and the number of values
included in the type Character.
• Use highlighting comments for each package, subprogram, and
task where any nonportable features are present.
• For each nonportable feature employed, describe the
expectations for that feature.
• Consider using only a parameterless procedure as the main
subprogram.
• Consider using Ada.Command_Line for accessing values from
the environment, but recognize that this package’s behavior
and even its specification are nonportable.
• Encapsulate and document all uses of package
Ada.Command_Line.
• Create packages specifically designed to isolate hardware
and implementation dependencies and designed so that their
specification will not change when porting.
• Clearly indicate the objectives if machine or solution
efficiency is the reason for hardware or implementation-
dependent code.
• For the packages that hide implementation dependencies,
maintain different package bodies for different target
environments.
• Isolate interrupt receiving tasks into implementation-
dependent packages.
• Refer to Annex M of the Ada Reference Manual (1995) for a
list of implementation-dependent features.
• Avoid the use of vendor-supplied packages.
• Avoid the use of features added to the predefined packages
that are not specified in the Ada language definition or
Specialized Needs Annexes.
• Use features defined in the Specialized Needs Annexes rather
than vendor-defined features.
• Document clearly the use of any features from the
Specialized Needs Annexes (systems programming, real-time
systems, distributed systems, information systems, numerics,
and safety and security).
• Do not write code whose correct execution depends on the
particular parameter passing mechanism used by an
implementation (Ada Reference Manual 1995, §6.2; Cohen
1986).
• If a subprogram has more than one formal parameter of a
given subtype, at least one of which is [in] out, make sure
that the subprogram can properly handle the case when both
formal parameters denote the same actual object.
• Avoid depending on the order in which certain constructs in
Ada are evaluated.
numeric types and expressions
• Avoid using the predefined numeric types in package
Standard. Use range and digits declarations and let the
implementation pick the appropriate representation.
• For programs that require greater accuracy than that
provided by the global assumptions, define a package that
declares a private type and operations as needed; see Pappas
(1985) for a full explanation and examples.
• Consider using predefined numeric types (Integer, Natural,
Positive) for:
- Indexes into arrays where the index type is not
significant, such as type String
- “Pure” numbers, that is, numbers with no associated
physical unit (e.g., exponents)
- Values whose purpose is to control a repeat or iteration
count
• Use an implementation that supports the Numerics Annex (Ada
Reference Manual 1995, Annex G) when performance and
accuracy are overriding concerns.
• Carefully analyze what accuracy and precision you really
need.
• Do not press the accuracy limits of the machine(s).
• Comment the analysis and derivation of the numerical aspects
of a program.
• Anticipate the range of values of subexpressions to avoid
exceeding the underlying range of their base type. Use
derived types, subtypes, factoring, and range constraints on
numeric types.
• Consider using <= and >= to do relational tests on real
valued arguments, avoiding the , =, and /= operations.
• Use values of type attributes in comparisons and checking
for small values.
• In information systems, declare different numeric decimal
types to correspond to different scales (Brosgol, Eachus,
and Emery 1994).
• Create objects of different decimal types to reflect
different units of measure (Brosgol, Eachus, and Emery
1994).
• Declare subtypes of the appropriately scaled decimal type to
provide appropriate range constraints for application-
specific types.
• Encapsulate each measure category in a package (Brosgol,
Eachus, and Emery 1994).
• Declare as few decimal types as possible for unitless data
(Brosgol, Eachus, and Emery 1994).
• For decimal calculations, determine whether the result
should be truncated toward 0 or rounded.
• Avoid decimal types and arithmetic on compilers that do not
support the Information Systems Annex (Ada Reference Manual
1995, Annex F) in full.
storage control
• Do not use a representation clause to specify number of
storage units.
• Do not compare access-to-subprogram values.
• Consider using explicitly defined storage pool mechanisms.
tasking
• Do not depend on the order in which task objects are
activated when declared in the same declarative list.
• Do not depend on a particular delay being achievable (Nissen
and Wallis 1984).
• Never use knowledge of the execution pattern of tasks to
achieve timing requirements.
• Do not assume a correlation between System.Tick and type
Duration.
• Do not depend on the order in which guard conditions are
evaluated or on the algorithm for choosing among several
open select alternatives.
• Do not assume that tasks execute uninterrupted until they
reach a synchronization point.
• Use pragma Priority to distinguish general levels of
importance only.
• Avoid using the abort statement.
• Do not use unprotected shared variables.
• Consider using protected types to provide data
synchronization.
• Have tasks communicate through the rendezvous mechanism.
• Do not use unprotected shared variables as a task
synchronization device.
• Consider using protected objects to encapsulate shared data.
• Use pragma Atomic or Volatile only when you are forced to by
run-time system deficiencies.
exceptions
• Do not depend on the exact locations at which predefined
exceptions are raised.
• Do not rely on the behavior of Ada.Exceptions beyond the
minimum defined in the language.
• Do not raise implementation-specific exceptions.
• Convert implementation-specific exceptions within interface
packages to visible user-defined exceptions.
representation clauses and implementation-dependent features
• Use algorithms that do not depend on the representation of
the data and, therefore, do not need representation clauses.
• Consider using representation clauses when accessing or
defining interface data or when a specific representation is
needed to implement a design.
• Do not assume that sharing source files between programs
guarantees the same representation of data types in those
files.
• Avoid using package System constants except in attempting to
generalize other machine-dependent constructs.
• Avoid machine code inserts.
• Use the package Interfaces and its language-defined child
packages rather than implementation-specific mechanisms.
• Consider using pragma Import rather than access-to-
subprogram types for interfacing to subprograms in other
languages.
• Isolate all subprograms employing pragmas Import, Export,
and Convention to implementation-specific (interface)
package bodies.
• Avoid pragmas and attributes added by the compiler
implementor.
• Avoid dependence on Ada.Unchecked_Deallocation.
• Avoid dependence on the attribute Unchecked_Access.
• Avoid dependence on Ada.Unchecked_Conversion.
• Avoid the direct invocation of or implicit dependence upon
an underlying host operating system or Ada run-time support
system, except where the interface is explicitly defined in
the language (e.g., Annex C or D of the Ada Reference Manual
[1995]).
• Use standard bindings and the package Ada.Command_Line when
you need to invoke the underlying
run-time support system.
• Use features defined in the Annexes rather than vendor-
defined features.
input/output
• Use constants and variables as symbolic actuals for the Name
and Form parameters on the predefined I/O packages. Declare
and initialize them in an implementation dependency package.
• Close all files explicitly.
• Avoid performing I/O on access types.
• Consider using Sequential_IO or Direct_IO instead of
Stream_IO unless you need the low-level, heterogeneous I/O
features provided by Stream_IO.
• Consider using Current_Error and Set_Error for run-time
error messages.