An Overview of Ada 95

Solving the Implementation Problems of the New Millennium

The capabilities of Ada 83 are being enhanced through the definition of a small number of new “building blocks” in three basic areas, object-oriented programming, programming in the large and real-time and parallel programming. In each case, the revision team has used existing features as the basic for the enhanced capabilities: Derived and universal types provide the basis for object-oriented programming; the existing library unit concept forms the basis for the hierarchical name space and program partitioning; and the concepts of private types, functions, procedures, and entries form the basis for the protected record construct, supporting fast mutual exclusion and asynchronous task communication. By using this building process, the team believes that the revision represents a natural evolution of the existing excellent Ada 83 capabilities, while ensuring that Ada can be used efficiently and productively to solve the complex systems implementation problems of the ’90s.

Object-Oriented Programming

Ada 83 has been called an “object-based” language, but it does not qualify as a true “object- oriented” language because it lacks full support for inheritance and run-time polymorphism. For Ada 95, the existing “object-based” features of Ada are generalized to allow them to fully support object-oriented programming.

Ada 83 has a rich abstract type definition capability. Furthermore, it allows one type to be defined as a “derivative” of another. However, it does not allow additional components to be added to the type as part of this derivation. For Ada 95, the type derivation capability is generalized by allowing private and record types to be “extended” with additional components. For example, in Ada 83 a new type may be defined in terms of an existing type as follows:

      
       type Fancy_Window is new Basic_Window; 

This new type (Fancy_Window) inherits the same set of components as Basic_Window, and the same set of “primitive” operations. In addition, new operations may be defined, and inherited operations may be overridden. For Ada 95, we allow new components to be defined as part of a type derivation, as follows:

 
       type Fancy_Window is new Basic_Window 
         with record 
           Border_Width : Pixel_count := 1; 
           Border_Color : Color := Black; 
         end record; 

Now, the new type Fancy_Window has all of the components of a Basic_Window, plus two additional components to allow more control over the appearance of the border. If the names and types of these new components are to be kept “private” to the package in which the derived type is defined, a private extension part may be specified:

      
       type Fancy_Window is new Basic_Window with private; 

In the “private” part of the package, the actual components of the extension part must be specified.

Because of Ada 83′s pre-existing support for derived types, this modest generalization is adequate to provide full support for what is called “single inheritance.” However, to provide run-time polymorphism, additional language enhancements are required. The existing Ada 83 concept of “universal” types and type “classes” were used as the basis for our proposals for run-time polymorphism.

In Ada 83, integer literals are defined to be a “universal” integer type, which is implicitly convertible to any integer type. Similarly, real literals are defined to be of a universal real type, convertible to any real type. The set of all integer types forms the integer “class” of types in Ada 83. Similarly, the set of all real types forms the real “class.”

For Ada 95, the concept of “class” is generalized to refer to any set of types formed from the direct and indirect derivatives of a given type. Furthermore, the revision team generalized the universal numeric type concept to the concept of a “class-wide” type, which can be used to define universal or “class-wide” operations that are as convenient to use with any type in a class, as are integer literals with any type in the integer class.

