Ada Style Guide/Portability
Introduction
[edit | edit source]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.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.
Fundamentals
[edit | edit source]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.
Obsolescent Features
[edit | edit source]guideline
[edit | edit source]- 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
[edit | edit source]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
[edit | edit source]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).
Global Assumptions
[edit | edit source]guideline
[edit | edit source]- 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
[edit | edit source]- 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 [Annotated])
- 20 milliseconds for Duration'Small (as specified in Ada Reference Manual 1995, §9.6 [Annotated])
rationale
[edit | edit source]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
[edit | edit source]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 [Annotated]. 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.
Comments
[edit | edit source]guideline
[edit | edit source]- 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
[edit | edit source]------------------------------------------------------------------------
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
[edit | edit source]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.
Main Subprogram
[edit | edit source]guideline
[edit | edit source]- 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
[edit | edit source]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
[edit | edit source]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
[edit | edit source]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.
Encapsulating Implementation Dependencies
[edit | edit source]guideline
[edit | edit source]- 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
[edit | edit source]See Guideline 7.1.3.
rationale
[edit | edit source]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
[edit | edit source]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.
Implementation-Added Features
[edit | edit source]guideline
[edit | edit source]- 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
[edit | edit source]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
[edit | edit source]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.
Specialized Needs Annexes
[edit | edit source]guideline
[edit | edit source]- 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
[edit | edit source]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.
Dependence on Parameter Passing Mechanism
[edit | edit source]guideline
[edit | edit source]- Do not write code whose correct execution depends on the particular parameter passing mechanism used by an implementation (Ada Reference Manual 1995, §6.2 [Annotated]; 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
[edit | edit source]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
[edit | edit source]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 [Annotated]).
See also Guideline 8.2.7.
exceptions
[edit | edit source]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.
Arbitrary Order Dependencies
[edit | edit source]guideline
[edit | edit source]- Avoid depending on the order in which certain constructs in Ada are evaluated.
example
[edit | edit source]The output of this program depends upon the order of evaluation of subprogram parameters, but the Ada Reference Manual 1995, §6.4 [Annotated] 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
[edit | edit source]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 [Annotated]). Avoid arbitrary order dependencies, but also recognize that even an unintentional error of this kind could prohibit portability.
Numeric Types and Expressions
[edit | edit source]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.
Predefined Numeric Types
[edit | edit source]guideline
[edit | edit source]- 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
[edit | edit source]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
[edit | edit source]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
[edit | edit source]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.
Accuracy Model
[edit | edit source]guideline
[edit | edit source]- Use an implementation that supports the Numerics Annex (Ada Reference Manual 1995, Annex G) when performance and accuracy are overriding concerns.
rationale
[edit | edit source]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.
Accuracy Analysis
[edit | edit source]guideline
[edit | edit source]- Carefully analyze what accuracy and precision you really need.
rationale
[edit | edit source]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.
Accuracy Constraints
[edit | edit source]guideline
[edit | edit source]- Do not press the accuracy limits of the machine(s).
rationale
[edit | edit source]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.
Comments
[edit | edit source]guideline
[edit | edit source]- Comment the analysis and derivation of the numerical aspects of a program.
rationale
[edit | edit source]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.
Subexpression Evaluation
[edit | edit source]guideline
[edit | edit source]- 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
[edit | edit source]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
[edit | edit source]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.
Relational Tests
[edit | edit source]guideline
[edit | edit source]- 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
[edit | edit source]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
[edit | edit source]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
[edit | edit source]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.
Decimal Types and the Information Systems Annex
[edit | edit source]guideline
[edit | edit source]- 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
[edit | edit source]-- 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
[edit | edit source]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 [Annotated]). 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.
Storage Control
[edit | edit source]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.
Representation Clause
[edit | edit source]guideline
[edit | edit source]- Do not use a representation clause to specify number of storage units.
rationale
[edit | edit source]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
[edit | edit source]During a porting activity, it can be assumed that any occurrence of storage specification indicates an implementation dependency that must be redesigned.
Access-to-Subprogram Values
[edit | edit source]guideline
[edit | edit source]- Do not compare access-to-subprogram values.
rationale
[edit | edit source]The Ada Reference Manual 1995, §3.10.2 [Annotated] 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 [Annotated] 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
[edit | edit source]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.
Storage Pool Mechanisms
[edit | edit source]guideline
[edit | edit source]- Consider using explicitly defined storage pool mechanisms.
example
[edit | edit source]See the Ada Reference Manual 1995, §13.11.2 [Annotated].
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.
Tasking
[edit | edit source]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.
Task Activation Order
[edit | edit source]guideline
[edit | edit source]- Do not depend on the order in which task objects are activated when declared in the same declarative list.
rationale
[edit | edit source]The order in which task objects are activated is left undefined in the Ada Reference Manual 1995, §9.2 [Annotated]. See also Guideline 6.1.5.
Delay Statements
[edit | edit source]guideline
[edit | edit source]- 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
[edit | edit source]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.
Package Calendar, Type Duration, and System.Tick
[edit | edit source]guideline
[edit | edit source]- Do not assume a correlation between System.Tick and type Duration (see Guidelines 6.1.7 and 7.4.2).
rationale
[edit | edit source]Such a correlation is not required, although it may exist in some implementations.
Select Statement Evaluation Order
[edit | edit source]guideline
[edit | edit source]- Do not depend on the order in which guard conditions are evaluated or on the algorithm for choosing among several open select alternatives.
rationale
[edit | edit source]The language does not define the order of these conditions, so assume that they are arbitrary.
Task Scheduling Algorithm
[edit | edit source]guideline
[edit | edit source]- 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
[edit | edit source]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
[edit | edit source]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
[edit | edit source]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.
Abort
[edit | edit source]guideline
[edit | edit source]- Avoid using the abort statement.
rationale
[edit | edit source]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.
Unprotected Shared Variables and Pragmas Atomic and Volatile
[edit | edit source]guideline
[edit | edit source]- 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
[edit | edit source]See Guidelines 6.1.1 and 6.1.2.
rationale
[edit | edit source]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.
Exceptions
[edit | edit source]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 an 5.8 for further information on exceptions. See Guideline 7.1.6 for further information on vendor-supplied features.
Predefined and User-Defined Exceptions
[edit | edit source]guideline
[edit | edit source]- 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
[edit | edit source]The Ada Reference Manual 1995, §Section 11 [Annotated]) 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.
Implementation-Specific Exceptions
[edit | edit source]guideline
[edit | edit source]- Do not raise implementation-specific exceptions.
- Convert implementation-specific exceptions within interface packages to visible user-defined exceptions.
rationale
[edit | edit source]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.
Representation Clauses And Implementation-Dependent Features
[edit | edit source]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.
Representation Clauses
[edit | edit source]guideline
[edit | edit source]- 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
[edit | edit source]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
[edit | edit source]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.
Package System
[edit | edit source]guideline
[edit | edit source]- Avoid using package System constants except in attempting to generalize other machine-dependent constructs.
rationale
[edit | edit source]Because the values in this package are implementation-provided, unexpected effects can result from their use.
notes
[edit | edit source]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 [Annotated]. 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
[edit | edit source]Do use package System constants to parameterize other implementation-dependent features (see Pappas (1985, §13.7.1).
Machine Code Inserts
[edit | edit source]guideline
[edit | edit source]- Avoid machine code inserts.
rationale
[edit | edit source]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
[edit | edit source]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.
Interfacing to Foreign Languages
[edit | edit source]guideline
[edit | edit source]- 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. (Preferably using the "External_Name =>" argument.)
- Isolate all subprograms employing pragmas Import, Export, and Convention to implementation-specific (interface) package bodies.
example
[edit | edit source]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
[edit | edit source]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.
Note especially the distinction between the "External_Name =>" and "Link_Name =>" parameters to pragma Import which are frequently confused. External_Name specifies the procedure name as it appears in the source code of the other language (such as C or Fortran). Link_Name specifies the name used by the linker. Typically, only one of these parameters is specified, and generally External_Name is the preferred choice for portability.
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
[edit | edit source]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.
Implementation-Specific Pragmas and Attributes
[edit | edit source]guideline
[edit | edit source]- Avoid pragmas and attributes added by the compiler implementor.
rationale
[edit | edit source]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.
Unchecked Deallocation
[edit | edit source]guideline
[edit | edit source]- Avoid dependence on Ada.Unchecked_Deallocation (see Guideline 5.9.2).
rationale
[edit | edit source]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
[edit | edit source]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
[edit | edit source]Using unchecked deallocation of storage can be beneficial in local control of highly iterative or recursive algorithms where available storage may be exceeded.
Unchecked Access
[edit | edit source]guideline
[edit | edit source]- Avoid dependence on the attribute Unchecked_Access (see Guideline 5.9.2).
rationale
[edit | edit source]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.
Unchecked Conversion
[edit | edit source]guideline
[edit | edit source]- Avoid dependence on Ada.Unchecked_Conversion (see Guideline 5.9.1).
rationale
[edit | edit source]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
[edit | edit source]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.
Run-Time Dependencies
[edit | edit source]guideline
[edit | edit source]- 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
[edit | edit source]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
[edit | edit source]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.
Input/Output
[edit | edit source]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.
Name and Form Parameters
[edit | edit source]guideline
[edit | edit source]- 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
[edit | edit source]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
[edit | edit source]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).
File Closing
[edit | edit source]guideline
[edit | edit source]- Close all files explicitly.
rationale
[edit | edit source]The Ada Reference Manual 1995, §A.7 [Annotated] 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).
Input/Output on Access Types
[edit | edit source]guideline
[edit | edit source]- Avoid performing I/O on access types.
rationale
[edit | edit source]The Ada Reference Manual 1995, §A.7 [Annotated] 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.
Package Ada.Streams.Stream_IO
[edit | edit source]guideline
[edit | edit source]- 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
[edit | edit source]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.
Current Error Files
[edit | edit source]guideline
[edit | edit source]- Consider using Current_Error and Set_Error for run-time error messages.
example
[edit | edit source]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
[edit | edit source]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
[edit | edit source]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.
Summary
[edit | edit source]fundamentals
[edit | edit source]- 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 [Annotated]; 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
[edit | edit source]- 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
[edit | edit source]- 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
[edit | edit source]- 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
[edit | edit source]- 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
[edit | edit source]- 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
[edit | edit source]- 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.