Such a generalized “class-wide” type is named using attribute notation: for any “specific” type T, the “class-wide” type T’CLASS exists and may be used to define class-wide operations, and class-wide access types. For example, given our Basic_Window type above, the class-wide type Basic_Window’CLASS exists, and may be used to define operations that manipulate objects of type Basic_Window or any type derived from it, as follow:

      
       procedure Flash_Window(W : Basic_Window'CLASS) is
		(This "class-wide" operation flashes a window
		by using its Hide and Display primitive operations
		repeatedly.) 
       begin 
           Hide(W); 
           Display(W); 
           Hide(W); 
           Display(W); 
       end Flash_Window; 

Because the formal parameter type is Basic_Window’CLASS, the Flash_Window operation may be applied to any object whose type is in the “class” of type descended from Basic_Window. Note that the implementation of a class-wide operation normally uses the “primitive” operations of the “root” type (Basic_Window), in this case, Hide and Display.

“Run-time polymorphism” occurs whenever an object of a class-wide type is passed to one of the primitive operations — an automatic dispatch is performed to the “appropriate” implementation of the primitive operation. In this example, on each call of Hide and Display with the class-wide parameter W, an indirect call is made through a dispatch table identified at run- time by a “type tag” carried with each class-wide object.

The difference between specific types and class-wide types is a distinguishing feature of Ada 95. In Ada 83, user-defined types are always “specific” types, and when used as a formal parameter type, the actual parameter must have exactly the same type. In most object-oriented programming languages, all types in a type hierarchy are “class-wide,” and when used as a formal parameter type, the actual parameter may be of that type or any of its derivatives. For Ada 95, these two concepts are explicitly distinct, to provide exact type matching by default, while also supporting class-wide type matching when explicitly desired.

To summarize the Ada 95 support for object-oriented programming: record and private types may be extended as part of type derivation to support general single inheritance; class-wide types provide class-wide type matching, and use automatic dispatching when passed to a primitive operation to provide run- time polymorphism.

Programming in the Large

Ada 83 has been successfully used in the development of very large programs, including some programs with more than one million lines of code. However, there can be serious practical problems, when developing such large systems, associated with small changes resulting in large amounts of recompilation or relinking, due to the requirement for strong type checking across separately compiled program units. Even with a very fast compiler, it may be impractical to recompile or relink certain subsystems, due to configuration management and strict quality assurance and testing considerations.

For Ada 95, some of the issues associated with “programming in the large” are addressed by enhancing the separate compilation facilities, as well as by standardizing mechanisms for breaking large applications into separately linkable “partitions,” which can be loaded and elaborated independently from one another while preserving strong typing.

Ada 83 has excellent separate compilation facilities, enforcing full strong type-checking across separately compiled units of the application. However, the namespace for “library units” (the basic independently compilable unit of an application) is flat. For Ada 95, the library unit names may form a hierarchy. The program library becomes a forest of library unit trees, with each “root” library unit named with a single identifier, and “child” library units named by a sequence of identifiers separated by periods. Following up on the object-oriented programming examples given above, a hypothetical windowing user-interface subsystem is used to illustrate child library units:

      
       package Window_System is
		(This package defines the types that form the basis
		of the window system. Child Library units are used to
		provide additional types and operations.)

           type Window_Id is private; 
           type Pixel_Number is new Integer 
           subtype Pixel_Count is Pixel_Number range 0..Pixel_Number'LAST; 
      
           type Point is (A point on a two-dimensional screen.)
             record 
               X, Y : Pixel_Number; 
           end record; 
     
           type Basic_Window is tagged limited private;
		(The "root" of the hierarchy of window types.)
		(Here are the "primitive" operations of Basic_Window.
		These "dispatch" automatically when passed a class-wide
		object (of type Basic_Window'CLASS).)

           procedure Create_Window( 
             Win : in out Basic_Window; 
             Lower_Left : Point; 
             Upper_right : Point); 
      
           procedure Display(Win : in Basic_Window); 
           procedure Hide(Win : in Basic_Window); 
           function Id(Win : Basic_Window) return Window_Id; 
      
       private 
		(Here are the full declarations of the private types.)
           type Window_Id is new Integer; 
      
           type Basic_Window is tagged 
             record 
               Id : Window_Id; 
               Lower_Left : Point; 
               Upper_Right : Point; 
             end record; 
       end Window_System; 
      
       package Window_System.Bells_And_Whistles is 
		(This "child" library unit defines a
		type extension of the root window type.)

           type Fancy_Window is new Basic_Window with private; 
      
           procedure Display(FW : in Fancy_Window);
		(The inherited Display primitive operation
		is being overridden.)

       private 
           type Fancy_Window is new Basic_Window with 
             record 
               . . . (as in earlier example)
             end record; 
       end Window_System.Bells_And_Whistles; 
      
       with Window_System.Bells_And_Whistles; 
       procedure Test is
		(This procedure uses the child Library unit.)
           Win : Window_System.Bells_And_Whistles.Fancy_Window; 
       begin 
           ... 
       end Test; 

Going along with this hierarchical name space enhancement is the generalization of the concept that units logically nested within a parent unit may “see” the private declarations of the parent unit, and thereby effectively extend the functionality of the private types declared in the parent unit. By using child library units, the set of operations of a private type may be broken up into logical groupings, rather than being provided in a single monolithic package.

The hierarchical name space allows a very large application to be organized into a set of subsystems, each composed of a tree of library units. Furthermore, when a subsystem needs to be extended to service additional clients, one may add child library units rather than edit preexisting library units, thereby eliminating any disturbance (or recompilation) of existing clients of the subsystem. Furthermore, by keeping individual library units smaller and placing logically separable functionality in child library units, changes to child library units will affect only those clients interested in that particular part of the subsystems.

In addition to adding flexibility to library unit naming, flexibility has also been added to the definition of a “program.” In Ada 83, all of the library units of a program were loaded and elaborated together. However, for large systems, it is often more appropriate for a conceptual “program” to actually be composed of independently loaded and elaborated pieces. For Ada 95, this concept is formalized, using the term “partition” to represent the unit of a system that is loaded and elaborated at one time, while explicitly allowing such partitions to communicate with other partitions using shared packages and (remote) subprogram calls. This approach preserves the benefits of strong type checking, while allowing program partitions to be loaded and updated incrementally. This can be critical for a long-running system, as well as for a fault-tolerant system.

In the “core” of the language standard, the revision allows programs to be broken up into independently elaboratable partitions. In an annex devoted to support for distribution, there is a set of pragmas to provide a standard set of capabilities for partitioning programs. In the annex, two kinds of partitions are defined: “passive” partitions, consisting of shared data, but no thread of control; and “active” partitions, consisting of one or more threads of control, communicating with other active partitions using remote subprogram calls and data stored in passive partitions.

Real-Time and Parallel Programming

Ada 83 is one of the few “mainstream” programming languages with support for multiple threads of control (tasks) incorporated into the language syntax. Ada also incorporates syntactic support for high-level synchronization between tasks, based on the “rendezvous” concept. For many application areas, the synchronous task-to-task communication capability inherent in the rendezvous has been adequate for building multi-tasking systems. However, in other application areas, a lower-level or asynchronous communication capability is required.

Ada 83 does not preclude the provision of efficient synchronous communication capabilities through the use of implementation-defined packages or specialized pragmas. However, the fact that only synchronous communication is supported directly in the language has led to some user dissatisfaction, since the approaches based on implementation-defined packages or specialized pragmas are inherently less portable and generally less elegant.

For Ada 95, language support for synchronous communication is provided by a data-oriented synchronization “building block” called a “protected record,” to complement the synchronous task- to-task communication capability represented by rendezvous. A protected record is a combination of record components plus “protected operations,” which have exclusive access to the components. A protected record type is specified by a program unit that defines the components and the protected operations that are either read-only “protected functions,” read/write “protected procedures,” or potentially suspending entries (analogous to task entries). Here is an example of a “mailbox” protected record type, defined in a generic package:

       generic 
           type Message_Type is private; 
       package Mailbox_Pkg is 
           type Message_Array is array(Positive range <>) of Message_Type; 
      
           protected type Mailbox(Size : Natural) is 
               function Count return Natural;
		(Count of messages in the Mailbox.)
               procedure Discard_All; 
		(Discard all messages in the Mailbox.)
               entry Put(Message : in Message_Type); 
		(Put a message into the mailbox;
		suspend until there is room.)
               entry Get(Message : out Message_Type); 
		(Get a message from the mailbox;
		suspend until there is a message.)
           private record 
		(Define the components of the protected record.)
               Contents : Message_Array(1..Size); 
		(This array holds the messages.)
               Current_Count : Natural := 0; 
		(Count of messages in the mailbox.)
               Put_Index : Positive := 1; 
		(Index into array for adding next message.)
               Get_Index : Positive := 1; 
		(Index into array for moving next message.)
           end Mailbox; 
       end Mailbox_pkg; 

This generic package may be implemented as follows:

       package body Mailbox_Pkg is 
      
           protected body Mailbox is 
               function Count return Natural is 
		(Count of messages in the Mailbox.)
               begin 
                 return Current_Count 
           end Count; 
      
           procedure Discard_All is 
             Discard all messages in the mailbox 
           begin 
               count := 0; 
		(Reset the array indices as well.) 
               Put_Index := 1; 
               Get_Index := 1; 
           end Discard_All; 
      
           entry Put(Message : in Message_Type) when Count < Size is 
		(Put a message into the mailbox. 
		"Count ≶ Size" is the "barrier" for this entry. 
		Any caller is suspended until this is true.)
           begin 
		(Insert new message, bump index (cyclicly) 
		and increment counter.)
               Contents(Put_Index) := Message; 
               Put_Index := Put_Index mod Size + 1; 
               Count := Count + 1; 
           end Put; 
      
           entry Get(Message : out Message_Type) when Count > 0 is 
		(Get next message from the mailbox. 
		"Count > 0" is the "barrier" for this entry. 
		Any caller is suspended until this is true.)
           begin 
		(Get next message, bump index (cyclicly) 
		and decrement counter.)
               Message := Contents(Get_Index); 
               Get_Index := Get_Index mod Size + 1; 
               Count := Count - 1; 
           end Get; 
         end Mailbox; 
       end mailbox_Pkg; 

As illustrated above, each protected operation specified in the protected type definition must have a body provided in the protected body. Furthermore, each entry body must have an “entry barrier” specified after the reserved word when. Any caller of any entry is automatically suspended if the entry barrier is false, on an entry queue associated with the entry.

Upon completion of a protected procedure or entry, the components of the protected may have changed, so the entry barriers are rechecked to see if one of them has become true. If so, the first caller on the associated entry queue is selected and the entry body is executed for the caller. This process of “servicing the entry queues” continues until there are no more callers on entries with true barriers.

To use the generic package from the above example, one instantiates it with some message type, and then declares an instance of the Mailbox type. Protected operations are performed by using a subprogram call syntax, but with the protected record object identified by a prefix to the operation name. This is illustrated in the following example:

      
       with Text_IO, Mailbox_Pkg; 
       procedure Test is 
           type Line is record 
		(The type to be used for messages.)
               Length : Natural := 0; 
               Data : String(1..80); 
           end record; 
      
           package Line_Buffer_Pkg is 
             new Mailbox_Pkg(Message_Type => Line); 
   
           Line_Buffer : Line_Buffer_Pkg.Mailbox(Siz => 20); 
		(Instance of mailbox with room for 20 messages.)
    
           task Producer;  -- Task that will put messages into mailbox 
           task body Producer is 
               L : Line; 
           begin 
               for I in 1..100 loop 
                   Text_IO.Get_Line(L.Data, L.Length) 
                     -- Read a line from Standard_Input 
                   Line_Buffer.Put(L);  -- Entry call to put message 
               end loop; 
           end Producer; 
      
           task Consumer; (Task that will get message out of mailbox.)
           task body Consumer is 
               L : Line; 
               C : Natural; 
           begin 
               for I in 1..100 loop 
                   Line_Buffer.Get(L);  (Entry call to get message.)
                   Text_IO.Put_Line(L.Data(1..L.Length)); 
		(Write the line to Standard_Output.)


		(Check if Consumer is falling way behind Producer.)
                   C := Line_Buffer.Count(L);  (Protected func call.)
                   if C > Line_Buffer.Size/2 then 
		(Report that Consumer is falling way behind.)
                       Text_IO.Put_Line("****Buffer count now =" & 
                         Integer'IMAGE(C)); 
                   end if; 
               end loop; 
           end Consumer; 

       begin 
           null;  (Wait for tasks to complete.)
       end Test; 

Entry calls on protected record entries may be used anywhere; (task) entry calls are permitted in Ada 83, but in conditional and timed entry call statements in particular.

Protected records, with their support for efficient mutual exclusion and asynchronous signaling via entry barriers, are the most important addition to Ada 95 for real-time and parallel programming. There are other enhancements, too, including a more general form of selective entry call and a requeue capability so that a server can examine the parameters of an entry call before committing to servicing the call. Also, a real-time “annex” has been proposed where standardized packages and pragmas are defined to provide dynamic priority control, a “monotonic” real-time clock, and a CPU time accounting capability.

This flyer is based on an article written by S. Tucker Taft, Ada 95 Mapping/Revision Team.

Share and Enjoy:
  • email
  • LinkedIn
  • Twitter
  • Facebook
  • Digg
  • RSS